CalmSign
Developers

E-signature API documentation

Build a document out of blocks, send a signing link, and collect a sealed copy — without anyone logging into a dashboard. Everything the app does, the REST API does too.

API access and webhooks are part of the Business plan— $29/month, up to 5 team members. Free and Pro workspaces can use the editor and templates, but their API keys return 403 plan_required. Seeplans and pricing.

Overview

A CalmSign document is an ordered list of blocks — headings, paragraphs, inputs, and signature fields. You create one from your own blocks or from a saved template, send it to one or more signers, and CalmSign handles the signing link, the reminders, and the copies. When the last signer is done the document is sealed with a SHA-256 hash over an immutable snapshot, and the full audit trail is available for download.

Signers never need an account. They open a link, fill the inputs, sign, and download their copy.

MethodEndpointPurpose
POST/v1/documentsCreate a document from blocks or a template
GET/v1/documentsList documents
GET/v1/documents/{id}Retrieve one document and its status
POST/v1/documents/{id}/sendSend a document for signature
GET/v1/documents/{id}/pdfDownload the signed PDF
GET/v1/documents/{id}/audit-trailDownload the audit trail
GET/v1/documents/{id}/sealRead and check the SHA-256 seal

Authentication

Every request carries an API key in an Authorization header. Create keys in the app under Settings → API keys; the secret is shown once, at creation. Keys are scoped to one workspace and can be revoked at any time without touching the documents they created.

cURL
curl https://api.usecalmsign.com/v1/documents \
  -H "Authorization: Bearer cs_live_9c2f4a7b1d0e8f36"

Requests over plain HTTP are refused, not redirected. Keep the key server-side — it can read and send every document in the workspace, so it does not belong in browser or mobile code.

Base URL and versioning

All endpoints live under a single versioned base URL:

https://api.usecalmsign.com/v1

Requests and responses are JSON, UTF-8, with timestamps as ISO 8601 in UTC. New fields can appear inside existing objects at any time, so parse leniently and ignore what you do not know. Anything that would break a working integration — a removed field, a changed type — ships under a new version prefix instead, and the old prefix keeps working.

Documents

Create a document from blocks

POST/v1/documents

Post an ordered blocks array. The order you send is the order the signer reads, and the document is created as a draft — nothing is emailed until you call send.

FieldTypeRequired onNotes
typestringYesheading, text, input, or signature.
contentstringheading / textThe rendered copy of a heading or text block.
labelstringinput / signatureWhat the signer sees above the field, e.g. "Company Name".
input_typestringinputtext or date. Dates are captured in the signer’s locale and stored as ISO 8601.
requiredbooleanNoBlocks the signature until the field is filled. Defaults to false.
Request
curl -X POST https://api.usecalmsign.com/v1/documents \
  -H "Authorization: Bearer cs_live_9c2f4a7b1d0e8f36" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mutual Non-Disclosure Agreement",
    "blocks": [
      { "type": "heading", "content": "Mutual Non-Disclosure Agreement" },
      { "type": "text", "content": "This agreement governs the exchange of confidential information between the parties." },
      { "type": "input", "label": "Company Name", "input_type": "text", "required": true },
      { "type": "input", "label": "Effective Date", "input_type": "date", "required": true },
      { "type": "signature", "label": "Authorized Signature", "required": true }
    ]
  }'
201 Created
{
  "id": "doc_7Kq2mR4xa9",
  "name": "Mutual Non-Disclosure Agreement",
  "status": "draft",
  "template": null,
  "signers": [],
  "created_at": "2026-08-18T10:12:44Z",
  "updated_at": "2026-08-18T10:12:44Z"
}

Responses omit the blocks array unless you ask for it with?include=blocks. Documents stay editable while they are drafts.

Create a document from a template

POST/v1/documents

Pass a template key instead of blocksand CalmSign copies that template's blocks into a new draft. Every workspace is seeded with the starter templates below; your own saved templates work the same way, using the key shown on the template in the app.

Request
curl -X POST https://api.usecalmsign.com/v1/documents \
  -H "Authorization: Bearer cs_live_9c2f4a7b1d0e8f36" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "nda",
    "name": "NDA — Northwind Studio"
  }'

Browse what each one contains in the template gallery.

Retrieve a document

GET/v1/documents/{id}

Returns the document, each signer's progress, the seal once one exists, and the paths to the signed PDF and the audit trail.

