Creating a document

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

Updated

On this page

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

{
  "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": "[email protected]" }
  ],
  "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.

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
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();
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

{
  "id": "signer-1",
  "name": "Jane Doe",
  "email": "[email protected]",
  "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.

{
  "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

{
  "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.