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."
}
idis 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 issigner.signing_orderis response-only. Recipients are numbered by the order you send them, always, andapply_signing_orderdecides whether that order is enforced rather than whether it is reported. Sending asigning_orderof your own changes nothing.send_emaildefaults to true on a normal document and false on an embedded one, and is honoured either way.send_email_delayholds an invitation for up to 60 minutes.subjectandmessageoverride the document’s for this one recipient.languageis a document setting, not a recipient one.- A passcode is at least 4 characters. Turning on
passcode_delivery.enabledwithout a passcode stored or supplied is a422: 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_idis your stable handle. Use it to prefill values later and to read values back. Make it meaningful.heightis capped at 200 units forsignatureandinitialsand 74 for everything else.lock_sign_date: trueon 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 towarnings[]rather than failing the request.- Validation lives on text fields, and it is a plan feature:
validationneeds the entitlement that starts on Business, and a document that carries one on a lesser plan is refused withnot_available_on_plan. The accepted values areno_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, andcustom— which usesvalidation_regexandvalidation_regex_error_message. date_formatis one ofMM/DD/YYYY,DD/MM/YYYY,YYYY/MM/DD,Month DD YYYY,MM/DD/YYYY hh:mm:ss a.- On the response,
widthandheightcome back as decimal strings whilex,y, andpagestay 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. |