200 OK
{
  "id": "doc_7Kq2mR4xa9",
  "name": "Mutual Non-Disclosure Agreement",
  "status": "signed",
  "template": "nda",
  "created_at": "2026-08-18T10:12:44Z",
  "sent_at": "2026-08-18T10:15:02Z",
  "completed_at": "2026-08-18T14:32:07Z",
  "signers": [
    {
      "id": "sgn_3Vb8tL",
      "name": "Jordan Mitchell",
      "email": "jordan@example.com",
      "order": 1,
      "status": "signed",
      "signed_at": "2026-08-18T14:32:07Z"
    }
  ],
  "seal": {
    "algorithm": "sha256",
    "value": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "status": "intact"
  },
  "files": {
    "pdf": "/v1/documents/doc_7Kq2mR4xa9/pdf",
    "audit_trail": "/v1/documents/doc_7Kq2mR4xa9/audit-trail"
  }
}

Document statuses

A document moves through five states. Only signed anddeclined are terminal.

StatusMeaning
draftCreated but not sent. Blocks can still be edited.
sentA signing link has been issued to the current signer.
viewedThe signer opened the link. Logged with timestamp, IP, and device.
signedEvery signer has signed. The seal is applied and both sides get a copy.
declinedA signer declined. The document is closed and cannot be sent again.

List documents

GET/v1/documents

Newest first. Filter with status, page withlimit (1–100, default 25) and thenext_cursor returned alongside the results.

cURL
curl "https://api.usecalmsign.com/v1/documents?status=sent&limit=25" \
  -H "Authorization: Bearer cs_live_9c2f4a7b1d0e8f36"

Sending for signature

Send a document

POST/v1/documents/{id}/send

Moves a draft to sent and emails a private signing link. Give each signer an order to control the sequence: signer 2 is only invited once signer 1 has finished. Omit order and everyone is invited at once.

Request
curl -X POST https://api.usecalmsign.com/v1/documents/doc_7Kq2mR4xa9/send \
  -H "Authorization: Bearer cs_live_9c2f4a7b1d0e8f36" \
  -H "Content-Type: application/json" \
  -d '{
    "signers": [
      { "name": "Jordan Mitchell", "email": "jordan@example.com", "order": 1 },
      { "name": "Alex Rivera", "email": "alex@northwind.example", "order": 2 }
    ],
    "message": "Here is the NDA we discussed — it takes about a minute to sign."
  }'
200 OK
{
  "id": "doc_7Kq2mR4xa9",
  "status": "sent",
  "sent_at": "2026-08-18T10:15:02Z",
  "signers": [
    {
      "id": "sgn_3Vb8tL",
      "name": "Jordan Mitchell",
      "email": "jordan@example.com",
      "order": 1,
      "status": "sent",
      "signing_url": "https://app.usecalmsign.com/s/a7x9k2m"
    },
    {
      "id": "sgn_9Wd1pQ",
      "name": "Alex Rivera",
      "email": "alex@northwind.example",
      "order": 2,
      "status": "pending",
      "signing_url": null
    }
  ]
}

What the signer gets

The link opens the document in the browser — no account, no password, no download. The signer fills the required inputs, signs by drawing or typing, and gets their own copy from the same link when the document completes. Every step (opened, signed, sealed) lands in theaudit trailwith a timestamp, IP address, and device.

The signed PDF and audit trail

Download the signed PDF

GET/v1/documents/{id}/pdf

Streams the sealed PDF as application/pdf once the document reaches signed. Called earlier it returns409 invalid_state. The hash, the signatures, and the signer metadata are embedded in the file itself, so a downloaded copy can be checked away from CalmSign.

cURL
curl https://api.usecalmsign.com/v1/documents/doc_7Kq2mR4xa9/pdf \
  -H "Authorization: Bearer cs_live_9c2f4a7b1d0e8f36" \
  -o nda-northwind.pdf

Download the audit trail

GET/v1/documents/{id}/audit-trail

Returns the complete event log as JSON. SendAccept: application/pdf for the standalone certificate instead — the same document you would attach to a dispute.

200 OK
{
  "document_id": "doc_7Kq2mR4xa9",
  "events": [
    { "type": "created",  "at": "2026-08-18T10:12:44Z", "actor": "api",                 "ip": "203.0.113.24" },
    { "type": "sent",     "at": "2026-08-18T10:15:02Z", "actor": "api",                 "ip": "203.0.113.24" },
    { "type": "opened",   "at": "2026-08-18T14:28:19Z", "actor": "jordan@example.com",  "ip": "82.132.13.37", "device": "Chrome / macOS" },
    { "type": "signed",   "at": "2026-08-18T14:32:07Z", "actor": "jordan@example.com",  "ip": "82.132.13.37", "device": "Chrome / macOS" },
    { "type": "sealed",   "at": "2026-08-18T14:32:09Z", "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" }
  ]
}

Verifying the seal

Read the seal

