# Creating a document

> Every option on POST /documents — files, recipients, fields, settings, drafts, and the transient status that catches everyone once.

Source: https://signclad.com/docs/creating-a-document/

`POST /documents` is the single call that turns files into a document and sends it. Everything else on the sending side is a variation of it.

## The minimum

```json
{
  "name": "Loan agreement",
  "files": [
    { "name": "loan-agreement.pdf", "file_url": "https://files.example.com/loan-agreement.pdf" }
  ],
  "recipients": [
    { "id": "signer-1", "name": "Jane Doe", "email": "jane@example.com" }
  ],
  "text_tags": true
}
```

With `text_tags: true`, the fields come out of the document itself. Without it, pass a `fields` array.

## Sending the call

The same request in curl, Node, and Python. Everything else on this page is a property of the body
these three send.

```bash
curl -X POST https://api.signclad.com/api/v1/documents \
  -H "X-Api-Key: $SIGNCLAD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d @document.json
```

```js
import { readFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';

const response = await fetch('https://api.signclad.com/api/v1/documents', {
  method: 'POST',
  headers: {
    'X-Api-Key': process.env.SIGNCLAD_API_KEY,
    'Idempotency-Key': randomUUID(),
    'Content-Type': 'application/json',
  },
  body: await readFile('document.json', 'utf8'),
});

if (!response.ok) {
  // 422 bodies are `{ "errors": { field: "message" | { code: "message" } } }`.
  throw new Error(`${response.status}: ${await response.text()}`);
}
const { id, status } = await response.json();
```

```python
import json
import os
import uuid

import requests

with open("document.json") as handle:
    body = json.load(handle)

response = requests.post(
    "https://api.signclad.com/api/v1/documents",
    headers={
        "X-Api-Key": os.environ["SIGNCLAD_API_KEY"],
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json=body,
    timeout=30,
)
if response.status_code >= 400:
    raise RuntimeError(f"{response.status_code}: {response.text}")
document = response.json()
```

Log `X-Request-Id` from the response headers on both paths. It is the one value support can trace.

## Files

Three ways to supply a file, and one document may mix them:

| Property | Use when |
|---|---|
| `file_url` | Your server can hand out a URL Signclad can fetch — a presigned S3 link, for example. Signclad fetches it once, through SSRF protection. |
| `file_base64` | The file is small and you would rather not host it. |
| `file_id` | You uploaded it first with `POST /files` (multipart). Good for large files and for reusing the same file across documents. |

Exactly one of the three per file — sending two is a `422` on `files`. Several files concatenate in order into one document, up to 20 of them, 25 MB each and 500 pages in total.

Formats: **PDF, `.doc` and `.docx`**, and nothing else. The type is sniffed from the bytes rather than taken from the name, so a file called `contract` is fine and a `.pdf` that is really a spreadsheet is not; anything outside those three is refused with "Only PDF and Word (.doc, .docx) files are supported."

A PDF that already carries form fields has the widgets Signclad can map to a field type imported as editable Signclad fields. Widget types it cannot map are flattened into the page appearance instead, and the file records a warning saying which.

## Recipients

```json
{
  "id": "signer-1",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "role": "signer",
  "send_email": true,
  "send_email_delay": 0,
  "passcode": "4821",
  "subject": "Your loan agreement",
  "message": "Your copy of the agreement."
}
```

- `id` is **yours**. Fields reference recipients by it, and it comes back unchanged.
- Duplicate email addresses are accepted. One person signing in two roles is a real case, and recipients are rows rather than users.
- A recipient whose email matches the requester's is accepted too.
- `role: "cc"` marks someone who receives the completed document but does not sign. The default is `signer`.
- `signing_order` is **response-only**. Recipients are numbered by the order you send them, always, and `apply_signing_order` decides whether that order is enforced rather than whether it is reported. Sending a `signing_order` of your own changes nothing.
- `send_email` defaults to true on a normal document and false on an embedded one, and is honoured either way. `send_email_delay` holds an invitation for up to 60 minutes.
- `subject` and `message` override the document's for this one recipient. `language` is a document setting, not a recipient one.
- A passcode is at least 4 characters. Turning on `passcode_delivery.enabled` without a passcode stored or supplied is a `422`: it would produce a document nobody can open.

## Fields

`fields` is an array of arrays — one inner array per file, in the same order as `files`.

```json
{
  "api_id": "borrower_signature",
  "type": "signature",
  "recipient_id": "signer-1",
  "page": 1,
  "x": 375,
  "y": 206,
  "width": 180,
  "height": 44,
  "required": true
}
```

Coordinates are in **API units**, not PDF points: CSS pixels at 96 DPI, one unit being exactly 0.75 pt, with the origin at the top left of the page and `y` increasing downward. A US Letter page is 816 x 1056 units. Multiply by `0.75` to get points.

Types: `signature`, `initials`, `date`, `text`, `checkbox`, `dropdown`, `select`, and the autofill family — `autofill_name`, `autofill_first_name`, `autofill_last_name`, `autofill_email`, `autofill_company`, `autofill_phone`, `autofill_title`, `autofill_date_signed`. There is no bare `autofill` type and no `attachment` type; files a signer must upload are `attachment_requests`, not fields.

Field notes worth knowing:

