@assinafy/sdk 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,11 +2,15 @@
2
2
 
3
3
  TypeScript SDK for the [Assinafy API](https://api.assinafy.com.br/v1/docs) — a Brazilian digital signature platform.
4
4
 
5
- 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.
5
+ Covers the server-side surface of the API: documents, signers, assignments, templates, tags, workspaces, webhooks, field definitions, authentication, public/signer-side flows, and the high-level `uploadAndRequestSignatures` helper.
6
+
7
+ Deliberately not wrapped: the browser-redirect OAuth endpoints (`/auth/authenticate`, `/auth/link-social-login`, `/login-callback`), which a server-side SDK cannot meaningfully drive, and the account `theme`/`logo` branding routes.
6
8
 
7
9
  ## Requirements
8
10
 
9
- - Node.js 18+ for the built-in `FormData` / `Blob` APIs used by uploads
11
+ - Node.js 22+ for the built-in `FormData` / `Blob` APIs used by uploads. Tested
12
+ on 22 (maintenance LTS) and 24 (active LTS); Node 20 reached end-of-life in
13
+ April 2026 and is no longer supported.
10
14
  - or Bun 1.0+
11
15
 
12
16
  ## Installation
@@ -66,11 +70,19 @@ new AssinafyClient({ token: 'jwt_xxx', accountId: 'acc_xxx' });
66
70
  | `apiKey` | string | — | Preferred credential (sent as `X-Api-Key`). |
67
71
  | `token` | string | — | Legacy access token (sent as `Bearer`). |
68
72
  | `accountId` | string | — | Default workspace/account ID. |
69
- | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Override base URL. |
73
+ | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Override base URL (e.g. the sandbox). |
70
74
  | `webhookSecret` | string | — | Shared secret used by `WebhookVerifier`. |
71
75
  | `timeout` | number | `30000` | Request timeout in milliseconds. |
76
+ | `maxRetries` | number | `2` | Auto-retries on HTTP 429, honoring `Retry-After`. `0` disables. |
72
77
  | `logger` | `Logger` | no-op | Optional `{debug,info,warn,error}` logger. |
73
78
 
79
+ ### Rate limiting
80
+
81
+ The API allows ~120 requests/minute and returns `X-Rate-Limit-*` headers. On an
82
+ HTTP `429`, the client automatically retries up to `maxRetries` times, waiting
83
+ for the server-provided `Retry-After` (or `X-Rate-Limit-Reset`) delay before
84
+ each attempt. Only `429` is retried, so non-idempotent calls are safe.
85
+
74
86
  ### Factories
75
87
 
76
88
  ```ts
@@ -90,16 +102,16 @@ Every public endpoint documented in https://api.assinafy.com.br/v1/docs is cover
90
102
 
91
103
  | Resource | Endpoints |
92
104
  | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
- | `client.documents` | list, upload, details, activities, waitUntilReady, download, thumbnail, downloadPage, statuses, delete, verify, createFromTemplate, estimateCostFromTemplate, **getPublic**, **sendToken**, **listTags**, **replaceTags**, **addTags**, **detachTag**, isFullySigned, getSigningProgress |
105
+ | `client.documents` | list, **search**, upload, details, get, **rename**, activities, waitUntilReady, download, thumbnail, downloadPage, statuses, delete, verify, createFromTemplate, estimateCostFromTemplate, getPublic, sendToken, listTags, replaceTags, addTags, detachTag, isFullySigned, getSigningProgress |
94
106
  | `client.signers` | create, get, list, update, delete, findByEmail |
95
- | `client.assignments` | create, estimateCost, resetExpiration, resendNotification, estimateResendCost, listWhatsAppNotifications, cancel |
96
- | `client.templates` | list, get, downloadPage |
107
+ | `client.assignments` | **list**, create, estimateCost, resetExpiration, resendNotification, estimateResendCost, listWhatsAppNotifications |
108
+ | `client.templates` | **create**, list, get, **update**, **delete**, downloadPage |
97
109
  | `client.tags` | list, create, update, delete |
98
110
  | `client.workspaces` | create, list, get, update, delete |
99
- | `client.webhooks` | register, get, inactivate, delete, listEventTypes, listDispatches, retryDispatch |
111
+ | `client.webhooks` | register, get, inactivate, listEventTypes, listDispatches, retryDispatch |
100
112
  | `client.fields` | create, list, get, update, delete, validate, validateMultiple, listTypes |
101
113
  | `client.auth` | login, socialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword |
102
- | `client.signerDocuments` | getCurrent, list, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline |
114
+ | `client.signerDocuments` | getCurrent, list, **search**, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline |
103
115
  | `client.webhookVerifier` | verify, extractEvent, getEventType, getEventData |
104
116
 
105
117
  ## Resources
@@ -112,17 +124,40 @@ Most account-scoped methods accept an optional `accountId` that overrides the cl
112
124
  // Upload from a file path (recommended)
113
125
  const doc = await client.documents.upload(
114
126
  { filePath: './contract.pdf' },
115
- { metadata: { type: 'service' } },
127
+ { name: 'Service agreement', metadata: { type: 'service' } },
116
128
  );
129
+ // `name` is optional and defaults to the file's own name. The API derives the
130
+ // display name from the uploaded filename and appends `.pdf` when absent, so
131
+ // the document above is stored as 'Service agreement.pdf'. Accents are
132
+ // transliterated by the API ('Contrato de Serviço' → 'Contrato de Servico.pdf').
133
+ // → {
134
+ // resource: 'document', id: '1031…', account_id: '102d…', template_id: null,
135
+ // name: 'contract.pdf', status: 'uploaded',
136
+ // artifacts: { original: 'https://…/download/original' },
137
+ // signing_url: 'https://app…/sign/1031…',
138
+ // pages: [], // populated once status reaches `metadata_ready`
139
+ // tags: [], is_closed: false, created_at: '2026-…', updated_at: '2026-…'
140
+ // }
117
141
 
118
142
  // …or from a Buffer already in memory
119
143
  await client.documents.upload({ buffer, fileName: 'contract.pdf' });
120
144
 
145
+ // List → { data: IDocumentListItem[], meta?: { current_page, per_page, total, last_page } }
121
146
  const { data, meta } = await client.documents.list({ page: 1, per_page: 20, sort: '-created_at' });
147
+
148
+ // Search is the lightweight alternative to list: same item shape, but the API
149
+ // skips the expanded `assignment`/`pages`. Prefer it for name lookups.
150
+ const hits = await client.documents.search({ search: 'agreement', status: 'pending_signature', 'per-page': 20 });
151
+
122
152
  await client.documents.details(doc.id);
123
153
  await client.documents.activities(doc.id);
124
154
  await client.documents.waitUntilReady(doc.id, { maxWaitMs: 30_000 });
125
155
 
156
+ // Rename. The API rejects this with 400 while the document is still in
157
+ // `metadata_processing`, so await waitUntilReady() first on a fresh upload.
158
+ // (Passing `name` to upload() avoids both the round-trip and the race.)
159
+ await client.documents.rename(doc.id, 'Signed service agreement.pdf');
160
+
126
161
  await client.documents.download(doc.id, 'certificated'); // 'original' | 'certificated' | 'certificate-page' | 'bundle'
127
162
  await client.documents.thumbnail(doc.id);
128
163
  await client.documents.downloadPage(doc.id, pageId);
@@ -159,6 +194,9 @@ await client.signers.create({
159
194
  whatsapp_phone_number: '+5548999990000',
160
195
  cpf: '123.456.789-00', // optional Brazilian tax ID — non-digits are stripped automatically
161
196
  });
197
+ // → { id: '19e6…', full_name: 'John Doe', email: 'john@example.com',
198
+ // whatsapp_phone_number: '+5548999990000', has_accepted_terms: false }
199
+ // (note: `cpf` is accepted on input but never echoed back by the API)
162
200
 
163
201
  // `email` is optional — a WhatsApp-only signer is valid (at least one is required)
164
202
  await client.signers.create({
@@ -186,6 +224,10 @@ When an `email` is supplied, `signers.create()` is idempotent by email, matching
186
224
  ### Assignments
187
225
 
188
226
  ```ts
227
+ // List every assignment in the workspace.
228
+ // → { data: IAssignment[], meta?: { current_page, per_page, total, last_page } }
229
+ const { data, meta } = await client.assignments.list({ page: 1, 'per-page': 20 });
230
+
189
231
  // Signers may be ids or objects — the SDK normalises to the API shape.
190
232
  await client.assignments.create(documentId, {
191
233
  method: 'virtual',
@@ -204,40 +246,80 @@ await client.assignments.create(documentId, {
204
246
  ],
205
247
  });
206
248
 
207
- // Estimate cost (signers may omit `id` when only the channel matters)
249
+ // Estimate cost (signers may omit `id` when only the channel matters) → ICostEstimate
208
250
  await client.assignments.estimateCost(documentId, { signers: ['signer-1'] });
209
251
  await client.assignments.estimateCost(documentId, {
210
252
  signers: [{ verification_method: 'Whatsapp' }],
211
253
  });
254
+ // → {
255
+ // documents: 1, credits: 0, needs_extra_document: false, extra_document_cost: 0,
256
+ // total_credits: 0, breakdown: [], document_balance: 67, credit_balance: 0,
257
+ // has_sufficient_resources: true, blocking_reason: null, message: null
258
+ // }
212
259
 
213
260
  await client.assignments.resetExpiration(documentId, assignmentId, '2025-06-30T00:00:00Z');
214
261
  await client.assignments.resetExpiration(documentId, assignmentId, null); // remove expiration
262
+
215
263
  await client.assignments.resendNotification(documentId, assignmentId, signerId);
264
+ // → { is_sent: true, document_id: '…', signer_id: '…' }
265
+
216
266
  await client.assignments.estimateResendCost(documentId, assignmentId, signerId);
217
- await client.assignments.listWhatsAppNotifications(documentId, assignmentId);
218
- await client.assignments.cancel(documentId, 'No longer needed');
267
+ // → { total: 0, breakdown: [{ code: 'NotificationEmailResend', name: '…', cost: 0 }],
268
+ // credit_balance: 0, has_sufficient_credits: true }
269
+
270
+ await client.assignments.listWhatsAppNotifications(documentId, assignmentId); // → IWhatsAppNotification[]
219
271
  ```
220
272
 
273
+ The `create` response is an `IAssignment`: `{ id, method, signers: [...], items: [...], signing_urls: [{ signer_id, url }], … }`.
274
+
221
275
  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.
222
276
 
277
+ **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:
278
+
279
+ ```ts
280
+ await client.documents.delete(documentId); // workspace-side
281
+ await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'No longer needed'); // signer-side
282
+ ```
283
+
223
284
  ### Templates
224
285
 
225
286
  ```ts
287
+ // Create a template by uploading a PDF (multipart). The template starts in
288
+ // `Uploaded` status and becomes `Ready` once its pages are processed.
289
+ const created = await client.templates.create(
290
+ { filePath: './nda.pdf' }, // or { buffer, fileName: 'nda.pdf' }
291
+ { name: 'NDA template' },
292
+ );
293
+ // →
294
+ // {
295
+ // resource: 'template', id: '1032...', name: 'nda.pdf',
296
+ // document_name: 'nda.pdf', message: null, status: 'Uploaded',
297
+ // roles: [{ id: '1032...', name: 'TemplateEditor', assignment_type: 'Editor' }],
298
+ // pages: [], tags: [], created_at: '2026-…', updated_at: '2026-…'
299
+ // }
300
+
226
301
  const { data, meta } = await client.templates.list({ search: 'NDA', per_page: 20 });
227
- const template = await client.templates.get(templateId);
228
- await client.templates.downloadPage(templateId, pageId);
302
+ const template = await client.templates.get(created.id); // includes pages[] + default_document_tags
303
+ await client.templates.update(created.id, { name: 'NDA v2', message: 'Please sign' });
304
+ await client.templates.downloadPage(created.id, template.pages![0].id); // → Buffer (JPEG)
305
+ await client.templates.delete(created.id);
229
306
 
230
- // Create a document from a template (each signer maps to a template role)
307
+ // Create a *document* from a template (each signer maps to a template role)
231
308
  await client.documents.createFromTemplate(
232
309
  templateId,
233
310
  [{ role_id: template.roles![0].id, id: signerId, verification_method: 'Email', notification_methods: ['Email'] }],
234
311
  { name: 'NDA - John Doe', message: 'Please sign at your earliest convenience.' },
235
312
  );
236
313
 
237
- // Estimate the cost before creating
314
+ // Estimate the cost before creating → ICostEstimate
238
315
  await client.documents.estimateCostFromTemplate(templateId, [{ role_id: 'role_id', id: signerId }]);
316
+ // → { documents: 1, total_credits: 0, document_balance: 67, credit_balance: 0,
317
+ // has_sufficient_resources: true, blocking_reason: null, breakdown: [], … }
239
318
  ```
240
319
 
320
+ Template creation only uploads the PDF and provisions the default editor role —
321
+ configure roles/fields in the Assinafy editor (or the web UI) afterwards.
322
+
241
323
  ### Tags
242
324
 
243
325
  Workspace-scoped labels that can be attached to documents and templates. Tag names are unique per workspace (case-insensitive).
@@ -326,8 +408,7 @@ await client.webhooks.register({
326
408
  });
327
409
 
328
410
  await client.webhooks.get(); // current subscription or null
329
- await client.webhooks.inactivate();
330
- await client.webhooks.delete();
411
+ await client.webhooks.inactivate(); // stop deliveries (no delete route exists)
331
412
  await client.webhooks.listEventTypes();
332
413
  await client.webhooks.listDispatches({ delivered: false, page: 1, 'per-page': 20 });
333
414
  await client.webhooks.retryDispatch(dispatchId);
@@ -373,6 +454,8 @@ await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verific
373
454
 
374
455
  await client.signerDocuments.getCurrent(signerId, accessCode);
375
456
  const { data } = await client.signerDocuments.list(signerId, accessCode, { search: 'invoice' });
457
+ // Signer-side counterpart of documents.search(), authorised by the access code.
458
+ const found = await client.signerDocuments.search(signerId, accessCode, 'invoice');
376
459
  await client.signerDocuments.download(signerId, documentId, 'original', accessCode);
377
460
 
378
461
  await client.signerDocuments.confirmData(documentId, accessCode, {
@@ -448,9 +531,12 @@ A real-network test script under [`scripts/live-smoke.ts`](scripts/live-smoke.ts
448
531
  ```bash
449
532
  ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts # read-only
450
533
  ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --write # also creates+deletes a signer
451
- ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --upload # also uploads+deletes a PDF
534
+ ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --upload # also uploads a PDF + a template, then deletes both
452
535
  ```
453
536
 
537
+ Set `ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1` to run it against the
538
+ sandbox instead of production.
539
+
454
540
  ## Development
455
541
 
456
542
  ```bash