GET/v1/documents/{id}/seal

At completion CalmSign computes a SHA-256 hash over an immutable snapshot of the document. This endpoint recomputes that hash from the stored snapshot and compares it:status is intact when they match and broken when they do not. Read more abouthow the seal works.

200 OK
{
  "document_id": "doc_7Kq2mR4xa9",
  "algorithm": "sha256",
  "value": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "sealed_at": "2026-08-18T14:32:09Z",
  "checked_at": "2026-08-18T15:04:11Z",
  "status": "intact",
  "snapshot_url": "https://api.usecalmsign.com/v1/documents/doc_7Kq2mR4xa9/snapshot"
}

Recompute the hash yourself

You do not have to take our word for it. Pull the snapshot bytes and hash them with any SHA-256 implementation — the digest must equal value above. A single changed character produces a completely different digest.

Shell
curl -sS https://api.usecalmsign.com/v1/documents/doc_7Kq2mR4xa9/snapshot \
  -H "Authorization: Bearer cs_live_9c2f4a7b1d0e8f36" \
  | shasum -a 256

# 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08  -

Webhooks

Rather than polling, register an HTTPS endpoint underSettings → Webhooks and CalmSign will POST each event as it happens. You get a signing secret (whsec_…) when you create the endpoint.

Event types

EventSent when
document.sentA signing link has been issued and emailed to a signer.
document.signedThe last signer signed. The document is sealed and final.
document.declinedA signer declined to sign. No seal is produced.

Payload shape

Every event has the same envelope: an id, a type, a creation timestamp, and adata object holding the document and, where the event concerns one, the signer.

document.signed
{
  "id": "evt_5Hn8xT2c",
  "type": "document.signed",
  "created_at": "2026-08-18T14:32:07Z",
  "data": {
    "document": {
      "id": "doc_7Kq2mR4xa9",
      "name": "Mutual Non-Disclosure Agreement",
      "status": "signed",
      "completed_at": "2026-08-18T14:32:07Z",
      "seal": {
        "algorithm": "sha256",
        "value": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
        "status": "intact"
      }
    },
    "signer": {
      "id": "sgn_3Vb8tL",
      "name": "Jordan Mitchell",
      "email": "jordan@example.com"
    }
  }
}

Reply 2xx within 10 seconds. Anything else is retried with exponential backoff for 24 hours, so handlers should be idempotent — key them onid, which is stable across retries.

Verifying the webhook signature

Each delivery carries a signature header: a timestamp and an HMAC-SHA256 oft + "." + rawBody, keyed with your signing secret.

Header
CalmSign-Signature: t=1787063527,v1=6f1b0c9a4d2e8b73c05a1f9e2d4b8c7a05e3f61d9b2c4a780f13e5d6c8b9a204

Recompute it over the raw request body — before any JSON parsing or re-serialising, which would change the bytes — and compare in constant time.

Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody must be the exact bytes we POSTed — verify before JSON.parse.
export function isFromCalmSign(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));

  // Reject anything older than five minutes to kill replays.
  const age = Math.floor(Date.now() / 1000) - Number(parts.t);
  if (!Number.isFinite(age) || age > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(parts.t + '.' + rawBody)
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1 ?? '', 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Errors

CalmSign uses conventional HTTP status codes. Every failure returns the same body: a stabletype to branch on, a human-readablemessage, the offendingparam where one applies, and arequest_id to quote atsupport.

422 Unprocessable Entity
{
  "error": {
    "type": "validation_failed",
    "message": "A signature block needs a label.",
    "param": "blocks[4].label",
    "request_id": "req_2Fp6yN"
  }
}
StatusTypeCause
400invalid_requestMalformed JSON, or a parameter of the wrong type.
401unauthorizedMissing, malformed, or revoked API key.
403plan_requiredThe key belongs to a workspace without API access. Upgrade to Business.
404not_foundNo document with that id in this workspace.
409invalid_stateThe action does not apply to the current status — sending an already-signed document, for example.
422validation_failedThe request parsed but a field is unusable. The param key names the offender.
429rate_limitedToo many requests. Wait for the number of seconds in Retry-After.
500server_errorSomething broke on our side. Safe to retry with the same idempotency key.

Rate limits

120 requests per minute per API key. Every response carries the current budget; a429 also tells you how long to wait.

Response headers
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 41
Retry-After: 41

Limits apply to requests, not to documents: the Business plan has no monthly document cap, so creating and sending is bounded only by this rate. If a bulk import needs more headroom, write to hello@usecalmsign.combefore you start rather than after the 429s.

Build with CalmSign

API access, webhooks, and 5 team members on the Business plan. Start free and upgrade when you are ready to automate.