SignBob
API v1

SignBob API

Send a document for signature from your own system, and get called back when it is signed. The API is available on plans that include API access; create a key in Settings.

Authentication

Send your key as a bearer token. Keys are shown once, at creation. A key belongs to the account, not to the person who made it.

Authorization: Bearer sb_live_xxxxxxxxxxxxxxxx

Requests are limited to 600 per hour per key. Errors come back as { "error": "..." } with a matching status: 401 for a bad key, 402 when the plan or the monthly limit is the problem, 429 when rate limited.

Send a document

POST /api/v1/documents creates the document and sends it in one call. Give it either content (text) or a file (base64: PDF, Word, RTF, text, an image or a scan — anything that is not a PDF is converted, and the original is kept as evidence). Each signer needs a name and an email or a phone, or both: a signer with only a phone is reached by SMS, on plans that include it.

curl https://app.signbob.com/api/v1/documents \
  -H "Authorization: Bearer $SIGNBOB_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Consulting agreement",
    "content": "The parties agree as follows.\n\nName: [[text:client]]\nSignature: [[signature:client]]\nDate: [[date:client]]",
    "signers": [{ "name": "Dana Levi", "email": "dana@example.com", "phone": "0501234567" }],
    "locale": "he",
    "message": "Please sign by Friday"
  }'

The [[type:role]] anchors place the fields for you — one role per signer, in the order the roles first appear. Types: signature, initials, text, date, id, checkbox, attachment. Add ? to make one optional, e.g. [[text:client?]].

Or place fields yourself, in fractions of the page with y measured from the top:

{
  "title": "Lease",
  "file": { "name": "lease.docx", "data": "<base64>" },
  "signers": [{ "name": "Dana Levi", "email": "dana@example.com" }],
  "fields": [
    { "signerIndex": 0, "type": "signature", "page": 0, "x": 0.12, "y": 0.74, "w": 0.24, "h": 0.05 }
  ],
  "requireOtp": true,
  "expiresInDays": 14,
  "cc": ["office@example.com"]
}

A field may also carry label, the name it goes by, and value — an answer you already have. A field with a value goes out filled and locked: every signer reads it as part of the document and none of them can change it. Only text and date take one; a value on any other type is refused rather than dropped.

The response carries a signing link per signer. Treat it as a secret — anyone holding it can sign.

HTTP/1.1 201 Created
{
  "id": "cmu6...",
  "status": "sent",
  "title": "Consulting agreement",
  "signers": [
    { "name": "Dana Levi", "email": "dana@example.com", "order": 1,
      "signingUrl": "https://app.signbob.com/sign/9f2c..." }
  ]
}

Retrying safely

Sending a document emails real people and spends a document from your monthly allowance, so a retry of a request whose answer you never saw must not send it twice. Put a unique Idempotency-Key on the request and the same key always returns the first answer — replies to a repeat carry Idempotent-Replay: true. Keys are remembered for 24 hours and are scoped to your account. A repeat that arrives while the first request is still running gets a 409; wait and retry with the same key.

curl https://app.signbob.com/api/v1/documents   -H "Authorization: Bearer $SIGNBOB_KEY"   -H "Idempotency-Key: order-4821-contract"   -H "Content-Type: application/json" -d @body.json

Options

requireOtpOne-time code by SMS or email before the document opens.
accessCode / accessCodeHintA secret you agreed with the signer out of band.
requireIdPhotoSigner photographs an ID document and themselves to complete the signature, after reading and filling in the document.
requireCertifiedSignatureThe last signer must sign with a certificate of their own (smart card or USB token) from a registered certification authority. Mark that signer with certified: true.
expiresInDays / scheduledAtDeadline, and sending later instead of now.
retentionDaysDelete the document this long after completion.
localehe, en or el — the language the signer sees.
clientIdentifiedRequired when the account has strict identification on: how you know this signer.

Options that belong to a paid capability are refused with 402 if your plan does not include them, rather than silently ignored.

Read documents

GET /api/v1/documents?status=sent&limit=50
GET /api/v1/documents?limit=50&cursor=<id of the last row you received>
GET /api/v1/documents/{id}

Signer tokens and file paths are never returned by the list endpoint — a signing link in a list response would be a signature handed to anyone holding a read-only key.

The list is newest first and pages with a cursor. Each response carries nextCursor and hasMore; pass the cursor back to get the next page, and stop when hasMore is false. limit is 50 by default and 200 at most.

Finish a document

GET  /api/v1/documents/{id}/file            # the signed PDF
GET  /api/v1/documents/{id}/file?original=1  # what was uploaded
POST /api/v1/documents/{id}/void
POST /api/v1/documents/{id}/remind

/file returns the signed PDF as application/pdf. Until every signer has finished there is no signed file to return, and the answer is 409 with the document's current status in the body — distinct from 404, so a poller can tell “not yet” from “not yours”.

