Docs

Docs / Start / Node client

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 422 is 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. SignSealerError carries message, hint, code, status and requestId. 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
});
MethodCallsReturns
account()GET /v1/accountWhose 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/brandingThe name, colors and logos the business shows its signers, and its signing host.
templates({ code? })GET /v1/templatesEvery version of every template, or one template's versions; no text.
template(id)GET /v1/templates/:idOne 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/:idA draft changed; a published version refuses.
publishTemplate(id, key?)POST /v1/templates/:id/publishThe draft published: new documents use it, and its text is frozen.
reviseTemplate(id, input?, key?)POST /v1/templates/:id/reviseThe next version, as a draft, from the newest.
restoreTemplate(id, input?, key?)POST /v1/templates/:id/restoreAn older published version back, as a new draft above the newest.
discardTemplate(id)DELETE /v1/templates/:idA draft nothing was made from, gone.
setTemplateSettings(id, input)PATCH /v1/templates/:id/settingsOne-time, address, photographs in the copy, how long a signature lasts, reminders, sign again.
templatePdf(id)GET /v1/templates/:id/pdfWhether a template is an uploaded PDF, and whether every page is drawn.
documents({ status, q, external_ref, limit, offset })GET /v1/documentsDocuments, newest first, with total and has_more.
document(id)GET /v1/documents/:idOne 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 /v1/documents/:id/send{ 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 /v1/documents/:id/signers/:signer/remindA fresh link, sent to them.
embedLink(documentId, signerId)POST /v1/documents/:id/signers/:signer/embedA fresh link to frame, with the sites it may be framed on.
void(documentId, reason, key?)POST /v1/documents/:id/voidEvery link dead; the reason on the trail.
certificate(documentId)GET /v1/documents/:id/certificateThe certificate, or null before completion.
pdf(documentId)GET /v1/documents/:id/pdf{ filename, bytes }: the sealed PDF.
bundle(documentId)GET /v1/documents/:id/bundle{ filename, bytes }: the evidence bundle.
workflows()GET /v1/workflowsPublic forms, QR and kiosk workflows.
workflowQr(workflowId)GET /v1/workflows/:id/qrThe public form's address as a QR code: SVG and a 1024px PNG.
startWorkflow(workflowId, participant, key?)POST /v1/workflows/:id/startA 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 /v1/workflows/:id/bundle · /pages/:nThe published form, its wording and versions, and its PDF pages, to hold on a device ahead of time.
upsertReservation(input)PUT /v1/subjectsA packet for the booking; idempotent on your reference, so it takes no key.
packet(id)GET /v1/packets/:idEvery item, every person, and whether the packet is ready.
startPacket(id) · voidPacket(id, reason)POST /v1/packets/:id/start · /voidStart everything it still owes, safe to repeat; or void what nobody has finished.
packetByCode(code)GET /v1/packets/code/:codeA signing code from the receipt, resolved for a paired tablet: what is owed, by first name and last initial.
documentSigners(id)GET /v1/documents/:id/signersWho has signed one document, and nothing else; once signed, whether it is still good.
standing({ template, email?, phone? })GET /v1/standingWhether a person's signature on a template is still good: current, expired, resign or on_file, with valid_until.
gateLink(documentId, signerId)POST /v1/documents/:id/signers/:signer/linkA fresh link for a signer who has not signed, with a QR code as SVG.
reportProblem(input)POST /v1/devices/reportA tablet's problem as names and numbers; never a message.
startPacketItem(itemId, key?)POST /v1/packets/items/:id/startA link for one item.
recordSmsConsent({ phone, obtainedVia, wording }, key?)POST /v1/sms-consentsConsent you took on your own form. What the wording must say.
smsConsent(phone)GET /v1/sms-consentsWhether you may text that number, and where the consent came from.
webhooks() · addWebhook(input, key?) · setWebhook(id, input) · removeWebhook(id, reason)/v1/webhooksEndpoints; the secret is shown once on add.
rotateWebhookSecret(id) · webhookDeliveries(id) · replayWebhookDelivery(deliveryId, key?)/v1/webhooks/:id/rotate · /deliveries · /v1/webhooks/deliveries/:id/replayA new secret, both signing for 24 hours; the last fifty deliveries; one sent again.
integrations() · integrationEvents(connectionId)GET /v1/integrations · /:id/eventsThe booking systems it can connect and has, and what arrived from one.
devices() · addDevice(input) · newDeviceCode(id) · setDevice(id, input) · removeDevice(id, reason)/v1/devicesTablets from your own software; a pairing code is shown once.
pairDevice(input) · deviceHeartbeat(input?)POST /v1/devices/pair · /heartbeatOn 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 inFirst on npm in
1.0.1, 1.1.0, 1.2.0, 1.3.01.4.0
1.5.0, 1.6.01.7.0
1.8.0, 1.9.01.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.

MethodAdded inSince then
new SignSealer({ apiKey, baseUrl, maxAttempts })1.0.01.1.0: apiKey may be left out, for pairDevice.
account()1.0.0
branding()1.0.01.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.01.6.0: code.
template(id)1.6.0
createTemplate(input, key?)1.0.0
editTemplate, publishTemplate, reviseTemplate, discardTemplate, setTemplateSettings1.6.01.7.0: change_note on editTemplate and reviseTemplate. 1.10.0: photos_in_copy on setTemplateSettings.
restoreTemplate1.7.0
templatePdf(id)1.0.0
documents(filter)1.0.01.0.1: offset. 1.6.0: external_ref.
document(id)1.0.0
prepareDocument(input, key?)1.0.01.6.0: external_ref, metadata and return_url. 1.11.0: time_zone.
send(documentId, signers, key?)1.0.01.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, embedLink1.6.0
void, certificate, pdf, bundle1.0.0
workflows(), workflowQr(workflowId)1.0.01.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.01.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, workflowPage1.1.0
upsertReservation, packet, startPacketItem1.0.0
startPacket, voidPacket1.9.0
packetByCode, documentSigners, gateLink, reportProblem1.2.0
standing({ template, email?, phone? })1.5.0
recordSmsConsent, smsConsent1.0.0
textingNumberInvitation(prefill?)1.13.0
webhooks, addWebhook, setWebhook, removeWebhook, rotateWebhookSecret, webhookDeliveries, replayWebhookDelivery1.0.0
integrations, integrationEvents1.9.0
devices, addDevice, newDeviceCode, setDevice, removeDevice1.1.01.7.0: hand_off and gate on setDevice. 1.11.0: workflow_ids.
pairDevice, deviceHeartbeat1.0.01.2.0: queue_waiting, queue_refused and synced on deviceHeartbeat.
SignSealerSigning: link, consent, supply, sign, decline, verify, pageUrl1.0.01.11.0: deviceKey on the constructor.
SignSealerSigning: addParty, partyShare, partyLink1.15.0
SignSealerPartner: profile, businesses, statements, SignSealerPartner.signupUrl1.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