Node client
@signsealer/sdk. No dependencies; runs anywhere fetch does.
npm install @signsealer/sdk
Node 18 or later, every edge runtime, and the browser — though an API key belongs on a server. TypeScript types ship in the package, every version on npm is published by a workflow from a tagged commit, never from somebody's machine, and the changelog ships in the package.
Three things it does that a hand-rolled client usually does not
- Retries the failures worth retrying, with an idempotency key.
Every write that creates something gets one automatically, so a network blip
does not become a second waiver. A
422is never retried; it will be refused the same way every time, and the message says why. Pass your own key as the last argument when you have one, for example your order id. A write the server cannot de-duplicate — rotating a webhook secret, adding or pairing a tablet, a replay, the gate’s link — is never repeated after an answer that leaves it unknown whether it happened; it throws, and you decide. - Surfaces the API's own sentence.
SignSealerErrorcarriesmessage,hint,code,statusandrequestId. The message is frequently the database's own words, which are more specific than anything a client layer could write. - Keeps the request id. Every response carries one; quote it and we can find the request.
The client with a key
import { SignSealer, SignSealerError } from "@signsealer/sdk";
const signsealer = new SignSealer({
apiKey: process.env.SIGNSEALER_KEY!,
baseUrl: "https://api.signsealer.com", // the default
maxAttempts: 3, // total, the first included
});
| Method | Calls | Returns |
|---|---|---|
| account() | GET /v1/account | Whose key this is: the business, the key's scopes, live or test, the sites its pages may be framed on, and whether it can text. |
| branding() | GET /v1/branding | The name, colors and logos the business shows its signers, and its signing host. |
| templates({ code? }) | GET /v1/templates | Every version of every template, or one template's versions; no text. |
| template(id) | GET /v1/templates/:id | One version whole: text, questions, settings, and every version of its code. |
| createTemplate(input, key?) | POST /v1/templates | { template_id }: version 1, as a draft. |
| editTemplate(id, input) | PATCH /v1/templates/:id | A draft changed; a published version refuses. |
| publishTemplate(id, key?) | POST / | The draft published: new documents use it, and its text is frozen. |
| reviseTemplate(id, input?, key?) | POST / | The next version, as a draft, from the newest. |
| restoreTemplate(id, input?, key?) | POST / | An older published version back, as a new draft above the newest. |
| discardTemplate(id) | DELETE /v1/templates/:id | A draft nothing was made from, gone. |
| setTemplateSettings(id, input) | PATCH / | One-time, address, photographs in the copy, how long a signature lasts, reminders, sign again. |
| templatePdf(id) | GET / | Whether a template is an uploaded PDF, and whether every page is drawn. |
| documents({ status, q, external_ref, limit, offset }) | GET /v1/documents | Documents, newest first, with total and has_more. |
| document(id) | GET /v1/documents/:id | One document with its signers, trail, answers and your reference. |
| prepareDocument({ template_code, values, title, expires_at, sequential, external_ref, metadata, return_url, time_zone }, key?) | POST /v1/documents | { document_id }. Refused with the placeholder's name if a value is missing. |
| send(documentId, signers, key?) | POST / | { links }: one per signer, shown once, each with its url. Each needs an email, a mobile, or both; delivery: "embedded" sends them nothing. |
| remind(documentId, signerId, key?) | POST / | A fresh link, sent to them. |
| embedLink(documentId, signerId) | POST / | A fresh link to frame, with the sites it may be framed on. |
| void(documentId, reason, key?) | POST / | Every link dead; the reason on the trail. |
| certificate(documentId) | GET / | The certificate, or null before completion. |
| pdf(documentId) | GET / | { filename, bytes }: the sealed PDF. |
| bundle(documentId) | GET / | { filename, bytes }: the evidence bundle. |
| workflows() | GET /v1/workflows | Public forms, QR and kiosk workflows. |
| workflowQr(workflowId) | GET /v1/workflows/:id/qr | The public form's address as a QR code: SVG and a 1024px PNG. |
| startWorkflow(workflowId, participant, key?) | POST / | A link for that person. With expected_template_version and captured, a start a tablet ran with no network; 409 template_changed if the form moved on. |
| workflowBundle(workflowId) · workflowPage(workflowId, n) | GET / | The published form, its wording and versions, and its PDF pages, to hold on a device ahead of time. |
| upsertReservation(input) | PUT /v1/subjects | A packet for the booking; idempotent on your reference, so it takes no key. |
| packet(id) | GET /v1/packets/:id | Every item, every person, and whether the packet is ready. |
| startPacket(id) · voidPacket(id, reason) | POST / | Start everything it still owes, safe to repeat; or void what nobody has finished. |
| packetByCode(code) | GET / | A signing code from the receipt, resolved for a paired tablet: what is owed, by first name and last initial. |
| documentSigners(id) | GET / | Who has signed one document, and nothing else; once signed, whether it is still good. |
| standing({ template, email?, phone? }) | GET /v1/standing | Whether a person's signature on a template is still good: current, expired, resign or on_file, with valid_until. |
| gateLink(documentId, signerId) | POST / | A fresh link for a signer who has not signed, with a QR code as SVG. |
| reportProblem(input) | POST /v1/devices/report | A tablet's problem as names and numbers; never a message. |
| startPacketItem(itemId, key?) | POST / | A link for one item. |
| recordSmsConsent({ phone, obtainedVia, wording }, key?) | POST /v1/sms-consents | Consent you took on your own form. What the wording must say. |
| smsConsent(phone) | GET /v1/sms-consents | Whether you may text that number, and where the consent came from. |
| webhooks() · addWebhook(input, key?) · setWebhook(id, input) · removeWebhook(id, reason) | /v1/webhooks | Endpoints; the secret is shown once on add. |
| rotateWebhookSecret(id) · webhookDeliveries(id) · replayWebhookDelivery(deliveryId, key?) | / | A new secret, both signing for 24 hours; the last fifty deliveries; one sent again. |
| integrations() · integrationEvents(connectionId) | GET /v1/integrations · /:id/events | The booking systems it can connect and has, and what arrived from one. |
| devices() · addDevice(input) · newDeviceCode(id) · setDevice(id, input) · removeDevice(id, reason) | /v1/devices | Tablets from your own software; a pairing code is shown once. |
| pairDevice(input) · deviceHeartbeat(input? | POST /v1/devices/pair · /heartbeat | On the tablet: trade a code for its own credential (no key), then say it is still here. |
Each method's request and response are the ones on the API reference, which is generated from the router; the client adds nothing the API does not have.
The signer's side, with no key
SignSealerSigning takes no API key, because on those routes a
signing token is the whole credential. It is a separate class on purpose: a
client that could send a key alongside a token would be a client that widens
somebody's authority by accident. The one credential it will carry is a paired
tablet's own, given as deviceKey, and only on a signature that
says when it was taken offline (below).
import { SignSealerSigning } from "@signsealer/sdk";
const signing = new SignSealerSigning();
const link = await signing.link(token);
if (!link.ok) return show(link.reason); // written for a person to read
await signing.consent(token); // before signing, always
await signing.supply(token, "emergency_contact", "Sam Reyes, +1 503 555 0199");
await signing.sign(token, "typed", "Dana Reyes");
// or: await signing.decline(token, "wrong dates");
const result = await signing.verify("4KJ72M9PXQ3TVW8H");
// { certificate_found: true, still_matches_record: true, … }
Consent is a separate call and that is not an oversight: ESIGN requires
consent to electronic records to precede the transaction, and a system that
records both in the same instant cannot show that it did. If you would rather
not build a signing page, send the signer to
https://api.signsealer.com/s/<token> and skip all of it.
Signing where there is no network
A paired tablet that took a signature offline passes captured
with what the device thought the time was; the server records when it received
the event and keeps the claim beside it, marked as a claim. The claim is kept
only from a paired tablet of the business the link belongs to, because the day
it carries is the day written into the document: build the client with the
tablet's credential (the key pairDevice answered) and
it is sent with that one call. From anybody else captured is left
out without an error and the signature is dated when it arrives.
const signing = new SignSealerSigning({ deviceKey: tabletKey });
await signing.sign(token, "drawn", dataUri,
{ offline: true, at: capturedAt.toISOString(), device: "counter-2" });
A template that is a PDF
When the template behind a link is an uploaded PDF,
link.pages lists its pages and a field may carry a
place. Draw the pages rather than link.body: a PDF
template's body is a sentence naming the file and its fingerprint, so
rendering it would show somebody a hash where their lease should be.
Signing a PDF has the whole of it.
for (const p of link.pages ?? []) {
const img = new Image();
img.src = signing.pageUrl(token, p.n); // no API key: the token is the credential
}
/* And on the keyed side, whether a template is one of these at all. */
const pdf = await signsealer.templatePdf(templateId);
if (pdf.has_pdf && !pdf.ready) { /* not every page is drawn; it cannot be published */ }
Errors
try {
await signsealer.prepareDocument({ template_code: "rental-agreement", values: {} });
} catch (e) {
if (e instanceof SignSealerError) {
console.error(e.status); // 422
console.error(e.code); // "invalid"
console.error(e.message); // "no value for guest_name"
console.error(e.hint); // "A contract that ships with a placeholder…"
console.error(e.requestId); // quote this and we can find the request
}
}
Other languages
Python, Ruby, PHP and Go clients are not written. The API is small enough
that a hand-rolled client is a morning's work; the quickstart
shows the four calls in Python with requests, and
openapi.json feeds any generator.
Versions
This page describes @signsealer/sdk 1.17.0. The package
declares Node 18 or later (engines in its
package.json), as it has since 1.0.0; outside Node it needs a
runtime with fetch. On Node 18, use 1.11.1 or later: before it,
every write not given its own idempotency key threw unless Node was started
with --experimental-global-webcrypto. Each version's changes are in
CHANGELOG.md, which ships in the package.
What you have
npm ls @signsealer/sdk # the version installed here
npm view @signsealer/sdk version # the newest on npm
From code, the package exports its own package.json (from an
ES module, make require with createRequire):
const { version } = require("@signsealer/sdk/package.json");
In TypeScript, calling a method your copy does not have is a compile error;
in JavaScript it is a TypeError when the line runs.
Numbers that never reached npm
Eight versions in the changelog were never published: 1.0.1, 1.1.0, 1.2.0, 1.3.0, 1.5.0, 1.6.0, 1.8.0 and 1.9.0. What each added first reached npm in the next version that was.
| Added in | First on npm in |
|---|---|
| 1.0.1, 1.1.0, 1.2.0, 1.3.0 | 1.4.0 |
| 1.5.0, 1.6.0 | 1.7.0 |
| 1.8.0, 1.9.0 | 1.10.0 |
When each method arrived
The options listed are the ones you send. Fields in an answer are not listed: the client hands back whatever the API sends, so a newer version only declares them in its types.
| Method | Added in | Since then |
|---|---|---|
new SignSealer({ apiKey, baseUrl, maxAttempts }) | 1.0.0 | 1.1.0: apiKey may be left out, for pairDevice. |
account() | 1.0.0 | |
branding() | 1.0.0 | 1.11.0: the Branding type names what the API sends (businessName, primaryColor, hasLogo, signingHost, logoUrl and the rest). The names it declared before were never sent, so code written against them no longer compiles. |
templates({ code? }) | 1.0.0 | 1.6.0: code. |
template(id) | 1.6.0 | |
createTemplate(input, key?) | 1.0.0 | |
editTemplate, publishTemplate, reviseTemplate, discardTemplate, setTemplateSettings | 1.6.0 | 1.7.0: change_note on editTemplate and reviseTemplate. 1.10.0: photos_in_copy on setTemplateSettings. |
restoreTemplate | 1.7.0 | |
templatePdf(id) | 1.0.0 | |
documents(filter) | 1.0.0 | 1.0.1: offset. 1.6.0: external_ref. |
document(id) | 1.0.0 | |
prepareDocument(input, key?) | 1.0.0 | 1.6.0: external_ref, metadata and return_url. 1.11.0: time_zone. |
send(documentId, signers, key?) | 1.0.0 | 1.3.0: phone on a signer. 1.6.0: delivery: "embedded". 1.14.0: a signer held for their turn has token: null and held: true. 1.16.0: for_business on a signer. |
remind, embedLink | 1.6.0 | |
void, certificate, pdf, bundle | 1.0.0 | |
workflows(), workflowQr(workflowId) | 1.0.0 | 1.17.0: email_check, the website link's limits, closed and needs_attention on each workflow, and address among its questions' kinds. |
startWorkflow(workflowId, participant, key?) | 1.0.0 | 1.1.0: expected_template_version and captured. 1.14.0: a fixed role held for their turn has token: null and held: true. 1.16.0: for_business on each link. |
workflowBundle, workflowPage | 1.1.0 | |
upsertReservation, packet, startPacketItem | 1.0.0 | |
startPacket, voidPacket | 1.9.0 | |
packetByCode, documentSigners, gateLink, reportProblem | 1.2.0 | |
standing({ template, email?, phone? }) | 1.5.0 | |
recordSmsConsent, smsConsent | 1.0.0 | |
textingNumberInvitation(prefill? | 1.13.0 | |
webhooks, addWebhook, setWebhook, removeWebhook, rotateWebhookSecret, webhookDeliveries, replayWebhookDelivery | 1.0.0 | |
integrations, integrationEvents | 1.9.0 | |
devices, addDevice, newDeviceCode, setDevice, removeDevice | 1.1.0 | 1.7.0: hand_off and gate on setDevice. 1.11.0: workflow_ids. |
pairDevice, deviceHeartbeat | 1.0.0 | 1.2.0: queue_waiting, queue_refused and synced on deviceHeartbeat. |
SignSealerSigning: link, consent, supply, sign, decline, verify, pageUrl | 1.0.0 | 1.11.0: deviceKey on the constructor. |
SignSealerSigning: addParty, partyShare, partyLink | 1.15.0 | |
SignSealerPartner: profile, businesses, statements, SignSealerPartner.signupUrl | 1.8.0 |
Retries changed in 1.6.0. Before it,
rotateWebhookSecret, replayWebhookDelivery,
pairDevice, addDevice, reportProblem and
gateLink were sent again after a lost answer or a 5xx,
and could happen twice. From 1.6.0 each is sent once and throws, and you
decide. On npm that is 1.7.0 or later.
Ready to build? An API key takes a minute in the portal, and the free plan covers the first 10 agreements a month.
Get an API key