← Workspace

A small API for signing.

Server-to-server document preparation. Branded, embeddable recipient signing. Completed PDFs and signing records.

API access and billing

API Developer is separate from Free, Solo, and Business workspace plans: $99/month includes 250 API documents per billing period, then $0.50 per additional document. There is no pay-as-you-go plan. Each new API document counts when created, even if it is never sent or is later voided. An identical idempotent upload retry does not create another document. Every created API document is reported to the billing meter; Stripe applies the 250-document allowance. Editing, issuing links, and downloading an existing document do not create another API document charge.

An account needs API Developer access or a server-assigned complimentary API entitlement. Without access, API operations return 403; already-issued signing flows can still complete. Manage API billing in Billing and create or revoke keys in API & integrations. Keys are shown once when created. API access includes branded embedded signing without a SignedOff footer and signed customer webhooks.

1. Authenticate from your server

Create an API key in the dashboard. Store it as a secret in your application’s backend. Each key can access only the contracts created through that connection. API contracts are excluded from the regular workspace and kept in the owner-only API contract archive. Never share one key among independent software companies. Your backend must also enforce tenant and recipient access for every request. Deleting a key revokes its access; archived records remain available to the workspace owner.

Authorization: Bearer YOUR_SIGNEDOFF_API_KEY

2. Upload the original PDF

POST /api/v1/documents?title=Service%20agreement&external_id=RECORD_ID
Content-Type: application/pdf
Idempotency-Key: record-123-agreement-v1

[raw PDF bytes]

Example from your backend or terminal, with the key loaded from a server-side secret:

curl --request POST \
  'https://app.signedoffdocs.com/api/v1/documents?title=Service%20agreement&external_id=record-123' \
  --header "Authorization: Bearer $SIGNEDOFF_API_KEY" \
  --header 'Content-Type: application/pdf' \
  --header 'Idempotency-Key: record-123-agreement-v1' \
  --data-binary @agreement.pdf

The response includes id, revision, page_count, and original_hash. Reusing the same idempotency key with identical bytes and metadata returns the same document. Different content returns 409. PDFs must be at most 4 MiB (4,194,304 bytes) and 50 pages, with unrotated, uncropped pages and no interactive form fields.

3. Set the people, fields, and branding

PATCH /api/v1/documents/DOCUMENT_ID
Content-Type: application/json

{
  "title": "Service agreement",
  "revision": 0,
  "recipient_name": "",
  "recipient_email": "",
  "signing_mode": "others",
  "signers": [{
    "id": "b00b0000-0000-4000-8000-000000000001",
    "name": "Example Signer",
    "email": "signer@example.com",
    "role": "recipient"
  }],
  "branding": {
    "name": "Your business",
    "color": "#176a50",
    "footer": false,
    "embedOrigin": "https://your-software.example"
  },
  "fields": [{
    "id": "a00a0000-0000-4000-8000-000000000001",
    "signer_id": "b00b0000-0000-4000-8000-000000000001",
    "type": "signature",
    "page": 0,
    "x": 0.1, "y": 0.8,
    "width": 0.3, "height": 0.05,
    "label": "Signature"
  }]
}

Coordinates are normalized from the top-left of each page. Page indices start at zero. Field types: signature, initials, date, name, text. Assign every field using signer_id. Every signer needs a signature field. Signers can fill only their own assigned fields; the server rejects values for other people’s fields. All fields are required. Date fields use each signer’s calendar date when they sign. The hosted signing page supplies the browser timezone; API completion requests may supply an IANA timezone, with UTC used when omitted. The server computes the date from its own signing timestamp. branding.logo optionally accepts a PNG data URL (250 KB recommended; the full data URL is limited to 400,000 characters). Branding and fields freeze once a link is issued. Use the latest revision for edits; stale writes return 409.

4. Create private signing links

POST /api/v1/documents/DOCUMENT_ID/link

The response includes signing_links, one entry per pending recipient with signer_id, name, email, signing_url, and a seven-day expiry. Reissuing links revokes the previous links for pending recipients. The raw token is returned only when issued and is never stored in the database. No email is sent: your software can deliver the link through its normal email service.

Invitation delivery belongs to your application: link creation never sends an invitation email. Send your own portal URL or embed the signing experience in your app. The SignedOff dashboard’s explicit email invitation action is unavailable to API keys. Hosted recipient links require email verification once for the document session; signing does not request a second code while that session is valid. Separately approved native portal integrations use the authentication flow described below.

5. Embed or redirect

<iframe src="SIGNING_URL" title="Sign your agreement"
  style="width:100%;height:850px;border:0"></iframe>

Set the exact parent origin in branding.embedOrigin before issuing the link. The signing page restricts embedding using CSP frame-ancestors. Hosted platform headers and authentication can impose additional restrictions; test against the final public deployment.

window.addEventListener('message', async (event) => {
  if (event.origin !== SIGNING_SERVICE_ORIGIN) return;
  if (event.source !== signingIframe.contentWindow) return;
  if (event.data?.type !== 'simplysign.completed') return;
  if (event.data.document_id !== expectedDocumentId) return;
  // Ask your backend to check authoritative document status.
  // Never mark a record signed based only on a browser message.
});

6. Check completion and retrieve the PDF

GET /api/v1/documents/DOCUMENT_ID
GET /api/v1/documents/DOCUMENT_ID/events
GET /api/v1/documents/DOCUMENT_ID/pdf?version=completed
POST /api/v1/documents/DOCUMENT_ID/void

Statuses: draft → awaiting → completed; an unsigned document can become voided. The document becomes completed only after every assigned signer has signed. Use signed webhook notifications or poll from your backend with backoff. Downloading before completion returns 409. Voiding invalidates the signing link and preserves the original document and history.

