Compatibility reference
Measured wire behaviour Signclad reproduces on purpose: status casing, transient create statuses, the "t" checkbox literal, the 0.75 pt coordinate unit, and the error envelopes — with the quirks it deliberately does not reproduce.
Updated
On this page
Signclad’s /api/v1 is wire-compatible with an existing third-party eSignature API, here called
the compatibility target. Every operation that is not marked x-signclad-extension: true in
the OpenAPI document has the same path, method, request fields,
response fields, status strings, error envelopes, and webhook event names. A working client against
that API works against Signclad after changing two things: the base URL and the API key.
This page is the behavioural half of that promise — the part OpenAPI cannot express. It was measured, not inferred: recorded against the compatibility target’s live API in test mode, then reproduced and pinned by Signclad’s own test suite. If you are porting an integration, read this before you read anything else; most of what breaks in a migration is on this page.
Conventions
| Item | Signclad |
|---|---|
| Base URL | https://api.signclad.com/api/v1 |
| Auth | X-Api-Key: <token> — sandbox keys are prefixed sk_test_, live keys sk_live_ |
| Trailing slashes | Every path routes with and without one. /documents/{id} and /documents/{id}/ return byte-identical bodies. |
| Unknown request keys | Silently dropped. Never rejected, never echoed back. Every request body is additionalProperties: true. |
| Success codes | 200 on GET/PUT/PATCH, 201 on create/send/remind, 204 on DELETE |
| Rate limits | Three buckets: 120/min general, 10/min creates and mutations, 50/min unauthenticated. Failed creates still charge the create bucket. |
x-ratelimit-reset |
An ISO 8601 timestamp, not a seconds count. The additive RateLimit-Reset is in seconds. |
| Pagination | The compatibility surface paginates only bulk-send documents: limit 1–50 (default 10), page ≥ 1, with current_page, next_page, previous_page, total_count, total_pages. Extension list endpoints are cursor-paginated instead. |
| IDs | UUIDs for server objects. Recipients, placeholders, and field api_ids are caller-supplied strings. |
metadata |
Up to 50 pairs. Keys under 40 characters, values under 500, values must be strings. |
Statuses
Document and template statuses are capitalised. Recipient statuses are lowercase. They are not the same vocabulary, and no amount of wishing makes them one.
- Documents:
Draft, Created, Sending, Sent, Pending, Viewed, Completed, Manually completed, Declined, Canceled, Bounced, Blocked, Error, Expired. - Templates:
Created, Draft, Available. Only anAvailabletemplate can spawn a document. - Recipients:
created, draft, sent, viewed, completed, declined, bounced. A finished recipient iscompleted, neversigned.
Create and send answer with a transient status
POST /documents and POST /document_templates answer "Created" regardless of what you passed
for draft. POST /documents/{id}/send and POST /document_templates/documents answer "Draft".
The settled status appears only on the next GET.
This catches every integration once. If your code reads status off a create response and decides
anything from it, it is reading a placeholder. Read the record back, or wait for the webhook.
Field serialisation on the wire
These are the details that make a naive port fail its first test:
widthandheightcome back as strings ("200.0","50.0").x,y, andpageare numbers.- A checked checkbox serialises as
"t", the Postgres boolean literal — nottrue. files[].pages_numberis0in the create response and carries the real count on the nextGET. Page counting is asynchronous. The extension fieldfiles[].statusisprocessinguntil ingest finishes.GETreorders fields by type. The create response echoes your request order; a laterGETregroups the same fields by field type. Never key off array index — useapi_id.- There is no document-level
embedded_signing_url, not even on a document with exactly one embedded signer. Onlyrecipients[].embedded_signing_urlexists. signing_urlis regenerated on send. A recipient URL captured from a draft response is dead once the document is sent. Mint a fresh one rather than caching it.completed_pdf_urlis not returned on the document object, even when the document isCompleted. FetchGET /documents/{id}/completed_pdffor the bytes.
Coordinates
The origin is the top-left of the visible page, with y increasing downward, and
1 API unit = 0.75 PDF points — CSS pixels at 96 DPI, so a US Letter page is 816 × 1056 units.
The signing viewer lays the page out at exactly that size and positions each field at its API
x/y 1:1.
Size limits: signature and initials fields up to 200 units tall, every other field up to 74.
The compatibility target’s own completed-PDF stamper measures 0.7528125 pt per unit — 0.37% larger, up to about 2.3 pt of drift across a letter page. That is a rendering wart, not the intended model. Signclad implements 0.75 and does not reproduce the drift, so a field lands in the same place in the signing view and in the sealed PDF.
Error envelopes
401 — a JSON body, not a plain string:
{
"message": "Missing or invalid authorization key",
"meta": {
"error": "api_key_unauthorized_error",
"message": "Not valid authorization token",
"messages": ["Not valid authorization token"]
}
}
404 — and GET /documents/{id}/completed_pdf before completion returns exactly this, byte for
byte. There is no marker distinguishing “not finished yet” from “no such document”, so a retry loop
must treat 404 on that endpoint as “not ready”, not as “gone”:
{
"message": "Not found",
"meta": {
"error": "record_not_found",
"message": "Couldn't find the document requested",
"messages": ["Couldn't find the document requested"]
}
}
422 — {"errors": {…}}, where each value is either a string or a nested object keyed by an
error code (or by a per-item key, then a code). No arrays appear anywhere. Model it as
string | { [code_or_item]: string | object }:
{ "errors": { "files": "At least one file should be present." } }
{ "errors": { "recipients": { "duplicated_emails": "These emails are duplicated: [email protected]." } } }
{ "errors": { "template_fields": { "tdate1": { "invalid_date_format": "DateField value must be in Iso8601 format." } } } }
The last one is keyed by the field’s api_id, then by the error code, and Iso8601 is capitalised
exactly so. template_fields dates need a full ISO 8601 value (2026-07-02T00:00:00Z); a bare
Y-m-d is rejected. The accepted value comes back rendered through the field’s date_format.
See Errors and rate limits for the full catalogue, including the Signclad-only fields added alongside these.
Test mode does not suppress email
Test mode excludes a document from billing and watermarks it as not legally binding. It does not
stop mail going out. A send to an address that does not exist really bounces: the document moves
to Bounced while the recipients stay sent with bounced: true. send_email: false on a
non-embedded document is echoed back as false and the mail still goes — the flag is not an
indicator of delivery.
The only genuinely mail-free send is embedded_signing: true together with send_email: false.
Use addresses you control for everything else. Sandbox and test mode
covers the rest.
Webhook events
The preserved event names are document_created, document_sent, document_viewed (every view,
not just the first), document_in_progress (once), document_recipients_updated,
document_signed (per recipient), document_completed, document_expired, document_canceled,
document_declined, document_bounced, document_error, template_created, and
template_error.
The compatibility payload is:
{
"event": { "hash": "…", "time": 1789050255, "type": "document_completed",
"related_signer": { "email": "[email protected]", "name": "Jane Doe" } },
"data": { "object": { "…": "the full document or template" }, "account_id": "…" }
}
hash is hex(HMAC-SHA256(key = webhook id, data = "<type>@<time>")). It authenticates nothing
about the body: there is no body signature and no timestamp tolerance in that scheme. Signclad
accepts it for compatibility and adds a real one — X-Signclad-Signature, an HMAC over the raw
body with a timestamp — which is what you should verify against. Signclad’s data also carries
workspace_id beside account_id, and its events carry a stable event.id to dedupe on.
Webhooks has both schemes in full, and Webhook events is the complete catalogue, generated from the specification, with every additive event marked as such.
Text tags
Signclad’s preferred syntax is [type|modifier|modifier], for example
[sig|signer1|req|id:borrower_signature].
For migration, the compatibility target’s positional syntax —
{{type:signer:required:label:prefill:api_id:width:height}}, with blanks keeping defaults
({{signature::y}}) — is also accepted, including its long and short type names, its text and date
trailing options, and its variables ({{set=a1:text:1:y:Age}} followed by {{a1}}). Both formats
may appear in the same file. Enable either with text_tags: true on the document or the template.
See Text tags.
What Signclad deliberately does not reproduce
Compatibility is not bug-for-bug. These are the intentional divergences:
- The coordinate drift. 0.75 pt per unit everywhere, in the viewer and in the sealed PDF.
- Excluding a role that has fields.
exclude_placeholdersnaming a role with assigned fields is a422rather than a silent drop of the recipient and its fields. - A single bad prefill failing the whole request. Send
skip_invalid_fields: trueand invalidtemplate_fieldsentries are reported inwarnings[]instead. - Unsigned webhooks. The compatibility hash is still emitted; a real body signature is emitted alongside it.
- Silent webhook failure. Endpoints carry health states, a delivery log, and replay.
Related
- Migrating to Signclad — the staged plan, and per-vendor mapping.
- Creating a document — every option on the create call.
- API reference — the generated reference, one page per endpoint group.