Node client
@signsealer/node. No dependencies; runs anywhere fetch does.
npm install @signsealer/node
Node 18 or later, every edge runtime, and the browser — though an API key
belongs on a server. TypeScript types ship in the package. The source is in
the packages/sdk directory of the repository, and every version
on npm is published by a workflow from a tagged commit with provenance, so
the registry can say which commit built the tarball you installed.
Until the first version is on npm, the
package installs from a checkout: clone the repository, run
npm run sdk:build, and npm install ../signsealer/packages/sdk
from your project. This note goes away with the first tag.
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. - 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/node";
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: business, plan, scopes. |
| templates() | GET /v1/templates | Every template, with its current version. |
| documents({ status, q, limit }) | GET /v1/documents | Documents, newest first. |
| document(id) | GET /v1/documents/:id | One document with its signers and trail. |
| prepareDocument({ template_code, values, title, expires_at, sequential }, 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. |
| void(documentId, reason, key?) | POST /v1/documents/:id/void | Every link dead; the reason on the trail. |
| certificate(documentId) | GET /v1/documents/:id/certificate | The 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/workflows | Public forms, QR and kiosk workflows. |
| startWorkflow(workflowId, participant, key?) | POST /v1/workflows/:id/start | A link for that person. |
| upsertReservation(input, key?) | PUT /v1/reservations | A packet for the booking; idempotent on your reference. |
| packet(id) | GET /v1/packets/:id | Every item, every person, and whether the packet is ready. |
| startPacketItem(itemId, key?) | POST /v1/packets/items/:id/start | A link for one item. |
| webhooks() · addWebhook(input, key?) · setWebhook(id, input) · removeWebhook(id, reason) | /v1/webhooks | Endpoints; the secret is shown once on add. |
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.
import { SignSealerSigning } from "@signsealer/node";
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. Signing where there
is no network passes captured with what the device thought the
time was; the server records when it received the event and keeps your claim
beside it, marked as a claim. If you would rather not build a signing page,
send the signer to https://api.signsealer.com/s/<token>
and skip all of it.
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.
Ready to build? An API key takes a minute in the portal, and the free plan covers the first 25 agreements a month.
Get an API key