Evidence and retention

GET /api/v1/documents/DOCUMENT_ID/evidence
GET /api/v1/documents/DOCUMENT_ID/export
GET /api/v1/documents/DOCUMENT_ID/integrity
GET /api/v1/verification-key
POST /api/v1/documents/DOCUMENT_ID/access-link

Every new completion has a detached ECDSA P-256 signature over its hashes, consent, signer verification, metadata, and audit snapshot. Downloads verify the evidence and PDF. This is not an Adobe certificate signature or an independent timestamp authority.

The JSON export contains both PDFs (base64), signed evidence, and later activity. Independently pin the public verification key before relying on an export. Use scripts/verify-record.mjs from the source to verify and extract PDFs offline. Later retention changes carry their own signed proofs; other post-completion activity is not part of the completion signature.

Records are kept indefinitely by default. The owner can choose a minimum retention period for new uploads, extend a completed record’s retention, and add or release a legal hold with a reason. There is no deletion endpoint or automatic purge, even after a minimum period elapses. These controls are application-enforced, not storage-level WORM locks. Link expiry does not delete records. The owner can renew access to completed records; recipients must verify email again.

Use the hosted signing page to handle verification; ordinary API keys cannot sign as a person. For a custom recipient interface, POST /api/v1/sign/TOKEN/request-code, then POST /verify-code with challenge_id and code. Pass the returned session_token in X-Signing-Session for document reads, PDF/evidence downloads and completion. Keep the token in memory, never in a URL or browser storage. Requests must originate on the signing service; embedded pages retain that origin.

Map to your application

What this version does and does not establish

The service records explicit consent, UTC completion time, document hashes, and access method. Hosted-link recipients must verify control of the designated email address with a six-digit code before opening the PDF or signing. This verifies email access, not legal identity. Codes expire after ten minutes, allow five attempts, and create a thirty-minute document-scoped session. Requests are rate limited. New links revoke older sessions. PDFs use typed electronic signatures, not certificate-based digital signatures. The signing record is retained separately in the workspace. No SignedOff Docs watermark is added. An optional per-page footer adds a record ID and timestamp.

Your integration remains responsible for tenant and recipient authorization, invitation delivery, webhook signature verification and deduplication, and reconciling authoritative document status. Test the complete workflow with documents and recipients you control before using customer agreements.

Request and document limits

Native portal signing (restricted capability)

Approved server integrations may use their own authenticated portal and render their own signing interface. This capability must be enabled separately for the server API key. Ordinary API keys cannot sign or bypass recipient email verification.

Bind every request to the current booking, assigned recipient, unexpired portal access, and the document revision and hash that the customer reviewed. Collect the explicit consent text and typed signature. Send POST /api/v1/documents/:id/portal-complete from your server only, with name, consent:true, values, timezone, revision, original_hash, recipient_email, issuer, subject, method:"portal_link", client_ip, user_agent. The issuer must equal the document’s configured embedding origin. This capability supports the single-recipient document format: omit signing_mode and signers, and set recipient_name and recipient_email when preparing the document. The multi-signer example above uses hosted signing and cannot be completed through this restricted endpoint.

The signing record identifies integration portal authentication, including the integration’s asserted client details, separately from SignedOff’s service connection. It does not claim that SignedOff verified an email code. Completion records the server signing time. No invitation or verification email is sent by this endpoint.

Separate API records

GET /api/v1/documents with a key lists only that connection’s contracts. Workspace document endpoints cannot access API contracts using an owner browser session. The signed-in owner can deliberately open API & integrations → API contract archive to read originals, completed PDFs and signing records. The archive endpoint is read-only and rejects API keys. Separation is enforced by authorization and storage namespaces within the existing private database and bucket; it is not a separate physical database.

Customer webhooks

POST /api/v1/webhooks
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{"url":"https://your-software.example/webhooks/signedoff"}

GET /api/v1/webhooks
DELETE /api/v1/webhooks/ENDPOINT_ID

API Developer access is required. An endpoint receives events only for documents belonging to the API key used to register it. Store the returned secret securely; it is shown only once. Disabling an endpoint or revoking its API key stops delivery. Up to ten active endpoints are supported per account. Use a public HTTPS endpoint on port 443; redirects and private destinations are rejected.

Events: document.completed, document.signer_completed, document.voided, document.links_created, and document.link_created.

{
  "id": "EVENT_ID",
  "type": "document.completed",
  "created_at": "2026-10-17T12:00:00.000Z",
  "data": {"document_id": "DOCUMENT_ID", "external_id": "YOUR_RECORD_ID"}
}

Verify SignedOff-Signature: t=TIMESTAMP,v1=HEX_SIGNATURE using HMAC-SHA256 with your endpoint secret over TIMESTAMP + "." + RAW_REQUEST_BODY. Compare signatures in constant time and reject timestamps more than five minutes old. Deduplicate by the event id (also supplied in SignedOff-Event-Id). Verify the raw bytes before parsing JSON. Events contain no PDF, signature, signing token, or recipient identity. Fetch authoritative document state from your backend before updating your record.

Return a 2xx response promptly. Delivery is asynchronous, generally within the five-minute scheduler interval, and may be duplicated or arrive out of order. Failed requests retry with backoff, up to eight attempts. Reconcile important records by polling if delivery is exhausted. Webhooks are separate from invitation emails: your application remains responsible for sending API contract invitations.

Separate signing record

GET /api/v1/documents/DOCUMENT_ID/audit-pdf

Download the signing record as a separate PDF after completion. The signed document keeps its original page count. The JSON evidence and export endpoints remain available for independent integrity verification.