# Templates and create-from-template

> Building a template, filling its roles, prefilling fields, excluding placeholders, composing several templates, and replacing the file without redrawing fields.

Source: https://signclad.com/docs/templates/

A template holds a file, a set of roles, the fields belonging to those roles, and the document settings. Creating a document from one is a matter of saying who fills each role.

## Creating a template

```json
{
  "name": "Onboarding agreement",
  "draft": true,
  "text_tags": true,
  "files": [{ "name": "onboarding.pdf", "file_url": "https://files.example.com/onboarding.pdf" }],
  "placeholders": [{ "id": "borrower", "name": "Borrower" }],
  "fields": [[
    { "api_id": "borrower_signature", "type": "signature", "placeholder_id": "borrower",
      "page": 1, "x": 375, "y": 206, "width": 180, "height": 44 }
  ]]
}
```

`draft: true` returns an `embedded_edit_url` that opens the template builder, so a person can place fields visually rather than you computing coordinates. `PUT /document_templates/{id}` with `{ "draft": false }` publishes it, and unlike the create it answers the **settled** status.

Statuses: the create answers the transient `Created`; a later `GET` reads `Draft` or `Available`. Only an `Available` template can be used.

Field coordinates are API units — CSS pixels at 96 DPI, one unit being 0.75 PDF points, origin at the top left of the page. The same units the document API uses.

## Using a template

```json
{
  "template_id": "3f1c9a2e-6b7d-4c8e-9a0b-1c2d3e4f5a6b",
  "draft": false,
  "embedded_signing": true,
  "recipients": [
    { "id": "signer-1", "placeholder_name": "Borrower",
      "name": "Jane Doe", "email": "jane@example.com", "send_email": false }
  ],
  "exclude_placeholders": ["Co-borrower"],
  "template_fields": [
    { "api_id": "borrower_full_name", "value": "Jane Doe" },
    { "api_id": "borrower_start_date", "value": "2026-07-02T00:00:00Z" }
  ],
  "conditional_rules_mode": "append",
  "skip_invalid_fields": true,
  "metadata": { "loan_id": "48213" }
}
```

Send it to `POST /document_templates/documents`:

```bash
curl -X POST https://api.signclad.com/api/v1/document_templates/documents \
  -H "X-Api-Key: $SIGNCLAD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "3f1c9a2e-6b7d-4c8e-9a0b-1c2d3e4f5a6b",
    "draft": false,
    "recipients": [
      { "id": "signer-1", "placeholder_name": "Borrower",
        "name": "Jane Doe", "email": "jane@example.com" }
    ],
    "template_fields": [{ "api_id": "borrower_full_name", "value": "Jane Doe" }]
  }'
```

```js
import { randomUUID } from 'node:crypto';

const response = await fetch(
  'https://api.signclad.com/api/v1/document_templates/documents',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.SIGNCLAD_API_KEY,
      'Idempotency-Key': randomUUID(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      template_id: '3f1c9a2e-6b7d-4c8e-9a0b-1c2d3e4f5a6b',
      draft: false,
      recipients: [
        { id: 'signer-1', placeholder_name: 'Borrower', name: 'Jane Doe', email: 'jane@example.com' },
      ],
      template_fields: [{ api_id: 'borrower_full_name', value: 'Jane Doe' }],
    }),
  },
);

if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const document = await response.json();
```

```python
import os
import uuid

import requests

response = requests.post(
    "https://api.signclad.com/api/v1/document_templates/documents",
    headers={
        "X-Api-Key": os.environ["SIGNCLAD_API_KEY"],
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "template_id": "3f1c9a2e-6b7d-4c8e-9a0b-1c2d3e4f5a6b",
        "draft": False,
        "recipients": [
            {
                "id": "signer-1",
                "placeholder_name": "Borrower",
                "name": "Jane Doe",
                "email": "jane@example.com",
            }
        ],
        "template_fields": [{"api_id": "borrower_full_name", "value": "Jane Doe"}],
    },
    timeout=30,
)
response.raise_for_status()
document = response.json()
```

Points worth knowing:

- **`placeholder_name` binds a recipient to a role.** Every field belonging to that role follows.
- **`exclude_placeholders`** drops a role by name, and its fields come off the document. This is how one template covers the single-signer and the co-signer case.
- **`template_fields`** prefills by `api_id`. Dates accept both ISO 8601 and bare `Y-m-d`; anything else is `invalid_date_format`, keyed by the `api_id` that failed.
- **`conditional_rules_mode`** is `replace`, `append`, or `none`.
- **`skip_invalid_fields: true`** drops entries that will not validate and lists them in `warnings[]` — as `template_field_skipped` — instead of failing the whole request. Without it, one bad value fails everything.
- **`files`** on this call are *appended* to the template's own files, never a replacement for them.
- The response status is the transient `Draft`. Read it back for the settled value.