/void stops a document that is out for signature: its signing links stop working immediately. /remind re-sends the request to whoever is still eligible and answers with reminded, the number of people mailed — 0 is a normal answer when everyone eligible has already signed, or is waiting their turn in a sequential flow. Both require the document to be awaiting signature and answer 409 otherwise.

Voiding and reminding are plan features in their own right, so a key on a plan without them gets 402 even though the same key can read and send.

Webhooks

Add an endpoint in Settings and we will POST to it as things happen. Delivery is at-least-once: a call that fails is retried with increasing delay, so your receiver should tolerate a repeat — the x-signbob-delivery header is stable across retries of the same event.

Events: createdsentviewedconsentedreviewedsigneddeclineddeclineWithdrawncompletedvoidedreminder

POST https://your-app.example.com/hooks/signbob
x-signbob-event: completed
x-signbob-delivery: cmu7...
x-signbob-signature: sha256=8f4b...

{
  "event": "completed",
  "occurredAt": "2026-09-19T08:14:22.117Z",
  "sequence": 7,
  "actor": "system",
  "detail": null,
  "document": {
    "id": "cmu6...",
    "title": "Consulting agreement",
    "status": "completed",
    "sentAt": "2026-09-19T07:55:03.001Z",
    "completedAt": "2026-09-19T08:14:22.004Z"
  }
}

Verify the signature over the raw body before trusting it:

import { createHmac, timingSafeEqual } from "crypto";

const expected = "sha256=" + createHmac("sha256", process.env.SIGNBOB_WEBHOOK_SECRET)
  .update(rawBody)          // the bytes as received, not re-serialised JSON
  .digest("hex");

const a = Buffer.from(expected);
const b = Buffer.from(req.headers["x-signbob-signature"] ?? "");
if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(401).end();

Answer 2xx to acknowledge. An endpoint that keeps failing is switched off after 20 consecutive failures and can be re-enabled in Settings.

Verify a signed document

Completed PDFs are sealed with a PKCS#7 signature, so any later edit is detectable in any PDF reader. Anyone — including someone without an account — can also check a file against our record by posting it to POST /api/verify as multipart form data under file. The file is hashed, not stored.

Managing endpoints with a key

Endpoints can be added from Settings, or programmatically — which is what an automation platform does when a user switches an automation on and off.

GET    /api/v1/webhooks
POST   /api/v1/webhooks     { "url": "https://...", "events": ["completed"] }   → { id, secret }
DELETE /api/v1/webhooks?id=<id>

The secret comes back once, on creation. Omit events to receive all of them.

What the status codes mean

400  the request is malformed - a missing title, no signers, a bad field
401  no key, or a key that is revoked or unknown
402  your plan does not include this - the body carries "upgrade": true
404  no such document, or one that belongs to another account
409  the document is real but in the wrong state for this call
429  too many requests - see Retry-After

404 is deliberately used for a document that exists but belongs to someone else: telling a caller that an id is real but forbidden is itself a leak. And 402 is always recoverable by the account owner — the upgrade flag is there so a client can say so rather than failing silently.

Errors from /api/v1 are always in English, whatever language the account's dashboard is set to. They are read by programs, and a message that changes because someone switched their interface to Hebrew would break the parser that relied on it.

Rate limits

X-RateLimit-Limit: 600
X-RateLimit-Window: 3600
Retry-After: 1840        # on 429 only

600 requests per hour, counted per key rather than per account — an account may hold up to ten keys. The window is fixed, so Retry-After on a 429 is the exact number of seconds until the allowance resets, not an estimate.

There is no X-RateLimit-Remaining header. We would rather omit it than send a number we cannot promise is accurate under concurrency — pace yourself against the window, and back off when you see a 429.

Versioning

Everything under /api/v1 keeps its shape. We may add fields to a response and add new endpoints; we will not remove a field, rename one, change what a status code means, or make an optional parameter required. Treat unknown fields as safe to ignore — that is what lets us add without breaking you.

A change that cannot be made that way becomes /api/v2, and v1 keeps working. If we ever need to retire a version we will email every account holding a key first.

Testing an integration

There is no sandbox yet: a document you send through the API is a real document, and the signer it names will receive a real email. Until there is one, test against addresses you control, and use /void to close anything you do not intend to complete.

Zapier, Make, WordPress

A WordPress plugin and a Zapier app are in the repository under integrations/. The WordPress plugin installs today by upload; the Zapier app is ready to push but is not yet listed in their directory, and Make has no app of ours yet.

Meanwhile any platform can use the endpoints directly: an HTTP module calling POST /api/v1/documents to send, and a webhook endpoint registered as above to be told when it is signed. In Make that is the "HTTP" and "Custom webhook" modules; in Zapier, "Webhooks by Zapier".

Questions: support@signbob.com. Breaking changes will be published under a new version prefix; v1 keeps working.