# Signclad > Document eSigning through a REST API, embedded signing, and MCP. Email delivery only. API base URL: https://api.signclad.com/api/v1 Authenticate server-side with X-Api-Key. API and MCP access require Light or above. Use sandbox keys (sk_test_) for development. Test mode never bills, delivers real emails with a [TEST] subject prefix, and watermarks documents as not legally binding; use recipient addresses you control. Never share real API keys with an AI assistant or expose them in client code. Use the OpenAPI schema for exact request and response shapes. No client library is published to npm yet, so call the API over plain HTTP or generate a client from the schema. This pack is the prose guides only — no OpenAPI document. For the complete machine-readable contract, fetch https://signclad.com/openapi/signclad.v1.yaml (or .json), or https://signclad.com/llms-full.txt for both in one file. ## Contents - LLMs.txt: integrate with AI - Getting started - Authentication and keys - Sandbox and test mode - Creating a document - Templates and create-from-template - Text tags - Embedded signing - Embedded builders and the compatibility shim - Webhooks - Errors and rate limits - MCP - Migrating to Signclad - Compatibility reference --- # LLMs.txt: integrate with AI > Download the Signclad integration docs and API schema as plain text for your AI assistant. Source: https://signclad.com/docs/llms/ Give your AI assistant the documentation it needs to build a Signclad integration. The downloads are generated from these guides and the published API schema on every site build. ## Download the docs - Download the guides pack (.txt) — every prose guide in one file, without the schema. Start here: it is an order of magnitude smaller than the full pack, and it is the half an assistant actually reads. - Download the full integration guide (.txt) — the same guides plus the complete OpenAPI schema. Large, and mostly specification, so reach for it when your assistant has the context window and needs exact field shapes inline. - Download llms.txt — a short index: the four downloads with their measured sizes, then every guide with a link to its Markdown version. Use this with an assistant that can fetch URLs, or to let it pick a file it can hold. - Download the OpenAPI schema (.yaml) — every published operation, request, response, and error schema. The same document is at /openapi/signclad.v1.json for tools that only read JSON. Both packs cover authentication, creating and sending documents, templates, text tags, embedded signing and builders, webhook delivery and verification, errors, rate limits, test mode, migration, the compatibility reference, and MCP. The sizes in `llms.txt` are measured from the files that build emitted, so they cannot drift from what you download. ## A starter prompt Attach the full integration guide, then describe your application and the workflow you need: ```text Help me integrate Signclad into my application using the attached documentation. My stack is [language and framework]. I want to [describe the document workflow and recipients]. Use the included OpenAPI schema for exact endpoints and request fields. Start with a sandbox key stored server-side in an environment variable. Use recipient addresses I control: test mode sends real emails. Add idempotency keys, error handling, and verified webhook processing. Never include API keys in browser code, logs, or the conversation. Ask about missing requirements instead of inventing API capabilities. Explain how to test the integration before switching to a live key. ``` ## Before you start API and MCP access require Light or above. Create a sandbox key in your workspace's API settings and keep it in your application's secret store. Share the documentation with your assistant, not your credentials or private documents. Test documents send real emails with a test notice and carry a watermark stating that they are not legally binding. They do not count toward billable usage. Use addresses you control while developing. No Signclad client library is published to npm, so `npm install` will not find one and an assistant that writes `import { createClient } from '@signclad/sdk'` is writing against something you cannot install. Call the API over plain HTTP, or generate a client from the OpenAPI schema — which is what the schema is there for. ## Smaller context windows Start with [llms.txt](https://signclad.com/llms.txt), then load only the Markdown guides relevant to your workflow and the API schema. Each guide has a plain Markdown version at `/docs/markdown/{guide-name}.md`, such as [Getting started](https://signclad.com/docs/markdown/getting-started.md). Links inside the downloads are rewritten to absolute URLs, so they still resolve once the file is outside the website. Downloaded files are snapshots. Download a fresh copy when revisiting an integration. For an assistant that should operate on documents directly, see the [MCP integration guide](https://signclad.com/docs/mcp/). --- # Getting started > Create a key, send your first document in test mode, and watch the events arrive. About ten minutes end to end. Source: https://signclad.com/docs/getting-started/ This walks you from nothing to a signed document. Everything here runs against a sandbox key, so emails are marked as test documents, documents are watermarked, and nothing is billed. Use email addresses you control. ## 1. Get a key Create an account and choose Light or above for API access, then open [Settings → API keys](https://app.signclad.com/app/settings/api-keys) and create a key. Sandbox keys are prefixed `sk_test_`, live keys `sk_live_`. Use the sandbox one for everything below. The key identifies both your workspace and the mode. There is no separate "test mode" flag to remember and no environment variable to get wrong — if the key starts with `sk_test_`, the request is a test request. ## 2. Send a document Signclad fetches your file from a URL, so a presigned S3 link, a public URL, or anything your server can hand out for a few minutes all work. You can also upload the bytes with `POST /files` first and pass the returned `file_id`. Documents and templates take PDF, `.doc` and `.docx` — up to 20 files per document, 25 MB each. ```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 '{ "name": "Loan agreement", "subject": "Please sign your loan agreement", "message": "

