Skip to content

assinafy/typescript-sdk

Repository files navigation

@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: 'john@example.com' },
    { name: 'Jane Smith',  email: 'jane@example.com', 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, 'jane@example.com', '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: 'john@example.com',
  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: 'john@example.com',
//     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: 'jane@example.com',
  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('john@example.com');

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: 'foo@bar.com' },
  ],
  { 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('me@example.com', '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('me@example.com');
await client.auth.resetPassword({ email, token: 'tk', new_password: 'next' });

Webhooks

await client.webhooks.register({
  url: 'https://example.com/webhooks/assinafy',
  email: 'admin@example.com',
  // 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: 'me@example.com',
  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: 'john@example.com' },
    { name: 'Jane', email: 'jane@example.com', 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

About

Typescript SDK for the Assinafy digital signature API. A digital signature platform for Brazil.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors