Embedded signing

Put the signer view inside your own product — URLs, the iframe, the message protocol, expiry, branding, and branding options.

Updated

On this page

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:

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

curl -X POST \
  https://api.signclad.com/api/v1/documents/{id}/recipients/signer-1/signing_url \
  -H "X-Api-Key: $SIGNCLAD_API_KEY"
{ "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

<iframe
  src="https://app.signclad.com/sign/embedded/qN4vR7xJ2mLd...W8kZ"
  allow="clipboard-read; clipboard-write"
  style="width: 100%; height: 800px; border: 0"
  title="Sign the agreement"
></iframe>

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:

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.

Branding

Create an API application and bind it to your key:

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