npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@assinafy/sdk

v1.5.0

Published

TypeScript SDK for Assinafy API - Digital signature platform

Readme

@assinafy/sdk

TypeScript SDK for the Assinafy API — a Brazilian digital signature platform.

Provides 100% endpoint coverage of the public API: documents, signers, assignments, templates, tags, workspaces, webhooks, field definitions, authentication, public/signer-side flows, and the high-level uploadAndRequestSignatures helper.

Requirements

  • Node.js 20+ (current LTS) for the built-in FormData / Blob APIs used by uploads
  • or Bun 1.0+

Installation

npm install @assinafy/sdk
# or
bun add @assinafy/sdk

The package is published to both npmjs.com and GitHub Packages. To install from GitHub Packages, add to your .npmrc:

@assinafy:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

Quick start

import { AssinafyClient } from '@assinafy/sdk';

const client = new AssinafyClient({
  apiKey: process.env.ASSINAFY_API_KEY!,
  accountId: process.env.ASSINAFY_ACCOUNT_ID!,
  webhookSecret: process.env.ASSINAFY_WEBHOOK_SECRET,
});

const result = await client.uploadAndRequestSignatures({
  source: { filePath: './contract.pdf' },
  signers: [
    { name: 'John Doe',    email: '[email protected]' },
    { name: 'Jane Smith',  email: '[email protected]', whatsapp_phone_number: '+5548999990000' },
  ],
  message: 'Please sign this contract',
});

console.log('Document ID:', result.document.id);

Authentication

The API supports two authentication methods. Prefer apiKey — it maps to the X-Api-Key header recommended by Assinafy for backend services.

// Preferred: X-Api-Key header
new AssinafyClient({ apiKey: 'k_xxx', accountId: 'acc_xxx' });

// Legacy: Authorization: Bearer <token>
new AssinafyClient({ token: 'jwt_xxx', accountId: 'acc_xxx' });

Configuration

| Option | Type | Default | Description | | --------------- | -------- | --------------------------------------- | --------------------------------------------- | | apiKey | string | — | Preferred credential (sent as X-Api-Key). | | token | string | — | Legacy access token (sent as Bearer). | | accountId | string | — | Default workspace/account ID. | | baseUrl | string | https://api.assinafy.com.br/v1 | Override base URL (e.g. the sandbox). | | webhookSecret | string | — | Shared secret used by WebhookVerifier. | | timeout | number | 30000 | Request timeout in milliseconds. | | maxRetries | number | 2 | Auto-retries on HTTP 429, honoring Retry-After. 0 disables. | | logger | Logger | no-op | Optional {debug,info,warn,error} logger. |

Rate limiting

The API allows ~120 requests/minute and returns X-Rate-Limit-* headers. On an HTTP 429, the client automatically retries up to maxRetries times, waiting for the server-provided Retry-After (or X-Rate-Limit-Reset) delay before each attempt. Only 429 is retried, so non-idempotent calls are safe.

Factories

// Positional factory
const client = AssinafyClient.create('api-key', 'account-id', { webhookSecret: 'shhh' });

// From a plain object (accepts snake_case or camelCase keys)
const client = AssinafyClient.fromConfig({
  api_key: process.env.ASSINAFY_API_KEY!,
  account_id: process.env.ASSINAFY_ACCOUNT_ID!,
});

Endpoint coverage

Every public endpoint documented in https://api.assinafy.com.br/v1/docs is covered. The table below maps each resource to its API surface.