Two signatures, about a minute.

", "reminders": true, "expires_in": 30, "metadata": { "loan_id": "48213" }, "files": [ { "name": "loan-agreement.pdf", "file_url": "https://files.example.com/loan-agreement.pdf?X-Amz-Expires=7200" } ], "recipients": [ { "id": "signer-1", "name": "Jane Doe", "email": "jane@example.com" } ], "fields": [[ { "api_id": "borrower_signature", "type": "signature", "recipient_id": "signer-1", "page": 1, "x": 375, "y": 206, "width": 180, "height": 44, "required": true }, { "api_id": "borrower_date", "type": "date", "recipient_id": "signer-1", "page": 1, "x": 375, "y": 262, "width": 120, "height": 24, "lock_sign_date": true, "date_format": "MM/DD/YYYY" } ]] }' ``` The same call from Node (18+, no dependencies) and from Python with `requests`: ```js import { randomUUID } from 'node:crypto'; const body = { name: 'Loan agreement', subject: 'Please sign your 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' }], fields: [[ { api_id: 'borrower_signature', type: 'signature', recipient_id: 'signer-1', page: 1, x: 375, y: 206, width: 180, height: 44, required: true }, ]], }; 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: JSON.stringify(body), }); if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`); const document = await response.json(); console.log(document.id, document.status); // "Created" — the transient value ``` ```python import os import uuid import requests body = { "name": "Loan agreement", "subject": "Please sign your 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"}], "fields": [[ {"api_id": "borrower_signature", "type": "signature", "recipient_id": "signer-1", "page": 1, "x": 375, "y": 206, "width": 180, "height": 44, "required": True} ]], } 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, ) response.raise_for_status() document = response.json() print(document["id"], document["status"]) # "Created" — the transient value ``` Two things about the response will look wrong if you are not expecting them, and both are deliberate: - **`status` is `Created`**, whatever you passed for `draft`. It is a transient value. The settled status — `Draft` or `Sent` — appears on the next `GET`. - **`files[].pages_number` is `0`** and `files[].status` is not yet `ready` — it reads `uploaded`, then `processing`. Ingest has not finished. The real page count arrives on the next `GET`. Signing URLs in the response are valid immediately, so you can hand a recipient their link before ingest finishes. ## 3. Read it back ```bash curl https://api.signclad.com/api/v1/documents/8f14e45f-ea8f-4b2c-9f0e-1b2c3d4e5f60 \ -H "X-Api-Key: $SIGNCLAD_API_KEY" ``` Once the send job has run, `status` is `Sent`, `pages_number` is real, `files[].status` is `ready`, and each recipient carries a `status` of its own. Read it immediately after the create and you may still catch `Created` or `Sending`. Fields come back grouped by type rather than in the order you sent them, and `width` and `height` are decimal strings while `x`, `y`, and `page` stay numbers. That is the compatibility contract, quirks included. ## 4. Sign it Test mode emails the recipients with a clear test notice. Open the link in the email or the recipient's `embedded_signing_url` and sign the watermarked test document. If you lose the URL, mint a new one rather than recreating the document: ```bash curl -X POST \ https://api.signclad.com/api/v1/documents/{id}/recipients/signer-1/signing_url \ -H "X-Api-Key: $SIGNCLAD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "expires_in_minutes": 60 }' ``` ## 5. Watch the events Create a webhook endpoint pointed at your receiver — or at the hosted inbox in **Settings → Webhooks**, which records requests so you can look at real payloads before writing any code. ```bash curl -X POST https://api.signclad.com/api/v1/webhook_endpoints \ -H "X-Api-Key: $SIGNCLAD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/webhooks/signclad", "description": "Local development", "event_types": ["document_sent", "document_viewed", "document_signed", "document_completed"], "include_test_mode": true, "verify": true }' ``` `verify: true` sends a `ping` before the endpoint goes live and shows you the result: the endpoint starts paused and only activates if your receiver answers `2xx`. Without it the endpoint is active immediately and nothing is pinged. Note `include_test_mode` too — without it, test-mode documents produce no deliveries. ## 6. Download the evidence When every signer has finished: ```bash curl -L https://api.signclad.com/api/v1/documents/{id}/completed_pdf \ -H "X-Api-Key: $SIGNCLAD_API_KEY" -o completed.pdf ``` Before that, this endpoint returns `404` — not `403`, and not an empty `200`. It is the one place where a `404` does not mean "no such document". The completed PDF carries the audit page and a digital seal using a self-issued certificate. `audit_trail_url` on the document gives you the same evidence as signed JSON. ## Common questions ### Which languages does the Signclad eSignature API support? Any language that can make an HTTPS request. Signclad is a plain REST API — JSON in, JSON out, one `X-Api-Key` header — so curl, Node's `fetch`, Python's `requests`, PHP's Guzzle, Ruby's `Net::HTTP`, and Go's `net/http` all work without a library. The samples above are Node and Python; the [API reference](https://signclad.com/docs/api/) gives the request shape for every operation, and [the OpenAPI document](https://signclad.com/openapi/signclad.v1.yaml) generates a typed client for most languages. There is no published Signclad SDK package yet. ### Why is `status` still `Created` after I sent the document? Because the create and send responses answer with a transient status, deliberately: `POST /documents` answers `Created` and `POST /documents/{id}/send` answers `Draft`, whatever you passed. The settled status — `Draft`, `Sent`, `Completed` — appears on the next `GET /documents/{id}`. Never branch on the status in a create response; read the document back, or wait for the webhook. ### Does test mode send real email? Yes. A sandbox key (`sk_test_`) delivers real messages to the recipients, marked as a test and watermarked as not legally binding, and never bills. That means a send to an address you do not control really bounces. Use addresses you own, or set `embedded_signing: true` — which already defaults every recipient's `send_email` to `false` — to get signing links without any mail at all. See [Sandbox and test mode](https://signclad.com/docs/sandbox-and-test-mode/). ## What to read next - [Authentication and keys](https://signclad.com/docs/authentication/) — key types, rotation, and scopes. - [Sandbox and test mode](https://signclad.com/docs/sandbox-and-test-mode/) — what test mode does and does not do. - [Creating a document](https://signclad.com/docs/creating-a-document/) — every option on the create call. - [Webhooks](https://signclad.com/docs/webhooks/) — signatures, health, and replay. - [Errors and rate limits](https://signclad.com/docs/errors-and-rate-limits/) — the exact error shapes. --- # Authentication and keys > API keys, OAuth for applications acting on a workspace, signer sessions, and how each one is scoped, rotated, and revoked. Source: https://signclad.com/docs/authentication/ Signclad has three ways to authenticate, and they never overlap. A signer session cannot call the management API; an API key cannot act as a signer. ## API keys The default for server-to-server work. ```http X-Api-Key: sk_live_9f2a... ``` | Prefix | Mode | Behaviour | |---|---|---| | `sk_live_` | Live | Bills, sends real email, subject to the normal rate limits. | | `sk_test_` | Sandbox | Never bills. Delivers test emails to recipients. Subject to the same rate limits as live keys. | A key belongs to one workspace. Every key acts with the same permissions — the admin capabilities of that workspace, which is what `GET /me` reports as its role — with one deliberate exception: a key cannot touch billing, so a leaked integration key cannot cancel the subscription or transfer ownership. There is no per-key `read` or `write` scope; narrow a credential by using a sandbox key, not by scoping a live one. Keys are shown once at creation and stored hashed. A workspace holds at most 25 unrevoked keys. Creating a key needs the API access your plan includes — Light and above — and creating a sandbox key needs the same. A sandbox key can create only sandbox keys, so the first live key has to come from the app or from another live key. ### Rotation Rotation issues a new key and gives the old one a fixed 24-hour expiry, so you can deploy the new value before the old one stops. Both keys work during the overlap and both appear in the request log, so you can see when the last caller moved over. The rotate response carries the new plaintext and the old key's `stops_working_at`. Rotation is not revocation, and the overlap is not adjustable. If a key leaks, revoke it: `DELETE /api_keys/{id}` stops it immediately. A key cannot revoke itself — that answers `409 self_revoke` — so revoke it with another credential or from the app. ### What a sandbox key may reach A sandbox key is confined to an allow-list of route families, and anything outside it answers `403 sandbox_key_not_allowed`. Documents, templates, files, bulk sends, webhook endpoint configuration, the event catalog, its own keys and `/me` are in. The workspace address book, suppression list, webhook delivery logs and replay, data exports, members, billing, workspace settings and OAuth applications are out — they hold live data with nothing to filter test rows on. ## OAuth 2.0 For an application acting on behalf of a workspace that is not yours — an integration you distribute, or a product embedding Signclad for its own customers. Authorization code with PKCE. PKCE is required and `S256` is the only accepted method — a request without `code_challenge_method=S256` is refused rather than defaulted. Discovery lives at `/.well-known/oauth-authorization-server`. The workspace being acted on needs API access, the same Light-and-above entitlement an API key needs. Scopes: | Scope | Grants | |---|---| | `documents:read` | Documents, recipients, fields, events, and completed files. | | `documents:write` | Create, send, update, cancel, and delete documents. | | `templates:read` | Templates and placeholders. | | `templates:write` | Create, update, duplicate, and delete templates. | | `webhooks:read` | Webhook endpoint configuration and delivery logs. | | `webhooks:write` | Manage webhook endpoints. | | `account:read` | The membership, account, and workspace. | | `offline_access` | Issues a refresh token. Without it, an authorization lasts one access token. | A write scope satisfies its read half, so asking for `documents:write` alone is enough to read documents too. Ask for the narrowest set that works. Access tokens are bearer tokens, passed as `Authorization: Bearer `, and live an hour. An authorization code lives ten minutes and is single use. Refresh tokens rotate on every use and the whole family is revoked if an old one is replayed. ## Signer sessions A short-lived opaque bearer token held by the signer app, obtained by exchanging a signing link or an embedded signing URL and passing whatever gate the document has — a passcode or nothing. Only a hash of it is stored, like an API key. - Thirty minutes from issue. It does not extend with use. - Scoped to **one recipient of one document**. It cannot read another recipient's fields, let alone another document. - Revoked by bumping the recipient's or the document's token version, which is what happens when a document is cancelled or a recipient reassigns. - Accepted only by the `Signer` operations, and by nothing else. You will not normally issue these yourself; they exist so that the signer app can be a normal client of the same API. ## Working out who you are ```bash curl https://api.signclad.com/api/v1/me -H "X-Api-Key: $SIGNCLAD_API_KEY" ``` Returns the identity behind the credential: `user`, `role`, and an `account` object with the plan tier, the active template and user counts, and whether another template or document may be created right now. `workspace` is a **verbatim duplicate of `account`**, not a separate object — the real tenant id is `current_workspace_id`. For an API key there is no membership of its own, so `user` is the workspace owner and `role` is `admin`. Useful as a health check and for showing "connected as" in your own settings screen. ## Keeping keys safe - Never put a live key in client-side code. Embedded signing and the builders use short-lived URLs precisely so that you do not have to. - Restrict who can create keys: only owners and admins can, and every key creation is recorded in the audit log. - The request log at [Settings → API keys → Request log](https://app.signclad.com/app/settings/api-keys/log) keeps 30 days of method, path, status, latency, error code, IP, user agent, and request id per key. No request or response body is ever stored, and credential-shaped query parameters — anything containing `key`, `token`, `secret`, `password`, `passcode`, `signature`, `code` — are redacted before the row is written. Each entry comes with a `copy_as_curl` line that reproduces the shape of the request, with the key replaced by its display prefix. - If a key leaks, revoke it rather than rotating it. Rotation always leaves the old value working for 24 hours. ## Related - [Sandbox and test mode](https://signclad.com/docs/sandbox-and-test-mode/) - [Errors and rate limits](https://signclad.com/docs/errors-and-rate-limits/) --- # Sandbox and test mode > Send test emails and sign watermarked documents without billing. Source: https://signclad.com/docs/sandbox-and-test-mode/ Test mode exists so you can build and run a full integration — send, sign, complete, webhook, download — without usage charges. Recipients receive real emails explicitly marked as test documents. Use addresses you control. ## How a request becomes a test request Two ways, and they agree: 1. **The key.** Any request with an `sk_test_` key is a test request, and nothing the caller sends can turn that off. 2. **The flag.** `test_mode: true` on a create makes that document a test document even with a live key. There is nothing else to configure. If your code has an `if (env === 'staging')` branch deciding whether to set a flag, delete it and use a sandbox key instead. Both routes need the plan entitlement that sandbox keys come with: Light and above. On Free, `test_mode: true` is refused. ## What test mode does | | Test mode | |---|---| | Billing | Never counted. A test document never touches the monthly counters and can never be refused for being over them. | | Email | Delivered to recipients with “This is a test document” in the message and `[TEST]` in the subject. | | Signing | Works normally. Open the signing URL and sign. | | Artifacts | Every page is watermarked “Not legally binding -- test mode,” including the completed PDF. | | Webhooks | Delivered only to endpoints with `include_test_mode: true`. | | Rate limit | The same buckets and the same numbers as live keys: 120/min general, 10/min for `POST`, `PUT` and `PATCH`, 120/min for `POST /files`. | | Document limits | Unlimited. | ## Real delivery in test mode Invitations, reminders, passcodes, and completed copies go to the intended recipients. Test emails include an explicit notice, and test documents are not legally binding. Invalid recipient addresses can bounce just as they do in live mode. ## What a sandbox key cannot reach A sandbox key is not merely filtered to test rows; it is confined to a list of route families, and everything else answers `403 sandbox_key_not_allowed`. Documents, templates, files, bulk sends, webhook endpoint configuration, the event catalog, its own API keys and `/me` are allowed. The address book, the suppression list, webhook delivery logs and replay, the debug inbox, data exports, members, billing, workspace settings and OAuth applications are not — they hold live data with no test-mode column to filter on. Webhook endpoints are allowed but partitioned: a sandbox key sees and can change only the endpoints a sandbox key registered, so the production callback is a `404` to a staging credential. Delivery is not partitioned — an endpoint a sandbox key registered still receives the live events it subscribed to. A sandbox key also creates only sandbox keys. Asking it for a live key is a `422` on `mode`, and a live key reached by id is a `404`. ## Sending test events Every webhook endpoint has a "send test event" action for each event type: `POST /webhook_endpoints/{id}/ping` with an `event_type` sends a realistic sample of that event, synchronously, and never counts towards the endpoint's health. Creating an endpoint with `verify: true` sends a plain `ping` first and activates the endpoint only if your receiver answers `2xx`. Both let you exercise a receiver before there is a real document to exercise it with. ## Moving to live 1. Create a live key — from the app or with another live key, since a sandbox key cannot mint one — and put it wherever your sandbox key was. 2. Register your production webhook endpoint with the live key. A sandbox key's endpoints stay visible only to sandbox keys. 3. Check `include_test_mode` on your webhook endpoints — most production endpoints should have it off. 4. Nothing else changes. The paths, bodies, statuses, and event names are identical. ## Related - [Authentication and keys](https://signclad.com/docs/authentication/) - [Webhooks](https://signclad.com/docs/webhooks/) - [Migrating to Signclad](https://signclad.com/docs/migration/) --- # 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/) --- # 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/) --- # Text tags > Put the fields in the document. Tag syntax, sizing, validation, and how to check a file before you send it. Source: https://signclad.com/docs/text-tags/ Text tags let a document carry its own fields. Write a tag into the Word or PDF source, set `text_tags: true` on the create, and Signclad turns each tag into a field. The same syntax works for documents and templates. This is the right approach when documents are generated by another system — a loan origination platform, a CRM, a mail-merge — because nobody has to open an editor and place boxes. ## Syntax ``` [type|modifier|modifier|...] ``` The first segment is the field type. The rest are modifiers in any order. ``` [sig|req|signer1] [init|signer1] [date|signer1|fmt:MM/DD/YYYY|lock] [text|signer1|id:borrower_full_name|req] [text|signer1|id:borrower_email|validate:email] [check|signer1|id:consent_ach|req] [drop|signer1|id:entity_type|opts:LLC,Corp,Individual] [auto|signer1|value:name] ``` | Type | Field | |---|---| | `sig` | Signature | | `init` | Initials | | `date` | Date signed | | `text` | Text | | `check` | Checkbox | | `drop` | Dropdown | | `sel` | Select | | `auto` | Autofill | | Modifier | Meaning | |---|---| | `signerN` | Which recipient the field belongs to, by position. | | `req` | Required. `opt` is the opposite, and is the default for a bracket tag. | | `id:` | Sets the field's `api_id`. Give every field you intend to prefill or read one. | | `label:`, `name:` | The label shown to the signer, and the internal name shown in the builder. | | `lock` | On a date field, fills at signing and refuses a supplied value. | | `fmt:` | Date format: `MM/DD/YYYY`, `DD/MM/YYYY`, `YYYY/MM/DD`, `Month DD YYYY`, or `MM/DD/YYYY hh:mm:ss a`. | | `formula:` | A date formula, for example `sent_date + 10 days`. | | `validate:` | `email`, `number`, `letters`, `phone`, `zip`, `ssn`, `age`, `alphanumeric`, `routing`, `bank_account`, or a regular expression. Anything unrecognised becomes a `custom` validation with that pattern. | | `opts:`, `default:`, `other` | Options for `drop` and `sel`, which one is preselected, and whether the signer may type their own. | | `value:` (or `prefill:`) | A prefilled value. On an `auto` tag it picks the source instead: `first_name`, `last_name`, `name`, `email`, `company`, `phone`, `title`, or `date_signed`. | | `w:`, `h:` | Override the size the tag box would have given, in units. | | `fixed` | Keep the drawn width instead of growing the box to fit the text. | An `auto` tag without a supported `value:` is not a field at all — it is left in the document as literal text. So is any tag whose first segment is not a known type. Nothing rejects the document for it. ## Legacy compatibility Signclad also accepts the positional `{{...}}` format used by imported templates, so they can move over without being rewritten: ``` {{signature:1:y:Please sign::borrower_signature:200:50}} {{text:2:y:Full name::borrower_name:160:24:letters:y}} {{date:1:y:Signed::signed_on:110:24:y:mm/dd/yyyy:sent_date + 10 days}} {{af_e:2:n:::borrower_email}} ``` The positions are field type, signer number, required (`y` or `n`), label, prefill value, API ID, width, and height. Text fields may then specify validation and fixed width. Date fields may specify locked signing date, date format, and formula. Blank positions keep their defaults. All legacy field names and short autofill aliases are accepted. Variables also work: define one with `{{set=a1:text:1:y:Age? }}` and reuse it with `{{a1}}`. Use Signclad's bracket syntax for new templates. It names each modifier, so adding or removing one does not require a run of positional blanks. ## Sizing, and a deliberate improvement The field takes the size of the tag box as it appears in the document, measured in **units** — CSS pixels at 96 DPI, one unit being 0.75 PDF points. Where a tag box is smaller than a field of that type can usefully be, Signclad applies a **minimum size for the type** instead of producing an unusably small field, anchored at the tag's top-left so the field grows right and down from where you put it. | Type | Minimum | |---|---| | `sig` | 120 × 40 | | `init` | 60 × 40 | | `date` | 110 × 24 | | `text` | 100 × 20 | | `check` | 16 × 16 | | anything else | 100 × 20 | Practical guidance: give a signature tag roughly 180 × 44 units and a date tag roughly 120 × 24, and you will get exactly what you drew. `w:` and `h:` override the measured box outright, and the floor still applies afterwards. Height is capped at 200 units for `sig` and `init`, 74 for everything else. ## Making tags invisible Tags remain part of the rendered page: nothing removes them. Colour the tag text to match its background in the source — white on white is the usual case — and it disappears from the output while still occupying exactly the space that sets the field's size. A small run inside a table cell is a reliable way to position a tag precisely. Do that in the DOCX or the PDF before you upload. Signclad does not recolour tags for you. ## Checking before you send There is no HTTP endpoint for this. The check lives in the MCP server as `validate_text_tags`, which reads a local DOCX, text-based PDF, or plain text — or a snippet of tag text — and parses it with the same syntax the API uses. Nothing is uploaded, so it works on a document you have not sent and on a workspace you have not connected. See [MCP](https://signclad.com/docs/mcp/). It reports every tag it found, the field each one produces, the signers and `api_id`s the document expects, and a list of problems with the line each is on: an unknown field type, an unsupported modifier, a `signerN` below 1, an unknown validation or date format, a `drop` or `sel` with no options, a height over the maximum, a reference to a variable never defined with `set=`, a prefill on a signature field, and a duplicate `api_id`. Two things it cannot tell you, because both need the rendered page: the field's page and coordinates, and whether a tag is large enough to avoid the minimum for its type. A scanned PDF has no text layer at all, and it says so plainly rather than reporting no tags. One difference between the checker and the API is worth knowing. The checker calls a repeated `api_id` an error; the API does not fail on one — it keeps the first and suffixes the rest `_2`, `_3`. That is forgiving, and it also means a copy-pasted tag silently produces a field you cannot address by the name you expected. ## Combining tags with explicit fields You can do both, and the explicit field wins. `fields` you pass are written during the create request; tag fields are imported afterwards, once the file has been converted and its text extracted, and a tag whose `api_id` already exists on the document is skipped. So naming a field in `fields` with the same `api_id` as a tag overrides that tag's position without editing the source document. That import is also why fields from tags are not in the create response. They appear on the next `GET`, with the page count. ## Related - [Creating a document](https://signclad.com/docs/creating-a-document/) - [Templates and create-from-template](https://signclad.com/docs/templates/) --- # Embedded signing > Put the signer view inside your own product — URLs, the iframe, the message protocol, expiry, branding, and branding options. Source: https://signclad.com/docs/embedded-signing/ Embedded signing puts the signer view in an iframe in your product, so your users never see a Signclad-branded page or leave your application. ## Getting a URL Set `embedded_signing: true` when creating the document. Each recipient comes back with an `embedded_signing_url`: ```json { "recipients": [ { "id": "signer-1", "name": "Jane Doe", "email": "jane@example.com", "status": "sent", "embedded_signing_url": "https://app.signclad.com/sign/embedded/qN4vR7xJ2mLd...W8kZ" } ] } ``` If you lose it, or it expires, mint a new one. You do **not** need to recreate the document: ```bash curl -X POST \ https://api.signclad.com/api/v1/documents/{id}/recipients/signer-1/signing_url \ -H "X-Api-Key: $SIGNCLAD_API_KEY" ``` ```json { "embedded_signing_url": "https://app.signclad.com/sign/embedded/7hB2pK9sVc4n...T1yQ", "signing_url": "https://app.signclad.com/sign/Lm3xD8fW5qZa...R6nJ", "expires_at": "2026-09-09T18:44:10Z" } ``` The token is sixty-four opaque characters and it is the **last path segment**, not a query parameter. Nothing about the document or the recipient is derivable from it. Both forms come back on every call: `embedded_signing_url` for the iframe, `signing_url` for the hosted page you would put in your own email or a redirect. Regenerating supersedes the recipient's earlier links, so whatever you replaced stops working — which is the point of a regenerate button. The embedded link is **single use** and has to be opened within **thirty minutes**; `expires_at` is that deadline. The hosted `signing_url` is reusable and lives as long as the document does. So mint the embedded one when the user asks to sign, not when you create the document — a URL that sat in your database for a week is a URL you have to handle expiring. Also useful: `embedded_preview_url` on the document shows the whole document read-only, for a "review before signing" screen or for a party who is not a signer. It is `null` while the document is a draft and carries a URL once it has been sent. The URL carries its own read-only credential, so you can frame it for staff who have no Signclad account; treat it like any other signing link and do not publish it. ## Embedding it ```html ``` Give it real height. The signer view is a document; 800 pixels is a floor, not a target. That `allow` list is what the embed script puts on its own iframe by default (its `iframeAllow` option overrides it); nothing in the signer view asks for a camera or microphone, so do not delegate them — `clipboard-write` is the one the copy-a-link controls use. The embed script appends `signclad_embedded_iframe=1` to the URL and starts a **20 second timer**. If `load` has not fired by then it raises `SC-document.error` with `errorCode: "request_timed_out"`, so a blocked iframe surfaces as an error rather than as a blank box forever. ## Messages The iframe posts messages to the parent window. Listen and check the origin: ```js window.addEventListener('message', (event) => { if (event.origin !== 'https://app.signclad.com') return; switch (event.data.type) { case 'SC-document.setId': onReady(event.data); break; case 'SC-document.documentLoaded': hideSkeleton(); break; case 'SC-document.completed': onSigned(event.data); break; case 'SC-document.declined': onDeclined(event.data); break; case 'SC-document.closed': closeModal(); break; case 'SC-document.error': onError(event.data); break; } }); ``` The payload is a flat `{type, …}` object — the fields are on `event.data` itself, not on a nested `payload`. | Message | Payload | When | |---|---|---| | `SC-document.setId` | `{id, apiAppStyles, newEditUrl, view}` | Once, as soon as the document is known. Nothing reaches you before this. | | `SC-document.documentLoaded` | `{id}` | The document is rendered and interactive. | | `SC-document.completed` | `{id, redirect, url}` | This recipient finished. Others may still be outstanding. | | `SC-document.declined` | `{id, declineReason, url}` | This recipient declined, with the reason. | | `SC-document.closed` | `{id}` | The user asked to close the embedded view. | | `SC-document.clickedContinueBtn` | `{id, url}` | The user pressed the primary continue action. | | `SC-document.error` | `{id, errorCode, errorMessage}` | Something went wrong. `errorCode` is stable; `errorMessage` is for humans. | | `SC-Embedder.addLoader` / `.removeLoader` | `{}` | Show or hide your own loading overlay. | For a signing view, `view` in `setId` is `recipient_side`. An expired link does not get its own message: it arrives as `SC-document.error` with an error code, and the iframe shows a reason page. So does a session that has been revoked. **Treat these as hints, not as truth.** A browser message is not a durable record: the tab may close before it fires. Webhooks are the source of truth for "did this get signed"; the messages are for updating your own UI promptly. Your page can send messages back too — `SC-Embedder-SetAttr` with the options, `SC-Embedder.resizing`, `SC-Embedder.clickAway`, and `SC-document.completeSelfSigning`. The full table, including the flags `SetAttr` carries, is in [embedded builders](https://signclad.com/docs/embedded-builders/#messages-your-page--iframe). ## Branding Create an API application and bind it to your key: ```json { "name": "Acme Lending portal", "primary_color": "#1F6FEB", "button_text_color": "#FFFFFF", "link_text_color": "#1F6FEB", "buttons_border_radius": 8, "callback_urls": ["https://portal.acme.example"] } ``` The logo is a separate upload — `POST /api_applications/{id}/logo` — not a URL in this body. It comes back on the application as a short-lived signed URL. `callback_urls` entries are validated as **origins**: scheme and host, optionally a port, and nothing else. A bare host is read as `https`; a path, a query, or a wildcard is a `422` rather than a silent trim. Pass `api_application_id` on the create and the embedded surfaces carry your branding. There is no embed allowlist to configure: an embedded page addresses your host using the origin the browser reports for your own first message, so framing works wherever you serve it from. Branding resolves per value: the application first, then the workspace's own branding, then the Signclad default, so an application that sets only `primary_color` keeps the workspace's logo. Custom branding is plan-gated — on a plan without it the application's styles are ignored and the defaults render. Use `custom_requester_name` and `custom_requester_email` so the document reads as coming from your customer rather than from you. Neither changes the display name on the invitation email, which stays the workspace's. ## Emails An embedded document is silent by default. Your application is opening the iframe, so Signclad does not also mail the recipient a "please sign" invitation, and it does not run the automatic reminders on days 3, 6 and 10. Set `embedded_signing_notifications: true` on the create if you want those anyway, in which case each recipient's own `send_email` still applies. Two things are not covered by that default, and are worth knowing before you build your own: - **A reminder you ask for is sent.** `POST /documents/{id}/remind` mails the recipient whatever `embedded_signing_notifications` is set to. The default is about prompts Signclad decides to send on its own; a call you made is not one of those. The response's `reminded` names who was actually reached, which can be nobody, so read it rather than the status code. - **Everything that is not a prompt still goes out.** Passcodes, the completed copy, and the sender's own decline, bounce, complaint and expiry notices are unaffected. The switch that silences all mail to one person is their `send_email: false`. A recipient is never reminded once they have finished, declined or been reassigned, and a `cc` is never reminded at all — naming them explicitly does not change that. ## Expiry and errors | Situation | What you get | What to do | |---|---|---| | The URL expired | `SC-document.error` with an expiry code, and the iframe shows a reason page | Mint a new URL | | The document was cancelled | A reason page | Nothing; it is over | | Another recipient must sign first | A waiting page | Nothing; they will be invited automatically | | The recipient already signed | A completed page with the download | Nothing | Signer sessions are thirty minutes, sliding, and scoped to one recipient of one document. ## Related - [Embedded builders and the compatibility shim](https://signclad.com/docs/embedded-builders/) - [Webhooks](https://signclad.com/docs/webhooks/) - [Migrating to Signclad](https://signclad.com/docs/migration/) --- # Embedded builders and the compatibility shim > The document and template builders inside your product: the embed script, every message in both directions, the origin rules, and the message protocol. Source: https://signclad.com/docs/embedded-builders/ The builders let your users prepare documents and templates without leaving your product: upload files, place fields, set recipients, and send. This page is the full contract between your page and the iframe, using the Signclad embed script and its message protocol. ## The shape of it ``` your page (your origin) iframe (app.signclad.com) ┌───────────────────────────────┐ ┌────────────────────────────────┐ │