Rutba Sign

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.

One large rounded square joined by right-angled lines to three smaller ones, the middle in teal

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.

MethodPathWhat it does
POST/envelopesCreate a draft envelope
GET/envelopesList envelopes, filterable by status
GET/envelopes/{id}One envelope: parties, documents, progress
POST/envelopes/{id}/documentsAttach a document (base64; sha-256 pinned on entry)
POST/envelopes/{id}/partiesAdd a signer, approver, witness or cc party
POST/envelopes/{id}/fieldsPlace 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}/sendFreeze documents, mint links, notify the first parties
POST/envelopes/{id}/remindChase whoever’s turn it is (evidence-logged)
POST/envelopes/{id}/voidVoid, with a reason that joins the record
POST/envelopes/{id}/documents/{docId}/reviseNew version; voids exactly the signatures it invalidates
GET/envelopes/{id}/eventsThe evidence ledger, as the tracking screen reads it
GET/envelopes/{id}/artifactsThe completed package: certificate, executed copies, timestamp token, marks
GET/envelopes/{id}/artifacts/{artifactId}/contentOne artifact’s bytes; its sha256 is the one the record carries
GET/envelopes/{id}/evidence-packThe 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}/commentsThe comment thread
POST/envelopes/{id}/commentsAdd a comment
GET/templatesList reusable templates
POST/templates/{id}/useMake an envelope from a template
POST/templates/{id}/bulkOne envelope per row, up to 100, refused whole on a bad row
GET/verify/{digest}Public verification: valid, tampered or unknown
POST/envelopes/composeThe simple path: the draft and everything on it in one call; a refusal keeps the draft and names the step
POST/envelopes/{id}/agreementThe calendar: effective date, term end, renewal notice (a management act; the seal is untouched)
POST/envelopes/{id}/renewRenew a completed agreement: a new draft chained to it, as-is or with changes
POST/envelopes/{id}/fields/autoOne labelled signature pad per party on the last page
GET/envelopes/{id}/formalitiesStamps, the corporate seal, notarisation: each with its stage, status and proof
POST/envelopes/{id}/formalitiesDeclare a formality on a draft (before send, or before the seal)
POST/envelopes/{id}/formalities/{formalityId}/satisfySatisfy with a reference, an amount and/or the proof; a waiting envelope seals
POST/envelopes/{id}/formalities/{formalityId}/waiveWaive with a reason that joins the record
POST/verify/receiptPublic 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.

One family

The rest of Rutba

One account across all of it. Sign in once and the products know each other.

SIGN-DEVELOPERS · 7596826