| Resource | Endpoints | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | client.documents | list, upload, details, activities, waitUntilReady, download, thumbnail, downloadPage, statuses, delete, verify, createFromTemplate, estimateCostFromTemplate, getPublic, sendToken, listTags, replaceTags, addTags, detachTag, isFullySigned, getSigningProgress | | client.signers | create, get, list, update, delete, findByEmail | | client.assignments | create, estimateCost, resetExpiration, resendNotification, estimateResendCost, listWhatsAppNotifications | | client.templates | create, list, get, update, delete, downloadPage | | client.tags | list, create, update, delete | | client.workspaces | create, list, get, update, delete | | client.webhooks | register, get, inactivate, delete, listEventTypes, listDispatches, retryDispatch | | client.fields | create, list, get, update, delete, validate, validateMultiple, listTypes | | client.auth | login, socialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword | | client.signerDocuments | getCurrent, list, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline | | client.webhookVerifier | verify, extractEvent, getEventType, getEventData |

Resources

Most account-scoped methods accept an optional accountId that overrides the client default. Workspace get/update/delete always require an explicit account ID.

Documents

// Upload from a file path (recommended)
const doc = await client.documents.upload(
  { filePath: './contract.pdf' },
  { metadata: { type: 'service' } },
);
// → {
//   resource: 'document', id: '1031…', account_id: '102d…', template_id: null,
//   name: 'contract.pdf', status: 'uploaded',
//   artifacts: { original: 'https://…/download/original' },
//   signing_url: 'https://app…/sign/1031…',
//   pages: [],                 // populated once status reaches `metadata_ready`
//   tags: [], is_closed: false, created_at: '2026-…', updated_at: '2026-…'
// }

// …or from a Buffer already in memory
await client.documents.upload({ buffer, fileName: 'contract.pdf' });

// List → { data: IDocumentListItem[], meta?: { current_page, per_page, total, last_page } }
const { data, meta } = await client.documents.list({ page: 1, per_page: 20, sort: '-created_at' });
await client.documents.details(doc.id);
await client.documents.activities(doc.id);
await client.documents.waitUntilReady(doc.id, { maxWaitMs: 30_000 });

await client.documents.download(doc.id, 'certificated');   // 'original' | 'certificated' | 'certificate-page' | 'bundle'
await client.documents.thumbnail(doc.id);
await client.documents.downloadPage(doc.id, pageId);

await client.documents.statuses();                          // list every status code + deletable flag
await client.documents.isFullySigned(doc.id);
await client.documents.getSigningProgress(doc.id);
await client.documents.delete(doc.id);

// Verify a signed document by its SHA-1 hash
await client.documents.verify('FE32EDDADE7CBDDCBB934E7402047450B0E59C02');

// Public endpoints (no auth)
await client.documents.getPublic(doc.id);
await client.documents.sendToken(doc.id, '[email protected]', 'email');

// Tags attached to a document (by tag name; unknown names are auto-created)
await client.documents.listTags(doc.id);
await client.documents.replaceTags(doc.id, ['Contracts', '2026-Q1']); // [] detaches all
await client.documents.addTags(doc.id, ['Urgent']);                   // append, idempotent
await client.documents.detachTag(doc.id, tagId);                      // remove one

