Skip to content

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 signaturePaused on 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

VersionSurface
0.1.0Runtime path: signatures.fetch, signatures.status, users.lookup.
0.2.0This release. Users CRUD, orgs (incl. GDPR export), apiKeys, auth recovery, pagination iterator.
0.3.0+Templates, brand, deploy, billing.
1.0.0First stable release. 12-month deprecation runway from this point.

Track per-release changes in CHANGELOG.md.

Where to file what

WhatWhere
API behavior / spec questionsdocs.esigkit.com or support@esigkit.com
SDK bugs / feature requestsgithub.com/eSigKit/sdk-typescript/issues
DPA / data-processinglegal@esigkit.com