@assinafy/sdk 2.1.1 → 2.1.2

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
@@ -6,18 +6,19 @@ Covers all 89 operations in the current official OpenAPI document: accounts,
6
6
  authentication, users, documents, assignments, signers, signer-side flows,
7
7
  templates, tags, fields, webhooks, branding, statistics, and the high-level
8
8
  `uploadAndRequestSignatures` workflow. Five additional template-management
9
- routes exposed by the live API are retained as compatibility extensions.
9
+ routes used by existing integrations and two legacy browser URL helpers are
10
+ retained for compatibility.
10
11
 
11
- See [API coverage](docs/API_COVERAGE.md) for the exhaustive operation map and
12
- [compatibility notes](docs/COMPATIBILITY.md) for the few places where live
13
- behavior differs from the published schema.
12
+ See [API coverage](docs/API_COVERAGE.md) for the operation map and
13
+ [compatibility notes](docs/COMPATIBILITY.md) for deployment-specific request
14
+ and response variants.
14
15
 
15
16
  ## Requirements
16
17
 
17
18
  - Node.js 22+ for the built-in `FormData` / `Blob` APIs used by uploads. Packed
18
19
  CJS and ESM imports are tested on 22 (maintenance LTS), 24 (active LTS), and
19
20
  26 (Current); Node 20 reached end-of-life in April 2026 and is unsupported.
20
- - or Bun 1.3.14 (the version pinned for development and CI)
21
+ - or Bun 1.4.0 (the version pinned for development and CI)
21
22
 
22
23
  ## Installation
23
24
 