Uploads are validated locally: only .pdf files up to 25 MB are accepted (the API's current hard limit).

List endpoints return { data, meta } where meta is populated from the X-Pagination-* headers returned by the API.

Signers

await client.signers.create({
  full_name: 'John Doe',
  email: '[email protected]',
  whatsapp_phone_number: '+5548999990000',
  cpf: '123.456.789-00', // optional Brazilian tax ID — non-digits are stripped automatically
});
// → { id: '19e6…', full_name: 'John Doe', email: '[email protected]',
//     whatsapp_phone_number: '+5548999990000', has_accepted_terms: false }
// (note: `cpf` is accepted on input but never echoed back by the API)

// `email` is optional — a WhatsApp-only signer is valid (at least one is required)
await client.signers.create({
  full_name: 'WhatsApp Only',
  whatsapp_phone_number: '+5548999990000',
});

// PHP SDK compatibility aliases are also accepted
await client.signers.create({
  full_name: 'Jane Doe',
  email: '[email protected]',
  phone: '+5548999991111', // alias for whatsapp_phone_number
});

await client.signers.get(signerId);
await client.signers.list({ page: 1, per_page: 50, search: 'john' });
await client.signers.update(signerId, { full_name: 'Johnny Doe' });
await client.signers.delete(signerId);

const existing = await client.signers.findByEmail('[email protected]');

When an email is supplied, signers.create() is idempotent by email, matching the PHP SDK behavior: it reuses an existing signer when the same email is already present in the workspace. WhatsApp-only signers (no email) are always created fresh.

Assignments

// Signers may be ids or objects — the SDK normalises to the API shape.
await client.assignments.create(documentId, {
  method: 'virtual',
  signers: ['signer-1', 'signer-2'],
  message: 'Please review and sign',
  expires_at: '2024-12-31T23:59:00Z',
  copy_receivers: ['observer-id'],
});

// Sequential signing: `step` controls signing order (parallel within a step).
await client.assignments.create(documentId, {
  method: 'virtual',
  signers: [
    { id: 'signer-1', step: 1 },
    { id: 'signer-2', step: 2 }, // notified only after step 1 finishes
  ],
});

// Estimate cost (signers may omit `id` when only the channel matters) → ICostEstimate
await client.assignments.estimateCost(documentId, { signers: ['signer-1'] });
await client.assignments.estimateCost(documentId, {
  signers: [{ verification_method: 'Whatsapp' }],
});
// → {
//   documents: 1, credits: 0, needs_extra_document: false, extra_document_cost: 0,
//   total_credits: 0, breakdown: [], document_balance: 67, credit_balance: 0,
//   has_sufficient_resources: true, blocking_reason: null, message: null
// }

await client.assignments.resetExpiration(documentId, assignmentId, '2025-06-30T00:00:00Z');
await client.assignments.resetExpiration(documentId, assignmentId, null); // remove expiration

await client.assignments.resendNotification(documentId, assignmentId, signerId);
// → { is_sent: true, document_id: '…', signer_id: '…' }

await client.assignments.estimateResendCost(documentId, assignmentId, signerId);
// → { total: 0, breakdown: [{ code: 'NotificationEmailResend', name: '…', cost: 0 }],
//     credit_balance: 0, has_sufficient_credits: true }

await client.assignments.listWhatsAppNotifications(documentId, assignmentId); // → IWhatsAppNotification[]

The create response is an IAssignment: { id, method, signers: [...], items: [...], signing_urls: [{ signer_id, url }], … }.

For backwards compatibility, the SDK also accepts legacy signer_ids and signerIds payloads and rewrites them to the current signers: [{ id }] format expected by the API.

Cancelling a signature request. Assinafy has no workspace-side "cancel" endpoint. To stop a pending request either delete the document (when its status is deletable) or have the signer decline:

await client.documents.delete(documentId);                                  // workspace-side
await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'No longer needed'); // signer-side

Templates

// Create a template by uploading a PDF (multipart). The template starts in
// `Uploaded` status and becomes `Ready` once its pages are processed.
const created = await client.templates.create(
  { filePath: './nda.pdf' },          // or { buffer, fileName: 'nda.pdf' }
  { name: 'NDA template' },
);
// →
// {
//   resource: 'template', id: '1032...', name: 'nda.pdf',
//   document_name: 'nda.pdf', message: null, status: 'Uploaded',
//   roles: [{ id: '1032...', name: 'TemplateEditor', assignment_type: 'Editor' }],
//   pages: [], tags: [], created_at: '2026-…', updated_at: '2026-…'
// }

const { data, meta } = await client.templates.list({ search: 'NDA', per_page: 20 });
const template = await client.templates.get(created.id);   // includes pages[] + default_document_tags
await client.templates.update(created.id, { name: 'NDA v2', message: 'Please sign' });
await client.templates.downloadPage(created.id, template.pages![0].id); // → Buffer (JPEG)
await client.templates.delete(created.id);