## Composing several templates

Pass `template_ids` instead of `template_id`:

- Files concatenate in the order given.
- Placeholders with the same name merge into one recipient, so a packet of four templates is one signing session.
- Settings come from the first template unless the request overrides them.
- Field identity is the pair (source template, `api_id`). Every field in the response carries `template_id`, and conditional rules and checkbox groups resolve within their own source template.
- `template_fields` accepts `{api_id, value}` while the `api_id` is unique across the set, and requires `{template_id, api_id, value}` when it is not. An ambiguous bare reference is a `422` with `ambiguous_field_reference` and the candidates listed.

## Duplicating

```http
POST /document_templates/{id}/duplicate
{ "name": "Onboarding agreement (2027)" }
```

Copies the whole template — fields, coordinates, rules, groups, settings — and returns the new one. No reading and rebuilding.

## Template links

Publishing a template as a link gives you a public URL anyone can open to start their own copy. Each visitor's copy is a separate document with its own recipients, its own audit trail, and its own sealed file. The settings live on their own prefix — `GET` and `PUT /template_links/{template}` — and the feature starts on Light.

`POST /template_links/{template}/rotate` replaces the token, which is the only way to revoke a published link: the old URL stops working immediately, everywhere it was printed or pasted. Copies already in flight are ordinary documents and are unaffected.

## Replacing the file, keeping the fields

When the underlying document changes — legal edits a clause, the numbering shifts — you do not have to redraw the fields.

```http
PUT /document_templates/{id}/files/1
{
  "name": "loan-agreement-v2.pdf",
  "file_url": "https://files.example.com/loan-agreement-v2.pdf",
  "strategy": "anchors",
  "scale_to_page": true,
  "dry_run": true
}
```

| Strategy | Behaviour |
|---|---|
| `keep_positions` | Fields stay at the same coordinates. Right when only wording changed within lines. |
| `text_tags` | Fields are re-found from the tags in the new file. Right when the file is generated from a source that carries tags. |
| `anchors` | Each field is anchored to the text beside it and follows that text. Right when the layout moved. |

`anchors` is the default — it is the only strategy that works on fields nobody tagged. `scale_to_page` defaults to true. Send exactly one of `file_url`, `file_base64`, or `file_id`.

A dry run returns a report and changes nothing:

```json
{
  "revision": 7,
  "dry_run": true,
  "requires_review": true,
  "report": {
    "kept": [{ "api_id": "borrower_signature", "page": 1, "x": 375, "y": 206 }],
    "moved": [{ "api_id": "borrower_date", "page": 1, "x": 375, "y": 289,
                "previous_page": 1, "previous_x": 375, "previous_y": 262,
                "reason": "Anchor text moved down 27 points." }],
    "scaled": [],
    "orphaned": [{ "api_id": "witness_initials", "previous_page": 5,
                   "reason": "Page 5 does not exist in the new file." }]
  }
}
```

A field is never deleted by a replacement. One with nowhere to go is reported `orphaned` and left where it was, so a person can move it. `requires_review` is true when anything orphaned, or when a field travelled more than about half an inch.

Commit by repeating the call with `dry_run: false` and `expected_revision` set to the revision the dry run reported. If the template changed in between, the commit fails with `409 revision_mismatch` rather than applying a stale plan. Leaving `expected_revision` out means "apply this to whatever the template is now", which is the race the two-step design exists to prevent.

The same endpoint exists for drafts at `PUT /documents/{id}/files/{position}`. It never applies to a sent document.

## Listing and searching

```http
GET /document_templates?q=onboarding&archived=false&limit=50&cursor=...
```

Cursor-paginated, newest first, with the same filter vocabulary as documents: `status`, `label`, `updated_since`, `metadata`, `q`, `archived`, `test_mode`. `limit` caps at 100.

Revisions have their own pair: `GET /document_templates/{id}/revisions` lists them and `POST /document_templates/{id}/revisions/{number}/restore` puts one back.

## Related

- [Creating a document](https://signclad.com/docs/creating-a-document/)
- [Text tags](https://signclad.com/docs/text-tags/)
- [Embedded builders](https://signclad.com/docs/embedded-builders/)