@@ -39,23 +40,30 @@ The package is published to both [npmjs.com](https://www.npmjs.com/package/@assi
39
40
  ```ts
40
41
  import { AssinafyClient } from '@assinafy/sdk';
41
42
 
43
+ const baseUrl = process.env.ASSINAFY_BASE_URL ?? 'https://api.assinafy.com.br/v1';
42
44
  const client = new AssinafyClient({
43
45
  apiKey: process.env.ASSINAFY_API_KEY!,
44
46
  accountId: process.env.ASSINAFY_ACCOUNT_ID!,
47
+ baseUrl,
45
48
  });
46
49
 
47
50
  const result = await client.uploadAndRequestSignatures({
48
51
  source: { filePath: './contract.pdf' },
49
52
  signers: [
50
- { name: 'John Doe', email: 'john@example.com' },
51
- { name: 'Jane Smith', email: 'jane@example.com', whatsapp_phone_number: '+5548999990000' },
53
+ { name: 'John Doe', email: 'john@example.com' },
54
+ { name: 'Jane Smith', email: 'jane@example.com' },
52
55
  ],
53
56
  message: 'Please sign this contract',
54
57
  });
55
58
 
56
59
  console.log('Document ID:', result.document.id);
60
+ console.log('Assignment ID:', result.assignment.id);
57
61
  ```
58
62
 
63
+ This path uses email verification and notification for every signer. WhatsApp
64
+ and ICP-Brasil certificate signing have separate prerequisites and costs; see
65
+ [Paid signing branches](#paid-signing-branches) before enabling either one.
66
+
59
67
  ## Authentication
60
68
 
61
69
  The API supports two authentication methods. Prefer `apiKey` — it maps to the `X-Api-Key` header recommended by Assinafy for backend services.
@@ -85,6 +93,11 @@ await publicClient.signerDocuments.self(signerAccessCode);
85
93
  Protected methods still require `apiKey` or `token`; the API returns its normal
86
94
  `401` response if one is called without credentials.
87
95
 
96
+ Every SDK transport, including public and signer-access-code requests, sends
97
+ `User-Agent: Assinafy-Typescript-SDK/v<VERSION>`, where `<VERSION>` is the
98
+ installed package version. The exact value is also exported as
99
+ `SDK_USER_AGENT` for custom transport checks and observability rules.
100
+
88
101
  ## Configuration
89
102
 
90
103
  | Option | Type | Default | Description |
@@ -92,7 +105,7 @@ Protected methods still require `apiKey` or `token`; the API returns its normal
92
105
  | `apiKey` | string | — | Preferred credential (sent as `X-Api-Key`). |
93
106
  | `token` | string | — | Access token (sent as `Authorization: Bearer`). |
94
107
  | `accountId` | string | — | Default workspace/account ID. |
95
- | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Override base URL (e.g. the sandbox). |
108
+ | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Absolute HTTP(S) API base without credentials, query, or fragment. |
96
109
  | `webhookSecret` | string | — | Opt-in HMAC secret used by `WebhookVerifier`; see its [contract caveat](docs/COMPATIBILITY.md#webhook-signature-verification-is-not-in-the-openapi-contract). |
97
110
  | `timeout` | number | `30000` | Request timeout in milliseconds. |
98
111
  | `maxRetries` | number | `2` | Auto-retries eligible HTTP 429 responses, honoring `Retry-After`. `0` disables. |
@@ -102,9 +115,12 @@ Protected methods still require `apiKey` or `token`; the API returns its normal
102
115
 
103
116
  On an HTTP `429`, the client automatically retries up to `maxRetries` times,
104
117
  waiting for the server-provided `Retry-After` (or `X-Rate-Limit-Reset`) delay
105
- before each attempt. Automatic replay is limited to `GET`, `HEAD`, `OPTIONS`,
106
- `PUT`, and `DELETE`. A `POST` or `PATCH` is retried only when the request has a
107
- non-empty `Idempotency-Key` header. No other HTTP status is retried.
118
+ before each attempt. Automatic replay is limited to read-safe `GET`, `HEAD`,
119
+ `OPTIONS`, and `DELETE` requests. `GET /sign` is excluded because it records
120
+ that the signer viewed the assignment. Writes are not replayed by default.
121
+ A non-empty `Idempotency-Key` opts a custom request into SDK replay, but it is
122
+ not part of the current Assinafy OpenAPI contract: confirm that the target
123
+ route deduplicates that key server-side first. No other HTTP status is retried.
108
124
 
109
125
  ### Factories
110
126
 
@@ -122,8 +138,8 @@ const client = AssinafyClient.fromConfig({
122
138
  ## Endpoint coverage
123
139
 
124
140
  All 89 operations documented at https://api.assinafy.com.br/v1/docs are
125
- covered. The table below is the resource-level summary; the auditable,
126
- operation-by-operation ledger is in [docs/API_COVERAGE.md](docs/API_COVERAGE.md).
141
+ covered. The table below is the resource-level summary; the detailed operation
142
+ ledger is in [docs/API_COVERAGE.md](docs/API_COVERAGE.md).
127
143
 
128
144
  | Resource | Endpoints |
129
145
  | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -136,7 +152,7 @@ operation-by-operation ledger is in [docs/API_COVERAGE.md](docs/API_COVERAGE.md)
136
152
  | `client.webhooks` | register, get, inactivate, listEventTypes, listDispatches, retryDispatch |
137
153
  | `client.fields` | create, list, get, update, delete, validate, validateMultiple, listTypes |
138
154
  | `client.auth` | getSocialLoginUrl, getSocialLoginCallbackUrl, login, socialLogin, linkSocialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword |
139
- | `client.users` | getCurrent, getStats |
155
+ | `client.users` | getCurrent, getStats, getNotificationPreferences, updateNotificationPreferences |
140
156
  | `client.signerDocuments` | getCurrent, list, **search**, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline |
141
157
  | `client.webhookVerifier` | verify, extractEvent, getEventType, getEventData |
142
158
 
@@ -148,7 +164,251 @@ inline in the generated declarations. Editors expose the reference on hover,
148
164
  and declaration files ship with the package. The coverage ledger links those
149
165
  typed methods back to each upstream operation without duplicating the schema.
150
166
 
151
- ## Resources
167
+ ## Document lifecycle
168
+
169
+ The normal integration has an account-owner phase, a signer phase, and a final
170
+ artifact phase. The example below keeps every signer on email and uses a
171
+ `virtual` assignment, so no page coordinates or paid notification channel are
172
+ required.
173
+
174
+ ### 1. Upload the PDF
175
+
176
+ ```ts
177
+ const uploaded = await client.documents.upload({ filePath: './contract.pdf' });
178
+ ```
179
+
180
+ The official multipart body contains the `file` part. The SDK also supports a
181
+ display-name override (used as that file part's filename) and an optional JSON
182
+ `metadata` part for deployments that accept it. A successful response is
183
+ `IDocumentUploadResponse`:
184
+
185
+ ```ts
186
+ {
187
+ resource?: string;
188
+ id: string;
189
+ account_id: string;
190
+ template_id: string | null;
191
+ name: string;
192
+ status: DocumentStatus;
193
+ assignment?: IAssignment | null;
194
+ artifacts: {
195
+ original: string;
196
+ certificated?: string;
197
+ 'certificate-page'?: string;
198
+ pades?: string;
199
+ bundle?: string;
200
+ thumbnail?: string;
201
+ };
202
+ signing_url?: string;
203
+ pages: Array<{ id: string; number: number; height: number; width: number; download_url: string }>;
204
+ tags?: Array<{ id: string; name: string; color?: string | null }>;
205
+ created_at: string;
206
+ updated_at: string;
207
+ is_closed: boolean;
208
+ decline_reason: string | null;
209
+ declined_by: ISigner | null;
210
+ }
211
+ ```
212
+
213
+ `DocumentStatus` covers `uploading`, `uploaded`, `metadata_processing`,
214
+ `metadata_ready`, `pending_signature`, `expired`, `certificating`,
215
+ `certificated`, `rejected_by_signer`, `rejected_by_user`, and `failed`.
216
+
217
+ Uploads must be PDFs, at most 25 MB and at most 2,000 pages. The SDK checks the
218
+ extension, size, and `%PDF-` header before sending. A new upload can have an
219
+ empty `pages` array until metadata processing finishes. Wait before creating a
220
+ `collect` assignment because its fields refer to rendered page IDs; a
221
+ `virtual` assignment may be created immediately.
222
+
223
+ ```ts
224
+ const prepared = await client.documents.waitUntilReady(uploaded.id, {
225
+ maxWaitMs: 30_000,
226
+ pollIntervalMs: 2_000,
227
+ });
228
+ ```
229
+
230
+ ### 2. Create or reuse the email signers
231
+
232
+ ```ts
233
+ const signerA = await client.signers.create({
234
+ full_name: 'John Doe',
235
+ email: 'john@example.com',
236
+ });
237
+ const signerB = await client.signers.create({
238
+ full_name: 'Jane Smith',
239
+ email: 'jane@example.com',
240
+ });
241
+ ```
242
+
243
+ The wire body is `{ full_name, email }`. Each response is an `ISigner`:
244
+
245
+ ```ts
246
+ {
247
+ resource?: string;
248
+ id: string;
249
+ full_name: string;
250
+ email: string | null;
251
+ whatsapp_phone_number?: string | null;
252
+ cpf?: string | null; // compatibility type; not echoed by the API
253
+ has_accepted_terms?: boolean;
254
+ has_signature?: boolean; // signer-self response only
255
+ has_initial?: boolean; // signer-self response only
256
+ is_signature_reusable?: boolean; // signer-self response only
257
+ metadata?: Record<string, unknown>;
258
+ }
259
+ ```
260
+
261
+ When an email is present, `signers.create()` first looks up that email in the
262
+ workspace and reuses the matching signer; a name-only or phone-only request
263
+ always creates a new signer.
264
+
265
+ ### 3. Price, then request signatures
266
+
267
+ Cost estimation takes channel descriptors, not signer IDs:
268
+
269
+ ```ts
270
+ const estimate = await client.assignments.estimateCost(uploaded.id, {
271
+ method: 'virtual',
272
+ signers: [{}, {}], // `{}` selects Email for each signer
273
+ });
274
+
275
+ if (!estimate.has_sufficient_resources) {
276
+ throw new Error(estimate.blocking_reason ?? estimate.message ?? 'Insufficient resources');
277
+ }
278
+ ```
279
+
280
+ The response is `ICostEstimate`:
281
+
282
+ ```ts
283
+ {
284
+ documents: number;
285
+ credits: number;
286
+ needs_extra_document: boolean;
287
+ extra_document_cost: number;
288
+ total_credits: number;
289
+ breakdown: Array<{ code: string; name: string; cost: number; quantity?: number; unit_cost?: number }>;
290
+ document_balance: number;
291
+ credit_balance: number;
292
+ has_sufficient_resources: boolean;
293
+ blocking_reason: 'PendingPayment' | 'InsufficientDocuments' | 'InsufficientCredits' | null;
294
+ message: string | null;
295
+ }
296
+ ```
297
+
298
+ Create the email assignment only after accepting that estimate:
299
+
300
+ ```ts
301
+ const assignment = await client.assignments.create(uploaded.id, {
302
+ method: 'virtual',
303
+ signers: [
304
+ { id: signerA.id, verification_method: 'Email', notification_methods: ['Email'] },
305
+ { id: signerB.id, verification_method: 'Email', notification_methods: ['Email'] },
306
+ ],
307
+ message: 'Please review and sign',
308
+ expires_at: '2027-12-31T23:59:00Z',
309
+ });
310
+ ```
311
+
312
+ The request returns an `IAssignment`:
313
+
314
+ ```ts
315
+ {
316
+ resource?: string;
317
+ id: string;
318
+ sender_email?: string;
319
+ method: 'virtual' | 'collect';
320
+ expires_at?: string | null;
321
+ expiration?: string;
322
+ message?: string | null;
323
+ signers: IAssignmentSigner[];
324
+ copy_receivers?: Array<Record<string, unknown>>;
325
+ items?: IAssignmentItem[];
326
+ summary?: {
327
+ signer_count: number;
328
+ completed_count: number;
329
+ signers: Array<ISigner & { completed?: boolean }>;
330
+ };
331
+ signing_urls?: Array<{ signer_id: string; url: string }>;
332
+ }
333
+ ```
334
+
335
+ The URLs and delivered messages contain signer credentials; treat them as
336
+ secrets.
337
+
338
+ ### 4. Complete the email signer flow
339
+
340
+ Assinafy sends each signer a link containing their access code and sends the
341
+ one-time verification code through the selected channel. Neither value is
342
+ returned as a standalone owner-side API field. A custom signer portal must
343
+ obtain both values from the signer-delivery flow; do not manufacture them or
344
+ log them.
345
+
346
+ ```ts
347
+ // Signer-side client: no account API credential is needed or sent.
348
+ const signerClient = new AssinafyClient({
349
+ baseUrl,
350
+ });
351
+
352
+ const self = await signerClient.signerDocuments.self(accessCode); // ISignerSelf
353
+
354
+ // Query: signer-access-code=<accessCode>
355
+ // Body: { 'verification-code': '<six-digit code>' }
356
+ await signerClient.signerDocuments.verifyEmail({
357
+ signerAccessCode: accessCode,
358
+ verificationCode,
359
+ }); // Promise<void>
360
+
361
+ const confirmed = await signerClient.signerDocuments.confirmData(
362
+ uploaded.id,
363
+ accessCode,
364
+ { full_name: self.full_name, email: self.email ?? undefined },
365
+ ); // ISigner
366
+
367
+ const signable = await signerClient.signerDocuments.getAssignment(accessCode, true);
368
+ // `getAssignment` returns IDocumentDetailsResponse and records that the signer
369
+ // viewed the assignment. Do not issue it merely as a health check.
370
+
371
+ await signerClient.signerDocuments.signMultiple([signable.id], accessCode);
372
+ // Wire body: { document_ids: [signable.id] }; acknowledgement has no data.
373
+ ```
374
+
375
+ Repeat this phase separately for each signer with that signer's own access code
376
+ and one-time code. The two signers in this example share the default step and
377
+ can sign in parallel.
378
+
379
+ `signMultiple` is only for `virtual` assignments. For `collect`, read
380
+ `signable.assignment.items`, then call `sign(documentId, assignmentId,
381
+ accessCode, entries)` with a non-empty array of
382
+ `{ itemId, fieldId, pageId, value }`. A virtual signer must confirm their data
383
+ before signing. A `DigitalCertificate` signer cannot call `sign`; that branch
384
+ uses Assinafy's certificate-start and certificate-complete flow, which is not
385
+ part of this SDK's current 89-operation surface.
386
+
387
+ ### 5. Observe completion and download artifacts
388
+
389
+ Subscribe to `document_ready` for event-driven completion, or fetch
390
+ `documents.details(documentId)` until `status === 'certificated'`. Webhook
391
+ deliveries can repeat, so use their numeric `id` as an
392
+ idempotency key. Once complete:
393
+
394
+ ```ts
395
+ const finalDocument = await client.documents.details(uploaded.id);
396
+ const signedPdf = await client.documents.download(uploaded.id, 'certificated');
397
+ const certificatePage = await client.documents.download(uploaded.id, 'certificate-page');
398
+ const bundleZip = await client.documents.download(uploaded.id, 'bundle');
399
+
400
+ // Validate an Assinafy signature hash when your workflow has extracted it.
401
+ const validation = await client.documents.verify(documentSignatureHash);
402
+ ```
403
+
404
+ `original`, `certificated`, and `certificate-page` are PDFs. `bundle` is a ZIP
405
+ containing those three artifacts and also `pades` when the document had an
406
+ ICP-Brasil certificate signer. The `pades` PDF exists only for documents that
407
+ had certificate signers. An artifact can return `404` before generation has
408
+ finished. `decline_reason` is included in document details only when the access
409
+ token belongs to the document creator.
410
+
411
+ ## Resource reference
152
412
 
153
413
  Most account-scoped methods accept an optional `accountId` that overrides the
154
414
  client default. Workspace `get`, `update`, `delete`, branding, and statistics
@@ -162,13 +422,15 @@ const doc = await client.documents.upload(
162
422
  { filePath: './contract.pdf' },
163
423
  { name: 'Service agreement', metadata: { type: 'service' } },
164
424
  );
425
+ // `name` and `metadata` are compatibility multipart parts outside the published
426
+ // file-only request schema.
165
427
  // `name` is optional and defaults to the file's own name. The API derives the
166
428
  // display name from the uploaded filename and appends `.pdf` when absent, so
167
429
  // the document above is stored as 'Service agreement.pdf'. Accents are
168
430
  // transliterated by the API ('Contrato de Serviço' → 'Contrato de Servico.pdf').
169
431
  // → {
170
432
  // resource: 'document', id: '1031…', account_id: '102d…', template_id: null,
171
- // name: 'contract.pdf', status: 'uploaded',
433
+ // name: 'Service agreement.pdf', status: 'uploaded',
172
434
  // artifacts: { original: 'https://…/download/original' },
173
435
  // signing_url: 'https://app…/sign/1031…',
174
436
  // pages: [], // populated once status reaches `metadata_ready`
@@ -179,7 +441,7 @@ const doc = await client.documents.upload(
179
441
  await client.documents.upload({ buffer, fileName: 'contract.pdf' });
180
442
 
181
443
  // List → { data: IDocumentListItem[], meta?: { current_page, per_page, total, last_page } }
182
- const { data, meta } = await client.documents.list({ page: 1, per_page: 20, sort: '-created_at' });
444
+ const { data, meta } = await client.documents.list({ page: 1, per_page: 20, sort: 'updated_at' });
183
445
 
184
446
  // Search is the lightweight alternative to list: same item shape, but the API
185
447
  // skips the expanded `assignment`/`pages`. Prefer it for name lookups.
@@ -194,7 +456,11 @@ await client.documents.waitUntilReady(doc.id, { maxWaitMs: 30_000 });
194
456
  // (Passing `name` to upload() avoids both the round-trip and the race.)
195
457
  await client.documents.rename(doc.id, 'Signed service agreement.pdf');
196
458
 
197
- await client.documents.download(doc.id, 'certificated'); // 'original' | 'certificated' | 'certificate-page' | 'bundle'
459
+ await client.documents.download(doc.id, 'certificated'); // signed PDF
460
+ await client.documents.download(doc.id, 'certificate-page');
461
+ await client.documents.download(doc.id, 'bundle'); // ZIP
462
+ // `pades` exists only when at least one signer used DigitalCertificate.
463
+ await client.documents.download(doc.id, 'pades');
198
464
  await client.documents.thumbnail(doc.id);
199
465
  await client.documents.downloadPage(doc.id, pageId);
200
466
 
@@ -222,11 +488,18 @@ const urgentTag = await client.tags.create({ name: 'Urgent' });
222
488
  await client.documents.listTags(doc.id);
223
489
  await client.documents.replaceTags(doc.id, [contractsTag.id, quarterTag.id]); // [] detaches all
224
490
  await client.documents.addTags(doc.id, [urgentTag.id]); // append
225
- await client.documents.detachTag(doc.id, urgentTag.id); // remove one
491
+ await client.documents.detachTag(doc.id, urgentTag.id); // { detached: true }
226
492
  ```
227
493
 
228
494
  Uploads are validated locally: only `.pdf` files up to 25 MB whose bytes begin
229
- with the PDF magic header (`%PDF-`) are accepted (the API's current hard limit).
495
+ with the PDF magic header (`%PDF-`) are accepted. The API also limits documents
496
+ to 2,000 pages.
497
+
498
+ Page and artifact URLs embedded in JSON responses still require the same
499
+ account authentication as their download operations. Prefer
500
+ `documents.downloadPage()` and `documents.download()` so the SDK applies the
501
+ credential and returns a `Buffer`. `bundle` contains `original`, `certificated`,
502
+ and `certificate-page`, plus `pades` when available.
230
503
 
231
504
  List endpoints return `{ data, meta }` where `meta` is populated from the `X-Pagination-*` headers returned by the API.
232
505
 
@@ -236,38 +509,36 @@ List endpoints return `{ data, meta }` where `meta` is populated from the `X-Pag
236
509
  await client.signers.create({
237
510
  full_name: 'John Doe',
238
511
  email: 'john@example.com',
239
- whatsapp_phone_number: '+5548999990000',
240
- cpf: '123.456.789-00', // optional Brazilian tax ID — non-digits are stripped automatically
512
+ cpf: '123.456.789-00', // legacy compatibility input; non-digits are stripped
241
513
  });
242
514
  // → { id: '19e6…', full_name: 'John Doe', email: 'john@example.com',
243
- // whatsapp_phone_number: '+5548999990000', has_accepted_terms: false }
515
+ // whatsapp_phone_number: null, has_accepted_terms: false }
244
516
  // (note: `cpf` is accepted on input but never echoed back by the API)
245
517
 
246
- // Both contacts are optional in the create endpoint. A WhatsApp-only signer:
247
- await client.signers.create({
248
- full_name: 'WhatsApp Only',
249
- whatsapp_phone_number: '+5548999990000',
250
- });
251
-
252
- // Name-only is valid too, but cannot be notified until a contact is added.
518
+ // Both contacts are optional. A name-only signer cannot be notified until a
519
+ // contact is added.
253
520
  await client.signers.create({ full_name: 'Contact Pending' });
254
521
 
255
- // PHP SDK compatibility aliases are also accepted
256
522
  await client.signers.create({
257
523
  full_name: 'Jane Doe',
258
524
  email: 'jane@example.com',
259
- phone: '+5548999991111', // alias for whatsapp_phone_number
260
525
  });
261
526
 
262
527
  await client.signers.get(signerId);
263
528
  await client.signers.list({ page: 1, per_page: 50, search: 'john' });
264
- await client.signers.update(signerId, { full_name: 'Johnny Doe' });
529
+ await client.signers.update(signerId, {
530
+ full_name: 'Johnny Doe',
531
+ government_id: '390.533.447-05', // official update field; sent as digits
532
+ });
265
533
  await client.signers.delete(signerId);
266
534
 
267
535
  const existing = await client.signers.findByEmail('john@example.com');
268
536
  ```
269
537
 
270
- 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.
538
+ When an `email` is supplied, `signers.create()` is idempotent by email: it
539
+ reuses an existing signer when the same email is already present in the
540
+ workspace. Signers without email are always created fresh. See
541
+ [Paid signing branches](#paid-signing-branches) for phone-only signers.
271
542
 
272
543
  ### Assignments
273
544
 
@@ -281,8 +552,8 @@ await client.assignments.create(documentId, {
281
552
  method: 'virtual',
282
553
  signers: ['signer-1', 'signer-2'],
283
554
  message: 'Please review and sign',
284
- expires_at: '2024-12-31T23:59:00Z',
285
- copy_receivers: ['observer-id'],
555
+ expires_at: '2027-12-31T23:59:00Z',
556
+ copy_receivers: ['copy-recipient-signer-id'],
286
557
  });
287
558
 
288
559
  // Sequential signing: `step` controls signing order (parallel within a step).
@@ -294,31 +565,47 @@ await client.assignments.create(documentId, {
294
565
  ],
295
566
  });
296
567
 
568
+ // Collect fields use 150-DPI page-image pixels measured from the upper-left.
569
+ await client.assignments.create(documentId, {
570
+ method: 'collect',
571
+ signers: [{ id: signerId }],
572
+ entries: [{
573
+ page_id: pageId,
574
+ fields: [{
575
+ signer_id: signerId,
576
+ field_id: fieldId,
577
+ display_settings: {
578
+ left: 69, top: 282, width: 421, height: 45.86, fontSize: 22,
579
+ fontFamily: 'Arial', backgroundColor: '#D5EBFF',
580
+ },
581
+ }],
582
+ }],
583
+ });
584
+
297
585
  // Estimate cost (the endpoint prices channel descriptors, not signer IDs) → ICostEstimate
298
586
  await client.assignments.estimateCost(documentId, { signers: [{}] }); // default Email
299
- await client.assignments.estimateCost(documentId, {
300
- signers: [{ verification_method: 'Whatsapp' }],
301
- });
302
587
  // → {
303
588
  // documents: 1, credits: 0, needs_extra_document: false, extra_document_cost: 0,
304
589
  // total_credits: 0, breakdown: [], document_balance: 67, credit_balance: 0,
305
590
  // has_sufficient_resources: true, blocking_reason: null, message: null
306
591
  // }
307
592
 
308
- await client.assignments.resetExpiration(documentId, assignmentId, '2025-06-30T00:00:00Z');
309
- await client.assignments.resetExpiration(documentId, assignmentId, null); // remove expiration
593
+ await client.assignments.resetExpiration(documentId, assignmentId, '2027-06-30T00:00:00Z');
594
+ // Compatibility only: the published request requires a date-time string.
595
+ // Confirm target support before using `null` to clear an expiration.
596
+ await client.assignments.resetExpiration(documentId, assignmentId, null);
310
597
 
311
598
  await client.assignments.resendNotification(documentId, assignmentId, signerId);
312
599
  // → { is_sent: true, document_id: '…', signer_id: '…' }
313
600
 
314
- await client.assignments.estimateResendCost(documentId, assignmentId, signerId);
315
- // { total: 0, breakdown: [{ code: 'NotificationEmailResend', name: '…', cost: 0 }],
316
- // credit_balance: 0, has_sufficient_credits: true }
317
-
318
- await client.assignments.listWhatsAppNotifications(documentId, assignmentId); // → IWhatsAppNotification[]
601
+ const resendCost = await client.assignments.estimateResendCost(documentId, assignmentId, signerId);
602
+ // Official response: ICostEstimate. Older deployments can return the compact
603
+ // IResendCostEstimate branch with `total` and `has_sufficient_credits`; narrow
604
+ // with `'total_credits' in resendCost` before reading branch-specific fields.
319
605
  ```
320
606
 
321
- The `create` response is an `IAssignment`: `{ id, method, signers: [...], items: [...], signing_urls: [{ signer_id, url }], … }`.
607
+ The `create` response is an `IAssignment`: `{ id, method, signers: [...],
608
+ items: [{ display_settings, ... }], signing_urls: [{ signer_id, url }], … }`.
322
609
 
323
610
  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.
324
611
 
@@ -329,15 +616,92 @@ await client.documents.delete(documentId); // w
329
616
  await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'No longer needed'); // signer-side
330
617
  ```
331
618
 
619
+ ### Paid signing branches
620
+
621
+ Keep the email flow as the default. Enable either branch below only after the
622
+ workspace has the required plan or feature and the returned cost estimate is
623
+ acceptable.
624
+
625
+ #### WhatsApp verification and notification
626
+
627
+ WhatsApp is available only on paid subscriptions and costs 0.45 credit per
628
+ notification. Create a phone-only signer or add a phone to an existing signer,
629
+ then request the `Whatsapp` channel explicitly:
630
+
631
+ ```ts
632
+ const phoneSigner = await client.signers.create({
633
+ full_name: 'Mobile Signer',
634
+ whatsapp_phone_number: '+5511999990000',
635
+ });
636
+
637
+ const whatsappCost = await client.assignments.estimateCost(documentId, {
638
+ method: 'virtual',
639
+ signers: [{ verification_method: 'Whatsapp', notification_methods: ['Whatsapp'] }],
640
+ });
641
+
642
+ const whatsappAssignment = await client.assignments.create(documentId, {
643
+ method: 'virtual',
644
+ signers: [{
645
+ id: phoneSigner.id,
646
+ verification_method: 'Whatsapp',
647
+ notification_methods: ['Whatsapp'],
648
+ }],
649
+ });
650
+
651
+ const notices = await client.assignments.listWhatsAppNotifications(
652
+ documentId,
653
+ whatsappAssignment.id,
654
+ );
655
+ // IWhatsAppNotification[]:
656
+ // [{ sent_at, header, body, buttons: [{ text, url? }], phone_number, signer_id }]
657
+ ```
658
+
659
+ The high-level helper selects this paid branch for a signer that has a phone
660
+ number but no email. Button URLs can contain signer credentials; do not log or
661
+ forward them outside the signing flow.
662
+
663
+ #### ICP-Brasil digital certificate
664
+
665
+ `DigitalCertificate` requires the account feature, a CPF or CNPJ in the
666
+ signer's `government_id`, and exactly one certificate signer in that signing
667
+ step. It costs two credits per certificate signer in addition to the selected
668
+ notification cost.
669
+
670
+ ```ts
671
+ const certificateSigner = await client.signers.update(signerId, {
672
+ government_id: '390.533.447-05',
673
+ });
674
+
675
+ const certificateCost = await client.assignments.estimateCost(documentId, {
676
+ method: 'virtual',
677
+ signers: [{ verification_method: 'DigitalCertificate', notification_methods: ['Email'] }],
678
+ });
679
+
680
+ await client.assignments.create(documentId, {
681
+ method: 'virtual',
682
+ signers: [{
683
+ id: certificateSigner.id,
684
+ step: 1,
685
+ verification_method: 'DigitalCertificate',
686
+ notification_methods: ['Email'],
687
+ }],
688
+ });
689
+ ```
690
+
691
+ Before opening the assignment, the signer must confirm identity data and accept
692
+ terms with `confirmData(..., { has_accepted_terms: true })` or `acceptTerms()`.
693
+ The regular `sign()` endpoint rejects certificate signers; they complete the
694
+ ICP-Brasil flow through Assinafy's browser integration. After completion,
695
+ `documents.download(documentId, 'pades')` returns the qualified PAdES artifact.
696
+
332
697
  ### Templates
333
698
 
334
- `templates.list()` is part of the current OpenAPI document. The live API also
335
- exposes five template-management routes—`create`, `get`, `update`, `delete`,
336
- and `downloadPage`—that are retained as tested compatibility extensions even
337
- though they are absent from that document. See
338
- [compatibility notes](docs/COMPATIBILITY.md#template-management-live-extensions).
339
- Template status casing has varied between published examples and live
340
- responses; compare `template.status.toLowerCase()` when branching on it.
699
+ `templates.list()` is part of the current OpenAPI document. Existing
700
+ integrations can also use five template-management routes—`create`, `get`,
701
+ `update`, `delete`, and `downloadPage`—that are absent from that document. See
702
+ [compatibility notes](docs/COMPATIBILITY.md#template-management-extensions).
703
+ Template status casing can vary by deployment; normalize with
704
+ `template.status.toLowerCase()` when branching on it.
341
705
 
342
706
  ```ts
343
707
  // Create a template by uploading a PDF (multipart). The template starts in
@@ -383,8 +747,14 @@ await client.documents.estimateCostFromTemplate(templateId, [
383
747
  // has_sufficient_resources: true, blocking_reason: null, breakdown: [], … }
384
748
  ```
385
749
 
750
+ Template signer descriptors also accept
751
+ `verification_method: 'DigitalCertificate'` with the same prerequisites under
752
+ [ICP-Brasil digital certificate](#icp-brasil-digital-certificate).
753
+
386
754
  Template creation only uploads the PDF and provisions the default editor role —
387
755
  configure roles/fields in the Assinafy editor (or the web UI) afterwards.
756
+ The `download_url` values in template page objects are protected URLs; prefer
757
+ `templates.downloadPage()` so the API credential is attached.
388
758
 
389
759
  ### Tags
390
760
 
@@ -444,7 +814,8 @@ await client.workspaces.getStats(accountId, {
444
814
  });
445
815
 
446
816
  await client.workspaces.delete(accountId);
447
- // If the API reports deletion restrictions, explicitly opt in to overriding them:
817
+ // `force` cancels an active paid subscription as part of account deletion. It
818
+ // is not a general bypass for unrelated deletion restrictions.
448
819
  await client.workspaces.delete(restrictedAccountId, { force: true });
449
820
  ```
450
821
 
@@ -466,7 +837,7 @@ await client.fields.validate(fieldId, '400.676.228-36', { signerAccessCode });
466
837
  await client.fields.validateMultiple(
467
838
  [
468
839
  { field_id: 'f1', value: '1111111111111' },
469
- { field_id: 'f2', value: 'foo@bar.com' },
840
+ { field_id: 'f2', value: 'value@example.com' },
470
841
  ],
471
842
  { signerAccessCode },
472
843
  );
@@ -513,8 +884,17 @@ const daily = await client.users.getStats({
513
884
  granularity: 'daily',
514
885
  month: '2026-06',
515
886
  });
516
- // Each row includes period, documents_uploaded, documents_sent,
517
- // signature_requests by channel/view/completion, and documents_certified.
887
+ // Each row includes period, upload/send/certification totals, notification
888
+ // counts for email/WhatsApp/bypass, verification counts for
889
+ // email/WhatsApp/bypass/digital-certificate, viewed, and completed counts.
890
+
891
+ const preferences = await client.users.getNotificationPreferences();
892
+ await client.users.updateNotificationPreferences({
893
+ SignerDeclined: false,
894
+ DocumentExpired: false,
895
+ });
896
+ // Updates merge: omitted keys keep their current value. Both methods return
897
+ // the complete nine-key notification preference map.
518
898
  ```
519
899
 
520
900
  ### Webhooks
@@ -523,6 +903,7 @@ const daily = await client.users.getStats({
523
903
  await client.webhooks.register({
524
904
  url: 'https://example.com/webhooks/assinafy',
525
905
  email: 'admin@example.com',
906
+ is_active: true,
526
907
  // events defaults to the current SDK default set below
527
908
  events: [
528
909
  'document_ready',
@@ -533,13 +914,91 @@ await client.webhooks.register({
533
914
  ],
534
915
  });
535
916
 
536
- await client.webhooks.get(); // current subscription or null
917
+ await client.webhooks.get(); // IWebhookSubscription | null
537
918
  await client.webhooks.inactivate(); // stop deliveries (no delete route exists)
538
919
  await client.webhooks.listEventTypes();
539
- await client.webhooks.listDispatches({ delivered: false, page: 1, 'per-page': 20 });
540
- await client.webhooks.retryDispatch(dispatchId);
920
+ const history = await client.webhooks.listDispatches({
921
+ delivered: false,
922
+ page: 1,
923
+ 'per-page': 20,
924
+ }); // { data: IWebhookDispatch[], meta?: PaginationMeta }
925
+ const retried = await client.webhooks.retryDispatch(dispatchId); // IWebhookDispatch
541
926
  ```
542
927
 
928
+ `register` sends `{ events, is_active, url, email }` and returns
929
+ `{ events, is_active, url, email, updated_at? }`. Assinafy delivers each event
930
+ as an HTTP `POST` with `Content-Type: application/json` and `Connection: close`.
931
+ Any `2xx` is success. There are at most two automatic attempts, separated by
932
+ three seconds. After ten consecutive failed events, ordinary delivery pauses
933
+ and about 5% of later events are attempted until one succeeds; use
934
+ `retryDispatch()` for an immediate manual redelivery. The dispatch history
935
+ retains only the first 2,000 characters of the receiver's response body.
936
+
937
+ Each history or retry result is an `IWebhookDispatch`:
938
+
939
+ ```ts
940
+ {
941
+ resource?: string;
942
+ id: string;
943
+ event: string;
944
+ activity_id: number;
945
+ endpoint: string | null;
946
+ payload: IWebhookPayload | Record<string, unknown> | null;
947
+ delivered: boolean;
948
+ http_status: number | null;
949
+ response_body: string | null;
950
+ error: string | null;
951
+ created_at: string;
952
+ updated_at?: string;
953
+ }
954
+ ```
955
+
956
+ Every delivery body uses this envelope:
957
+
958
+ ```ts
959
+ {
960
+ id: number; // use for idempotent processing
961
+ event: string;
962
+ message: string | null;
963
+ payload: Record<string, unknown> | null;
964
+ origin: { ip?: string; 'user-agent'?: string } | null;
965
+ created_at: number; // Unix seconds
966
+ subject: { type: 'User' | 'Signer' | 'Account' | 'Document' | 'Template'; [key: string]: unknown };
967
+ object: { type: 'User' | 'Signer' | 'Account' | 'Document' | 'Template'; [key: string]: unknown };
968
+ account_id: string;
969
+ }
970
+ ```
971
+
972
+ Event-specific values are:
973
+
974
+ | `event` | `subject.type` | `object.type` | `payload` keys |
975
+ | --- | --- | --- | --- |
976
+ | `document_uploaded` | `User` | `Document` | — |
977
+ | `document_metadata_ready` | `User` | `Document` | — |
978
+ | `document_prepared` | `User` | `Document` | — |
979
+ | `assignment_created` | `User` | `Document` | `user_name`, `user_email`, `user_telephone` |
980
+ | `document_ready` | `Account` | `Document` | — |
981
+ | `document_processing_failed` | `Account` | `Document` | `error_message` |
982
+ | `signature_requested` | `User` | `Document` | `signer_email`, `signer_full_name`, or `signer_whatsapp_phone_number`, according to channel |
983
+ | `signer_created` | `User` | `Signer` | `signer_full_name` |
984
+ | `signer_email_verified` | `Signer` | `Document` | `signer_email` |
985
+ | `signer_whatsapp_verified` | `Signer` | `Document` | `signer_whatsapp_phone_number` |
986
+ | `signer_data_confirmed` | `Signer` | `Document` | `signer_email` |
987
+ | `signer_viewed_document` | `Signer` | `Document` | `signer_full_name` |
988
+ | `signer_signed_document` | `Signer` | `Document` | `signer_full_name` |
989
+ | `signer_rejected_document` | `Signer` | `Document` | `signer_full_name` |
990
+ | `user_rejected_document` | `User` | `Document` | `user_name` |
991
+ | `template_created` | `User` | `Template` | — |
992
+ | `template_processed` | `User` | `Template` | — |
993
+ | `template_processing_failed` | `Account` | `Template` | `error_message` |
994
+
995
+ `payload`, `subject`, and `object` are event-dependent. Accept unknown fields
996
+ for forward compatibility and acknowledge only after durable, idempotent
997
+ processing. Non-`2xx` responses, timeouts, and connection failures all count as
998
+ failed deliveries. `assignment_created` and `document_metadata_ready` have no
999
+ guaranteed ordering. For account entities, Assinafy removes the `integration`
1000
+ property before delivery.
1001
+
543
1002
  ### Webhook verification
544
1003
 
545
1004
  `WebhookVerifier` is an opt-in HMAC-SHA256 utility for integrations whose
@@ -591,7 +1050,6 @@ only for compatibility with deployments that still expect the legacy query.
591
1050
 
592
1051
  ```ts
593
1052
  await client.signerDocuments.self(accessCode);
594
- await client.signerDocuments.acceptTerms(accessCode);
595
1053
  await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verificationCode: '123456' });
596
1054
 
597
1055
  await client.signerDocuments.getCurrent(signerId, accessCode);
@@ -599,45 +1057,57 @@ const { data } = await client.signerDocuments.list(signerId, accessCode, { per_p
599
1057
  // Signer-side counterpart of documents.search(), authorised by the access code.
600
1058
  const found = await client.signerDocuments.search(signerId, accessCode, 'invoice');
601
1059
  await client.signerDocuments.download(signerId, documentId, 'original');
1060
+ // Available only after an ICP-Brasil certificate signer completes signing.
1061
+ await client.signerDocuments.download(signerId, documentId, 'pades');
602
1062
 
603
1063
  await client.signerDocuments.confirmData(documentId, accessCode, {
604
1064
  email: 'me@example.com',
605
1065
  full_name: 'Example Signer',
606
1066
  government_id: '123.456.789-00',
1067
+ has_accepted_terms: true,
607
1068
  });
608
- await client.signerDocuments.acceptTerms(accessCode);
1069
+ // Alternatively, accept terms separately before getAssignment():
1070
+ // await client.signerDocuments.acceptTerms(accessCode);
609
1071
 
610
1072
  // Signature image management ({ reuse: true } persists it for future documents)
611
1073
  await client.signerDocuments.uploadSignature(accessCode, pngBuffer, { imageType: 'signature', reuse: true });
612
1074
  await client.signerDocuments.downloadSignature(accessCode, 'signature');
613
1075
 
614
1076
  // Sign / decline
615
- const assignment = await client.signerDocuments.getAssignment(accessCode);
1077
+ const signable = await client.signerDocuments.getAssignment(accessCode);
1078
+ // `sign()` is for collect assignments and requires every placed field value.
616
1079
  await client.signerDocuments.sign(documentId, assignmentId, accessCode, [
617
1080
  { itemId, fieldId, pageId, value: 'Signed by John' },
618
1081
  ]);
619
1082
  await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'Not authorized');
620
1083
 
621
- // Bulk operations
1084
+ // `signMultiple()` is for virtual assignments only.
622
1085
  await client.signerDocuments.signMultiple(['doc-1', 'doc-2'], accessCode);
623
1086
  await client.signerDocuments.declineMultiple(['doc-1'], 'Unfavorable terms', accessCode);
624
1087
  ```
625
1088
 
1089
+ `sign()` also requires virtual signers to have confirmed their data first, but
1090
+ virtual assignments should normally use `signMultiple()`. Certificate signers
1091
+ cannot use `sign()`; see [Paid signing branches](#paid-signing-branches).
1092
+
626
1093
  ## High-level helper
627
1094
 
628
- Uploads a PDF, waits for processing, reuses or creates signers by email, and kicks off a virtual assignment.
1095
+ Uploads a PDF, reuses or creates signers by email, creates a virtual assignment
1096
+ immediately, and optionally waits for processing before returning.
629
1097
 
630
1098
  ```ts
631
1099
  const result = await client.uploadAndRequestSignatures({
632
1100
  source: { filePath: './contract.pdf' },
633
1101
  signers: [
634
1102
  { name: 'John', email: 'john@example.com' },
635
- { name: 'Jane', email: 'jane@example.com', whatsapp_phone_number: '+5548999990000' },
1103
+ { name: 'Jane', email: 'jane@example.com' },
636
1104
  ],
637
1105
  message: 'Please sign',
638
- metadata: { year: 2026 },
1106
+ metadata: { year: 2026 }, // compatibility upload part; omit for file-only wire format
639
1107
  waitForReady: true,
640
- expiresAt: '2026-12-31T00:00:00Z',
1108
+ waitOptions: { maxWaitMs: 30_000, pollIntervalMs: 1_000 },
1109
+ expiresAt: '2027-12-31T00:00:00Z',
1110
+ copyReceivers: ['existing-copy-recipient-signer-id'],
641
1111
  });
642
1112
 
643
1113
  result.document; // fully-processed IDocumentDetailsResponse (waitForReady: true, the default);
@@ -646,10 +1116,20 @@ result.assignment; // IAssignment
646
1116
  result.signer_ids; // string[]
647
1117
  ```
648
1118
 
649
- `waitForReady: false` skips only the final post-assignment document re-fetch.
650
- The helper always waits for the uploaded document to reach a metadata-ready
651
- state before creating its assignment; that safety wait is required to avoid a
652
- processing race.
1119
+ `waitForReady: false` skips post-assignment polling and returns the initial
1120
+ upload response. With the default `true`, the helper creates the assignment first and
1121
+ then waits for the current document details. Both production and sandbox allow
1122
+ virtual assignments in `uploaded` and `metadata_processing` and promote them
1123
+ automatically; only `collect` assignments require rendered pages.
1124
+ Every signer above uses the default email channel. A phone-only signer selects
1125
+ the paid WhatsApp branch described earlier. `copyReceivers` accepts existing
1126
+ signer IDs, not email addresses; check the returned assignment before treating
1127
+ a copy receiver as registered.
1128
+
1129
+ The helper is not transactional. A post-assignment polling error includes the
1130
+ created `documentId`, `assignmentId`, and `signerIds` in its `context` (and in
1131
+ `ValidationError.errors` for timeouts); inspect those IDs before deciding
1132
+ whether to retry the workflow.
653
1133
 
654
1134
  ## Errors
655
1135
 
@@ -674,47 +1154,10 @@ try {
674
1154
  }
675
1155
  ```
676
1156
 
677
- ## Live smoke test
678
-
679
- A redaction-safe real-network audit under
680
- [`scripts/live-smoke.ts`](scripts/live-smoke.ts) exercises read-only operations
681
- by default. Its `--all` mode creates an isolated sandbox workspace, audits
682
- reversible CRUD, upload, assignment, branding, statistics, and template flows,
683
- then attempts per-resource cleanup and force-deletes that workspace in `finally`.
684
- It complements the unit/contract suites. Optional login credentials enable the
685
- password-login probe; an optional signer access code/OTP enables read-only
686
- signer-link and OTP probes; and an optional HTTPS webhook receiver enables the
687
- subscription lifecycle. Password changes, API-key rotation, social-provider
688
- login/linking, legal consent, identity/signature mutation, signing, and decline
689
- flows are deliberately never automated by this harness and are always reported
690
- as explicit `SKIP`s. They require credentials, authorization, or legal fixtures
691
- that an API key/account/e-mail set cannot safely supply.
692
-
693
- ```bash
694
- # Read-only account/API checks. The base URL is deliberately mandatory.
695
- ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
696
- ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… \
697
- bun scripts/live-smoke.ts
698
-
699
- # Full reversible audit in a disposable sandbox workspace.
700
- ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
701
- ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… \
702
- ASSINAFY_TEST_EMAIL_PRIMARY=first@example.com \
703
- ASSINAFY_TEST_EMAIL_SECONDARY=second@example.com \
704
- bun scripts/live-smoke.ts --all
705
- ```
706
-
707
- Mutation is refused unless the URL's exact host is the Assinafy sandbox. The
708
- explicit `--confirm-production` escape hatch exists for controlled environments
709
- but should not be used for routine SDK verification. The script never prints
710
- credentials, IDs, e-mail addresses, URLs, request/response payloads, or raw API
711
- errors. See [CONTRIBUTING.md](CONTRIBUTING.md#sandbox-tests) for optional fixture
712
- variables and safety guidance.
713
-
714
1157
  ## Development
715
1158
 
716
1159
  ```bash
717
- bun install # or npm install
1160
+ bun install --frozen-lockfile
718
1161
  bun run typecheck # source, script, and test type checks
719
1162
  bun run lint
720
1163
  bun test # bun:test suites