// Create a *document* from a template (each signer maps to a template role)
await client.documents.createFromTemplate(
  templateId,
  [{ role_id: template.roles![0].id, id: signerId, verification_method: 'Email', notification_methods: ['Email'] }],
  { name: 'NDA - John Doe', message: 'Please sign at your earliest convenience.' },
);

// Estimate the cost before creating → ICostEstimate
await client.documents.estimateCostFromTemplate(templateId, [{ role_id: 'role_id', id: signerId }]);
// → { documents: 1, total_credits: 0, document_balance: 67, credit_balance: 0,
//     has_sufficient_resources: true, blocking_reason: null, breakdown: [], … }

Template creation only uploads the PDF and provisions the default editor role — configure roles/fields in the Assinafy editor (or the web UI) afterwards.

Tags

Workspace-scoped labels that can be attached to documents and templates. Tag names are unique per workspace (case-insensitive).

await client.tags.list({ search: 'contract' });          // ITag[]
const tag = await client.tags.create({ name: 'Contracts', color: 'ff8800' });
await client.tags.update(tag.id, { name: 'Sales Contracts' });
await client.tags.update(tag.id, { color: null });        // clear the color
await client.tags.delete(tag.id);                         // 409 if still attached
await client.tags.delete(tag.id, { force: true });        // detach everywhere, then delete

Attach/detach tags on a specific document via client.documents.listTags / replaceTags / addTags / detachTag (see Documents).

Workspaces

await client.workspaces.create({ name: 'My Workspace', primary_color: '#ff0066' });
await client.workspaces.list();
await client.workspaces.get(accountId);
await client.workspaces.update(accountId, { name: 'Renamed' });
await client.workspaces.delete(accountId);

Field definitions

Custom field types used by collect-method assignments.

await client.fields.create({ type: 'text', name: 'Contract Number' });
await client.fields.list({ include_inactive: true, include_standard: true });
await client.fields.get(fieldId);
await client.fields.update(fieldId, { name: 'Updated Name' });
await client.fields.delete(fieldId);

// Validate a single value (signer-access-code only required for signer-side calls)
await client.fields.validate(fieldId, '400.676.228-36', { signerAccessCode });

// Validate multiple values at once
await client.fields.validateMultiple(
  [
    { field_id: 'f1', value: '1111111111111' },
    { field_id: 'f2', value: '[email protected]' },
  ],
  { signerAccessCode },
);

// Catalog of every field type the platform recognises
await client.fields.listTypes();

Authentication / API key management

Most server-side integrations should just use X-Api-Key directly. Use these endpoints when you need to bootstrap a session for a human user.

const { access_token, user, accounts } = await client.auth.login('[email protected]', 'pw');
await client.auth.socialLogin({ provider: 'google', token: 'google-id-token', has_accepted_terms: true });

// Personal API key
await client.auth.createApiKey('current-password');
await client.auth.getApiKey();                     // → { api_key: '****...nBNr' } or null
await client.auth.deleteApiKey();

// Password lifecycle
await client.auth.changePassword({ email, password: 'current', new_password: 'next' });
await client.auth.requestPasswordReset('[email protected]');
await client.auth.resetPassword({ email, token: 'tk', new_password: 'next' });

Webhooks

await client.webhooks.register({
  url: 'https://example.com/webhooks/assinafy',
  email: '[email protected]',
  // events defaults to the current SDK default set below
  events: [
    'document_ready',
    'document_prepared',
    'signer_signed_document',
    'signer_rejected_document',
    'document_processing_failed',
  ],
});

await client.webhooks.get();          // current subscription or null
await client.webhooks.inactivate();
await client.webhooks.delete();
await client.webhooks.listEventTypes();
await client.webhooks.listDispatches({ delivered: false, page: 1, 'per-page': 20 });
await client.webhooks.retryDispatch(dispatchId);

Webhook verification

Webhook payloads are signed with HMAC-SHA256 of the raw body using the workspace webhookSecret. Assinafy sends the hex digest in the X-Assinafy-Signature header.

