Guide
@esigkit/node is the
official Node.js SDK for the eSigKit API. Use it from email-send services,
back-office jobs, server actions, or anywhere else you’d otherwise hand-roll
fetch against api.esigkit.com.
Install
npm install @esigkit/node
# or: pnpm add @esigkit/node / yarn add @esigkit/node
Requires Node ≥ 18 (uses native fetch, AbortController,
crypto.randomUUID). TypeScript types ship with the package.
Server-side only
Quickstart
The hot path: resolve a user’s signature at email-send time and inline it into the outgoing HTML.
import { Esigkit } from '@esigkit/node';
import nodemailer from 'nodemailer';
const esk = new Esigkit({ apiKey: process.env.ESIGKIT_API_KEY });
const sig = await esk.signatures.fetch({ email: 'alice@example.com' });
if (sig.status === 'live') {
await transporter.sendMail({
from: 'alice@example.com',
to: 'bob@example.com',
subject: 'hello',
html: `<p>Hi Bob,</p><p>...</p>${sig.html}`,
});
}
sig.status is one of 'live' | 'paused' | 'never_rendered' | 'not_found'.
Only 'live' carries non-empty html. Unknown emails return the
not_found shape rather than throwing — partial misses in a batch send
don’t break the loop.
Authentication
Three forms of credential resolution, in order of precedence:
new Esigkit({ apiKey: 'esk_live_…' }); // string
new Esigkit({ apiKey: () => readSecret() }); // callable (sync or async)
new Esigkit(); // reads ESIGKIT_API_KEY env
The callable form is awaited per request, so you can rotate keys through Vault / AWS Secrets Manager / similar without restarting the process — return the latest value from the callback and the very next request picks it up.
Mint API keys from
app.esigkit.com → Settings → API keys.
Keys cannot be created or rotated via the API itself — that surface is
JWT-only by design (see the API keys section of the auth
guide).
Pointing at staging
new Esigkit({
apiKey: process.env.ESIGKIT_API_KEY,
baseUrl: 'https://api-test.esigkit.com',
});
Or via env: ESIGKIT_BASE_URL=https://api-test.esigkit.com. The SDK
sanity-checks string-form keys against the resolved base URL on
construction and warns when they don’t match (e.g., esk_live_… against
api-test).
Signatures
The runtime path. Two methods, both accept { email } or { userId }:
// Fetch rendered HTML (with metadata).
const sig = await esk.signatures.fetch({ email: 'tyler@example.com' });
// { status, html, userId, orgId, signatureVersion, paused,
// resolvedUrl, publicUrl?, checkedAt }
// Fetch metadata only (no HTML, no caching).
const status = await esk.signatures.status({ userId: 'usr_…' });
// { userId, orgId, status, signatureVersion, paused,
// resolvedUrl, publicUrl?, checkedAt }
Per-call options
// Bypass the in-process cache (see "Cache consistency" below).
await esk.signatures.fetch({ email }, { ttl: 0 });
// Override TTL for this call only (ms).
await esk.signatures.fetch({ email }, { ttl: 5_000 });
// Cancellation via AbortSignal — propagates through the fetch chain.
const ac = new AbortController();
setTimeout(() => ac.abort(), 1_000);
await esk.signatures.fetch({ email }, { signal: ac.signal });
When you pass { email }, the SDK transparently calls users.lookup
first to resolve userId. Skip that round-trip by passing { userId }
directly when you have it cached.
Users
Batch lookup
const results = await esk.users.lookup([
'alice@example.com',
'unknown@example.com',
]);
// [
// { email: 'alice@example.com', userId: 'usr_…', found: true },
// { email: 'unknown@example.com', found: false },
// ]
Cap is 100 emails per call (matches the API’s batch limit). Order is preserved; duplicate inputs each get their own result entry.
CRUD
// Create
const u = await esk.users.create({
orgId,
email: 'engineering@example.com',
firstName: 'Jordan',
lastName: 'Engineering',
});
// Read
const same = await esk.users.get({ orgId, userId: u.userId });
// Update — version is required for optimistic concurrency (see below)
const updated = await esk.users.update({
orgId,
userId: u.userId,
title: 'Senior Engineer',
version: u.version,
});
// Delete (soft — sets `deletedAt`; idempotent)
await esk.users.delete({ orgId, userId: u.userId });
Listing
.list() returns an async iterable that walks every page automatically:
for await (const user of esk.users.list({ orgId })) {
console.log(user.email);
}
For checkpointing long-running jobs, use .pages() instead — yields one
page at a time so you can persist the cursor between batches:
for await (const page of esk.users.list({ orgId }).pages()) {
await processBatch(page.items);
await checkpoint(page.nextCursor);
if (shouldStop()) break;
}
Both forms pass through the underlying API’s cursor pagination — you don’t construct cursors yourself.
Optimistic concurrency
Every resource with a version: number field uses optimistic concurrency.
Pass back the version you got from the most recent read; the server
returns 409 CONFLICT (→ EsigkitConflictError) when your version is
stale.
import { EsigkitConflictError } from '@esigkit/node';
async function setTitle(orgId: string, userId: string, title: string) {
for (let attempt = 0; attempt < 3; attempt++) {
const u = await esk.users.get({ orgId, userId });
try {
return await esk.users.update({
orgId,
userId,
title,
version: u.version,
});
} catch (e) {
if (e instanceof EsigkitConflictError && attempt < 2) continue;
throw e;
}
}
}
Orgs
// Read
const org = await esk.orgs.get(orgId);
// Update (OCC — pass back version)
await esk.orgs.update({
orgId,
name: 'Renamed',
version: org.version,
});
// GDPR Article 15 export — returns a presigned URL valid for 7 days
const exp = await esk.orgs.export(orgId);
console.log(exp.downloadUrl);
API keys
// Mint — plaintext is returned ONCE; store it now or you'll need to mint again.
const minted = await esk.apiKeys.create({ orgId, name: 'CI deploy bot' });
console.log(minted.plaintext);
// List — async iterable
for await (const key of esk.apiKeys.list({ orgId })) {
console.log(`${key.prefix}… ${key.name}`);
}
// Revoke — idempotent (re-revoking returns 200 no-op)
await esk.apiKeys.revoke({ orgId, keyId: minted.keyId });
Auth recovery flows
Two narrow utilities for self-healing federated-auth edge cases:
// Reprovision — for users whose Cognito + DDB rows fell out of sync.
// JWT must carry `custom:needs_provisioning=true`; called from the
// dashboard's onboarding flow rather than directly by integrators.
await esk.auth.reprovision({ orgId, role: 'admin' });
// Add a password backup login on a federated (e.g., Google) account.
await esk.auth.addPassword({ password: 'REPLACEME-strongPa55word' });
Both are covered by the dashboard UI; you only need them if you’re building a custom onboarding flow on top of the API.
Errors
Every error class is exported from the top level and from a
/errors subpath (the latter is tree-shake-friendly):
import {
EsigkitError, // base
EsigkitBadRequestError, // BAD_REQUEST (400)
EsigkitAuthError, // UNAUTHORIZED (401)
EsigkitPlanLimitError, // PAYMENT_REQUIRED (402)
EsigkitForbiddenError, // FORBIDDEN (403)
EsigkitNotFoundError, // NOT_FOUND (404)
EsigkitConflictError, // CONFLICT (409) — has `serverVersion`
EsigkitPreconditionError, // PRECONDITION_FAILED (412)
EsigkitPayloadTooLargeError, // PAYLOAD_TOO_LARGE (413)
EsigkitRateLimitError, // TOO_MANY_REQUESTS (429) — has `retryAfter`
EsigkitServerError, // INTERNAL_ERROR (5xx)
EsigkitNetworkError, // base for network failures
EsigkitTimeoutError, // request timed out
EsigkitConnectionError, // TCP / TLS handshake failure
} from '@esigkit/node/errors';
Every instance carries code, message, details, and correlationId.
The correlationId matches the X-Correlation-Id response header — paste
it into a support ticket and we can find your request in CloudWatch.
import { EsigkitRateLimitError } from '@esigkit/node/errors';
try {
await esk.users.list({ orgId });
} catch (e) {
if (e instanceof EsigkitRateLimitError) {
await sleep((e.retryAfter ?? 1) * 1000);
// …retry
} else {
throw e;
}
}
PII safety
Every error class implements both:
.toString()— human-readable; emails redacted (a***@example.com)..toJSON()— full structured payload, no redaction.
console.error(err) calls .toString() by default, so accidental
log-line PII is opt-in: you’d have to write
console.error(err.toJSON()) or JSON.stringify(err) to log the
unredacted form.
Cache consistency
signatures.fetch() caches successful results in-process for 60 seconds.
Two implications you should know about:
- Pause toggles propagate within the TTL window. A user toggling
signaturePausedon the dashboard will keep returning the live signature for up to 60 seconds. For real-time accuracy, pass{ ttl: 0 }. - Deleted users. A hard-deleted user’s signature can keep returning cached HTML for the rest of the TTL window. The eSigKit API doesn’t currently emit outbound webhooks — invalidation is purely time-based.
The CDN itself caches the rendered HTML for 24 hours under a
versioned URL ({orgId}/{userId}.{version}.html), so the version
increment on re-render is what flushes the bytes. The SDK’s TTL is just
a cap on how stale the resolved-version pointer can get.
AbortSignal + cancellation
Every method accepts { signal: AbortSignal }. Useful when:
- You’re inside a request handler with its own cancellation deadline (you can pass through the framework’s signal).
- You’re fanning out parallel calls and want to cancel siblings on first failure.
import { AbortController } from 'node:abort-controller';
async function fetchOrAbort(emails: string[], deadlineMs: number) {
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), deadlineMs);
try {
return await Promise.all(
emails.map((email) =>
esk.signatures.fetch({ email }, { signal: ac.signal }),
),
);
} finally {
clearTimeout(timer);
}
}
Aborting raises an AbortError, not an EsigkitError — the SDK passes
it through unchanged so consumers can use the standard pattern.
Roadmap
| Version | Surface |
|---|---|
0.1.0 | Runtime path: signatures.fetch, signatures.status, users.lookup. |
0.2.0 | This release. Users CRUD, orgs (incl. GDPR export), apiKeys, auth recovery, pagination iterator. |
0.3.0+ | Templates, brand, deploy, billing. |
1.0.0 | First stable release. 12-month deprecation runway from this point. |
Track per-release changes in CHANGELOG.md.
Where to file what
| What | Where |
|---|---|
| API behavior / spec questions | docs.esigkit.com or support@esigkit.com |
| SDK bugs / feature requests | github.com/eSigKit/sdk-typescript/issues |
| DPA / data-processing | legal@esigkit.com |