- `api_id` is your stable handle. Use it to prefill values later and to read values back. Make it meaningful.
- `height` is capped at 200 units for `signature` and `initials` and 74 for everything else.
- `lock_sign_date: true` on a date field fills it at signing and refuses to be set. If you send a value anyway, Signclad ignores it and adds an entry to `warnings[]` rather than failing the request.
- Validation lives on text fields, and it is a plan feature: `validation` needs the entitlement that starts on Business, and a document that carries one on a lesser plan is refused with `not_available_on_plan`. The accepted values are `no_text_validation`, `numbers`, `letters`, `email_address`, `us_phone_number`, `us_zip_code`, `us_ssn`, `us_age`, `alphanumeric`, `us_bank_routing_number`, `us_bank_account_number`, and `custom` — which uses `validation_regex` and `validation_regex_error_message`.
- `date_format` is one of `MM/DD/YYYY`, `DD/MM/YYYY`, `YYYY/MM/DD`, `Month DD YYYY`, `MM/DD/YYYY hh:mm:ss a`.
- On the response, `width` and `height` come back as decimal strings while `x`, `y`, and `page` stay numbers.

## Document settings

| Field | Effect |
|---|---|
| `draft` | Keeps it editable and returns a document-level `embedded_edit_url` instead of sending. |
| `subject`, `message` | The invitation email. `message` accepts limited HTML. |
| `apply_signing_order` | Invite recipients in order rather than all at once. |
| `reminders` | A boolean. On by default, at 3, 6 and 10 days after sending. |
| `expires_in` | Days until the links stop working and the document is marked expired. 1 to 365. |
| `allow_decline`, `allow_reassign` | Whether a recipient may refuse or hand it on. Both default to true. |
| `language` | One of fifteen, per document: `en`, `fr`, `es`, `de`, `pl`, `pt`, `da`, `nl`, `it`, `ru`, `sv`, `ar`, `el`, `tr`, `sk`. |
| `embedded_signing` | Makes recipient URLs embeddable. |
| `custom_requester_name`, `custom_requester_email` | Send on behalf of someone else — the usual case when your product sends for its own users. |
| `api_application_id` | Applies that application's branding and allowed domains. |
| `metadata` | Up to 50 key/value pairs, returned unchanged. Values **must** be strings, at most 500 characters each. |
| `with_signature_page` | Appends a generated signature page with a field per signer. |
| `test_mode` | Marks it as a test document even with a live key. |

Conditional rules and attachment requests are plan features too, starting on Business, and at most 10 conditional rules per document.

Unknown properties are dropped silently. Almost no request is rejected for carrying a key this document does not define, and unknown keys are not echoed back either. The one exception is `recipients[].passcode_delivery.otp`, which is refused outright rather than ignored: it named a factor that was never minted or checked, and accepting it would let you configure something that does not exist.

## The response

```json
{
  "id": "8f14e45f-ea8f-4b2c-9f0e-1b2c3d4e5f60",
  "status": "Created",
  "files": [{ "file_id": "0a1b2c3d-...", "name": "loan-agreement.pdf", "pages_number": 0, "status": "uploaded" }],
  "recipients": [
    { "id": "signer-1", "status": "created", "signing_order": 1,
      "embedded_signing_url": "https://app.signclad.com/sign/embedded/9c8f2b1a4d6e?token=..." }
  ],
  "warnings": []
}
```

**Do not drive a state machine off this.** `status` is the transient `Created` whatever `draft` was, `pages_number` is `0`, and `files[].status` is whatever ingest has reached — `uploaded`, then `processing`, then `ready`. Read the document again for settled values. The same applies to `POST /documents/{id}/send` and `POST /document_templates/documents`, which both answer `Draft`.

## Safe retries

Put an `Idempotency-Key` on every create. It is scoped to the credential plus method plus path and kept 24 hours, so a retry after a timeout returns the original response instead of creating a second document. A request that is still in flight answers `409 idempotency_in_progress`, and the same key with a different body answers `422 idempotency_key_reused`.

A request that throws or answers `5xx` releases the key, so the retry you were told to make actually does the work.

## Editing a draft

`PATCH /documents/{id}` changes a draft's name, subject, message, settings, recipients, or fields.

Once a document is sent, its files and fields are frozen — that is what makes the revision manifest worth anything. Two corrections are still possible while it is `Sent`, `Viewed` or `Pending`: `PATCH /documents/{id}/recipients` fixes a recipient who has not started signing yet (a typo in an address, a passcode), and `PATCH /documents/{id}/authentication` changes the gate. A recipient who has already viewed or signed can have nothing but their passcode changed, and asking answers `409 document_not_active`.

## After sending

| Call | Does |
|---|---|
| `POST /documents/{id}/remind` | Nudges whoever has not signed. `reminded` in the response names who it reached, which can be nobody. |
| `POST /documents/{id}/cancel` | Stops it. Links die; the audit trail records why. |
| `POST /documents/{id}/complete` | Marks it complete manually, where your process allows. |
| `POST /documents/{id}/recipients/{rid}/reassign` | Moves it to the right person. |
| `POST /documents/{id}/recipients/{rid}/signing_url` | Fresh signing links for one recipient, no recreation. Optional `expires_in_minutes`, 1 to 1440, default 15. |
| `GET /documents/{id}/events` | The audit trail as structured data. |
| `GET /documents/{id}/completed_pdf` | The sealed file. `404` until every signer has finished. |

## Related

- [Templates and create-from-template](https://signclad.com/docs/templates/)
- [Text tags](https://signclad.com/docs/text-tags/)
- [Errors and rate limits](https://signclad.com/docs/errors-and-rate-limits/)