import express from 'express';

app.post('/webhooks/assinafy', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.header('x-assinafy-signature') ?? '';
  const rawBody = req.body as Buffer;

  if (!client.webhookVerifier.verify(rawBody, signature)) {
    return res.status(401).send('Invalid signature');
  }

  const event = client.webhookVerifier.extractEvent(rawBody);
  const type = client.webhookVerifier.getEventType(event);
  const data = client.webhookVerifier.getEventData(event);

  switch (type) {
    case 'document_ready':            break;
    case 'signer_signed_document':    break;
    case 'signer_rejected_document':  break;
    case 'document_processing_failed':break;
  }
  res.sendStatus(200);
});

Signer-side endpoints

For building custom signer portals. Every call requires the signer-access-code URL parameter that Assinafy emails/whatsapps to the signer.

await client.signerDocuments.self(accessCode);
await client.signerDocuments.acceptTerms(accessCode);
await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verificationCode: '123456' });

await client.signerDocuments.getCurrent(signerId, accessCode);
const { data } = await client.signerDocuments.list(signerId, accessCode, { search: 'invoice' });
await client.signerDocuments.download(signerId, documentId, 'original', accessCode);

await client.signerDocuments.confirmData(documentId, accessCode, {
  email: '[email protected]',
  whatsapp_phone_number: '+5548999990000',
  has_accepted_terms: true,
});

// Signature image management
await client.signerDocuments.uploadSignature(accessCode, pngBuffer, { imageType: 'signature' });
await client.signerDocuments.downloadSignature(accessCode, 'signature');

// Sign / decline
const assignment = await client.signerDocuments.getAssignment(accessCode);
await client.signerDocuments.sign(documentId, assignmentId, accessCode, [
  { itemId, fieldId, pageId, value: 'Signed by John' },
]);
await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'Not authorized');

// Bulk operations
await client.signerDocuments.signMultiple(['doc-1', 'doc-2'], accessCode);
await client.signerDocuments.declineMultiple(['doc-1'], 'Unfavorable terms', accessCode);

High-level helper

Uploads a PDF, waits for processing, reuses or creates signers by email, and kicks off a virtual assignment.

const result = await client.uploadAndRequestSignatures({
  source: { filePath: './contract.pdf' },
  signers: [
    { name: 'John', email: '[email protected]' },
    { name: 'Jane', email: '[email protected]', whatsapp_phone_number: '+5548999990000' },
  ],
  message: 'Please sign',
  metadata: { year: 2026 },
  waitForReady: true,
  expiresAt: '2026-12-31T00:00:00Z',
});

result.document;   // IDocumentUploadResponse
result.assignment; // IAssignment
result.signer_ids; // string[]

Errors

Every method rejects with an AssinafyError subclass.

import { ApiError, ValidationError, NetworkError, AssinafyError } from '@assinafy/sdk';

try {
  await client.documents.upload({ filePath: './x.pdf' });
} catch (err) {
  if (err instanceof ValidationError) {
    console.error('Validation failed:', err.errors);
  } else if (err instanceof ApiError) {
    console.error(`API error ${err.statusCode}:`, err.responseData);
  } else if (err instanceof NetworkError) {
    console.error('Network error:', err.message);
  } else if (err instanceof AssinafyError) {
    console.error('SDK error:', err.message, err.context);
  }
}

Live smoke test

A real-network test script under scripts/live-smoke.ts exercises the full API. Use it to sanity-check a workspace before shipping.

ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts            # read-only
ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --write    # also creates+deletes a signer
ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --upload   # also uploads a PDF + a template, then deletes both

Set ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 to run it against the sandbox instead of production.

Development

bun install        # or npm install
bun test           # runs bun:test suites (Bun is required for tests)
npm run typecheck  # tsc --noEmit
npm run lint
npm run build      # tsup → dist/ (CJS + ESM + .d.ts)

License

MIT