Developers
The API the incumbents taught you to expect
Everything the app does when sending (envelopes, parties, templates, bulk send, tracking, the evidence ledger) under your own API key, against your organization's own instance. The shared instance for individuals offers no API keys or webhooks. One thing no key can do: sign. The ceremony belongs to each party through their own link, because a signature your integration could fake would prove nothing.

Authentication
A sign_admin mints keys in Rutba Sign (Settings → Integrator API keys). The key is shown once, stored only as a salted hash, and revocable per key. Send it as a bearer token; the base URL is your instance’s own origin. The shared app host, app.sign.rutba.io, does not accept API key requests yet.
curl https://YOUR-INSTANCE/api/sign/v1/envelopes \
-H "Authorization: Bearer sgk_..." \
-H "Content-Type: application/json" \
-d '{"title": "Mutual NDA"}'Requests and responses are JSON; results arrive under a data key and errors as { error: { message } } with the usual status codes. An unknown key and a revoked key get the same refusal: a key that enumerates is a key that leaks.
Endpoints
All under /api/sign/v1. Sending an envelope through the API is billed exactly like sending it from the app: one envelope, one charge, whatever the door. The API describes itself: every instance serves its OpenAPI 3 document at GET /api/sign/v1/openapi.json, no key needed, and the SDK’s test suite walks every method against it.
| Method | Path | What it does |
|---|---|---|
| POST | /envelopes | Create a draft envelope |
| GET | /envelopes | List envelopes, filterable by status |
| GET | /envelopes/{id} | One envelope: parties, documents, progress |
| POST | /envelopes/{id}/documents | Attach a document (base64; sha-256 pinned on entry) |
| POST | /envelopes/{id}/parties | Add a signer, approver, witness or cc party |
| POST | /envelopes/{id}/fields | Place a field by page coordinates or by anchor text the document carries |
| DELETE | /envelopes/{id}/fields/{fieldId} | Remove a field from a draft (evidence-logged) |
| DELETE | /envelopes/{id}/parties/{partyId} | Remove a party from a draft, their fields with them |
| DELETE | /envelopes/{id}/documents/{docId} | Remove a document from a draft, its fields and versions with it |
| POST | /envelopes/{id}/send | Freeze documents, mint links, notify the first parties |
| POST | /envelopes/{id}/remind | Chase whoever’s turn it is (evidence-logged) |
| POST | /envelopes/{id}/void | Void, with a reason that joins the record |
| POST | /envelopes/{id}/documents/{docId}/revise | New version; voids exactly the signatures it invalidates |
| GET | /envelopes/{id}/events | The evidence ledger, as the tracking screen reads it |
| GET | /envelopes/{id}/artifacts | The completed package: certificate, executed copies, timestamp token, marks |
| GET | /envelopes/{id}/artifacts/{artifactId}/content | One artifact’s bytes; its sha256 is the one the record carries |
| GET | /envelopes/{id}/evidence-pack | The forensic bundle (ZIP): manifest, seal, chain, certificate, copies, recordings, README |
| GET | /verify/document/{sha256} | The original verifier: is this file the pinned original, an executed copy, or a certificate |
| GET | /envelopes/{id}/comments | The comment thread |
| POST | /envelopes/{id}/comments | Add a comment |
| GET | /templates | List reusable templates |
| POST | /templates/{id}/use | Make an envelope from a template |
| POST | /templates/{id}/bulk | One envelope per row, up to 100, refused whole on a bad row |
| GET | /verify/{digest} | Public verification: valid, tampered or unknown |
| POST | /envelopes/compose | The simple path: the draft and everything on it in one call; a refusal keeps the draft and names the step |
| POST | /envelopes/{id}/agreement | The calendar: effective date, term end, renewal notice (a management act; the seal is untouched) |
| POST | /envelopes/{id}/renew | Renew a completed agreement: a new draft chained to it, as-is or with changes |
| POST | /envelopes/{id}/fields/auto | One labelled signature pad per party on the last page |
| GET | /envelopes/{id}/formalities | Stamps, the corporate seal, notarisation: each with its stage, status and proof |
| POST | /envelopes/{id}/formalities | Declare a formality on a draft (before send, or before the seal) |
| POST | /envelopes/{id}/formalities/{formalityId}/satisfy | Satisfy with a reference, an amount and/or the proof; a waiting envelope seals |
| POST | /envelopes/{id}/formalities/{formalityId}/waive | Waive with a reason that joins the record |
| POST | /verify/receipt | Public verification of a party receipt: their own seal over their act |
The SDK
@rutba/sign-sdk is a dependency-free Node client: every method maps to exactly one endpoint above, so an integration is readable in one sitting and owns no transitive risk.
const { SignClient } = require('@rutba/sign-sdk');
const sign = new SignClient({ baseUrl: 'https://YOUR-INSTANCE', key: process.env.SIGN_API_KEY });
const draft = await sign.createEnvelope({ title: 'Mutual NDA' });
await sign.addDocument(draft.documentId, { name: 'nda.pdf', content: pdfBuffer });
await sign.addParty(draft.documentId, { role: 'signer', name: 'Ada Osei', email: 'ada@example.com' });
await sign.send(draft.documentId);Webhooks
Set a URL and a signing secret in Rutba Sign’s settings and every envelope event (sign.envelope.*, sign.party.*, sign.document.*) is POSTed to it, at-least-once with retries. Each delivery is signed with an HMAC over `${timestamp}.${body}`; verify it constant-time and timestamp-bounded, or let the SDK do it:
// X-Rutba-Sign-Signature: t=1756800000000,v1=<hex hmac>
const { verifyWebhook } = require('@rutba/sign-sdk');
app.post('/webhooks/rutba-sign', (req, res) => {
const ok = verifyWebhook({
body: req.rawBody, // the RAW body string
signature: req.get('x-rutba-sign-signature'),
secret: process.env.SIGN_WEBHOOK_SECRET, // set beside the URL in Sign's settings
});
if (!ok) return res.status(400).end();
const { event, envelopeId, occurredAt, payload } = JSON.parse(req.rawBody);
// sign.envelope.sent | sign.envelope.completed | sign.party.signed | ...
res.status(200).end(); // non-2xx is retried
});Verification, without trusting us
GET /verify/{digest} answers valid, tampered or unknown for any sealed record, with no key and no account. The sealed manifest is an Ed25519 JWS over the whole evidence chain, countersigned by Rutba with an RFC 3161 timestamp, so a verifier can check the mathematics without asking anyone’s permission, including ours. POST /verify/receipt does the same for one party’s receipt (their own seal over their act, issued the moment they acted), and GET /verify/document/{sha256} for a file in hand. On the shared app host, app.sign.rutba.io, what you bring is what finds the organization holding the record: the digest, the file’s hash, or the ledger entry a receipt cites. The same file signed in two organizations is not guessed at, and gets the answer a file nobody holds would get.
The platform surface
Everything above is served by the instance that holds the record. One thing is not: the countersignature. When a record seals, and the moment each party acts, the instance presents the digest to Rutba, which signs { iss, digest, received_at } on its own clock. GET strapi.rutba.io/api/sign/public/verify/{digest} answers countersigned (with the JWS, the attested instant, and whether the digest was presented as a manifest or a receipt) or the one constant unknown. The keys are at strapi.rutba.io/api/sign/public/jwks. No key, no account, and the check never touches the instance, so it keeps working whether or not the instance is up. What it attests is existence and time of a digest, and nothing else: not validity, not identity; those are the instance’s verifiers above.
The engine speaks to the platform through two other internal seams an integrator never calls directly: the countersign presentation itself, and the formality desk: a stamp, seal or notarisation a sender asked Rutba to arrange, filed with nothing of the envelope and polled until a person at Rutba records the reference your formality then carries as proof. Both shapes are frozen in the @rutba/contracts pack the engine and the platform each test against.
Send your first envelope from code
Where your organization’s Rutba instance has its own address, a Sign administrator mints a key in Rutba Sign’s settings and the curl above works as written. The shared app host does not accept API key requests yet, and Rutba Sign for individuals offers no API keys or webhooks. Rutba Sign is in early access. Card checkout is not open yet, so onboarding is hands-on.