@assinafy/sdk 2.1.1 → 2.2.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
@@ -6,18 +6,50 @@ 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.
10
-
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.
9
+ routes used by existing integrations and two legacy browser URL helpers are
10
+ retained for compatibility.
11
+
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.
15
+
16
+ ## Contents
17
+
18
+ This document runs from setup to a complete signature workflow, then to
19
+ per-resource detail. Read it in order the first time; use it as a reference
20
+ afterwards.
21
+
22
+ **Getting set up** — [Requirements](#requirements) ·
23
+ [Installation](#installation) · [Quick start](#quick-start) ·
24
+ [Authentication](#authentication) · [Configuration](#configuration)
25
+ ([rate limiting](#rate-limiting), [factories](#factories)) ·
26
+ [Endpoint coverage](#endpoint-coverage)
27
+
28
+ **The end-to-end flow** — [Document lifecycle](#document-lifecycle):
29
+ [upload](#1-upload-the-pdf) → [signers](#2-create-or-reuse-the-email-signers) →
30
+ [price and request signatures](#3-price-then-request-signatures) →
31
+ [the signer's side](#4-complete-the-email-signer-flow) →
32
+ [completion and artifacts](#5-observe-completion-and-download-artifacts)
33
+
34
+ **Per-resource detail** — [Resource reference](#resource-reference):
35
+ [documents](#documents) · [signers](#signers) · [assignments](#assignments) ·
36
+ [paid signing branches](#paid-signing-branches) · [templates](#templates) ·
37
+ [tags](#tags) · [workspaces](#workspaces) ·
38
+ [field definitions](#field-definitions) ·
39
+ [auth and API keys](#authentication--api-key-management) ·
40
+ [the current user](#authenticated-user) · [webhooks](#webhooks)
41
+ ([verification](#webhook-verification)) ·
42
+ [signer-side endpoints](#signer-side-endpoints)
43
+
44
+ **Everything else** — [High-level helper](#high-level-helper) ·
45
+ [Errors](#errors) · [Development](#development) · [License](#license)
14
46
 
15
47
  ## Requirements
16
48
 
17
49
  - Node.js 22+ for the built-in `FormData` / `Blob` APIs used by uploads. Packed
18
50
  CJS and ESM imports are tested on 22 (maintenance LTS), 24 (active LTS), and
19
51
  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)
52
+ - or Bun 1.4.0 (the version pinned for development and CI)
21
53
 
22
54
  ## Installation
23
55
 
@@ -39,23 +71,30 @@ The package is published to both [npmjs.com](https://www.npmjs.com/package/@assi
39
71
  ```ts
40
72
  import { AssinafyClient } from '@assinafy/sdk';
41
73
 
74
+ const baseUrl = process.env.ASSINAFY_BASE_URL ?? 'https://api.assinafy.com.br/v1';
42
75
  const client = new AssinafyClient({
43
76
  apiKey: process.env.ASSINAFY_API_KEY!,
44
77
  accountId: process.env.ASSINAFY_ACCOUNT_ID!,
78
+ baseUrl,
45
79
  });
46
80
 
47
81
  const result = await client.uploadAndRequestSignatures({
48
82
  source: { filePath: './contract.pdf' },
49
83
  signers: [
50
- { name: 'John Doe', email: 'john@example.com' },
51
- { name: 'Jane Smith', email: 'jane@example.com', whatsapp_phone_number: '+5548999990000' },
84
+ { name: 'John Doe', email: 'john@example.com' },
85
+ { name: 'Jane Smith', email: 'jane@example.com' },
52
86
  ],
53
87
  message: 'Please sign this contract',
54
88
  });
55
89
 
56
90
  console.log('Document ID:', result.document.id);
91
+ console.log('Assignment ID:', result.assignment.id);
57
92
  ```
58
93
 
94
+ This path uses email verification and notification for every signer. WhatsApp
95
+ and ICP-Brasil certificate signing have separate prerequisites and costs; see
96
+ [Paid signing branches](#paid-signing-branches) before enabling either one.
97
+
59
98
  ## Authentication
60
99
 
61
100
  The API supports two authentication methods. Prefer `apiKey` — it maps to the `X-Api-Key` header recommended by Assinafy for backend services.
@@ -85,6 +124,11 @@ await publicClient.signerDocuments.self(signerAccessCode);
85
124
  Protected methods still require `apiKey` or `token`; the API returns its normal
86
125
  `401` response if one is called without credentials.
87
126
 
127
+ Every SDK transport, including public and signer-access-code requests, sends
128
+ `User-Agent: Assinafy-Typescript-SDK/v<VERSION>`, where `<VERSION>` is the
129
+ installed package version. The exact value is also exported as
130
+ `SDK_USER_AGENT` for custom transport checks and observability rules.
131
+
88
132
  ## Configuration
89
133
 
90
134
  | Option | Type | Default | Description |
@@ -92,7 +136,7 @@ Protected methods still require `apiKey` or `token`; the API returns its normal
92
136
  | `apiKey` | string | — | Preferred credential (sent as `X-Api-Key`). |
93
137
  | `token` | string | — | Access token (sent as `Authorization: Bearer`). |
94
138
  | `accountId` | string | — | Default workspace/account ID. |
95
- | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Override base URL (e.g. the sandbox). |
139
+ | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Absolute API base without credentials, query, or fragment. Must be `https` unless the host is loopback. |
96
140
  | `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
141
  | `timeout` | number | `30000` | Request timeout in milliseconds. |
98
142
  | `maxRetries` | number | `2` | Auto-retries eligible HTTP 429 responses, honoring `Retry-After`. `0` disables. |
@@ -102,9 +146,12 @@ Protected methods still require `apiKey` or `token`; the API returns its normal
102
146
 
103
147
  On an HTTP `429`, the client automatically retries up to `maxRetries` times,
104
148
  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.
149
+ before each attempt. Automatic replay is limited to read-safe `GET`, `HEAD`,
150
+ `OPTIONS`, and `DELETE` requests. `GET /sign` is excluded because it records
151
+ that the signer viewed the assignment. Writes are not replayed by default.
152
+ A non-empty `Idempotency-Key` opts a custom request into SDK replay, but it is
153
+ not part of the current Assinafy OpenAPI contract: confirm that the target
154
+ route deduplicates that key server-side first. No other HTTP status is retried.
108
155
 
109
156
  ### Factories
110
157
 
@@ -122,8 +169,8 @@ const client = AssinafyClient.fromConfig({
122
169
  ## Endpoint coverage
123
170
 
124
171
  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).
172
+ covered. The table below is the resource-level summary; the detailed operation
173
+ ledger is in [docs/API_COVERAGE.md](docs/API_COVERAGE.md).
127
174
 
128
175
  | Resource | Endpoints |
129
176
  | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -136,7 +183,7 @@ operation-by-operation ledger is in [docs/API_COVERAGE.md](docs/API_COVERAGE.md)
136
183
  | `client.webhooks` | register, get, inactivate, listEventTypes, listDispatches, retryDispatch |
137
184
  | `client.fields` | create, list, get, update, delete, validate, validateMultiple, listTypes |
138
185
  | `client.auth` | getSocialLoginUrl, getSocialLoginCallbackUrl, login, socialLogin, linkSocialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword |
139
- | `client.users` | getCurrent, getStats |
186
+ | `client.users` | getCurrent, getStats, getNotificationPreferences, updateNotificationPreferences |
140
187
  | `client.signerDocuments` | getCurrent, list, **search**, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline |
141
188
  | `client.webhookVerifier` | verify, extractEvent, getEventType, getEventData |
142
189
 
@@ -148,7 +195,251 @@ inline in the generated declarations. Editors expose the reference on hover,
148
195
  and declaration files ship with the package. The coverage ledger links those
149
196
  typed methods back to each upstream operation without duplicating the schema.
150
197
 
151
- ## Resources
198
+ ## Document lifecycle
199
+
200
+ The normal integration has an account-owner phase, a signer phase, and a final
201
+ artifact phase. The example below keeps every signer on email and uses a
202
+ `virtual` assignment, so no page coordinates or paid notification channel are
203
+ required.
204
+
205
+ ### 1. Upload the PDF
206
+
207
+ ```ts
208
+ const uploaded = await client.documents.upload({ filePath: './contract.pdf' });
209
+ ```
210
+
211
+ The official multipart body contains the `file` part. The SDK also supports a
212
+ display-name override (used as that file part's filename) and an optional JSON
213
+ `metadata` part for deployments that accept it. A successful response is
214
+ `IDocumentUploadResponse`:
215
+
216
+ ```ts
217
+ {
218
+ resource?: string;
219
+ id: string;
220
+ account_id: string;
221
+ template_id: string | null;
222
+ name: string;
223
+ status: DocumentStatus;
224
+ assignment?: IAssignment | null;
225
+ artifacts: {
226
+ original: string;
227
+ certificated?: string;
228
+ 'certificate-page'?: string;
229
+ pades?: string;
230
+ bundle?: string;
231
+ thumbnail?: string;
232
+ };
233
+ signing_url?: string;
234
+ pages: Array<{ id: string; number: number; height: number; width: number; download_url: string }>;
235
+ tags?: Array<{ id: string; name: string; color?: string | null }>;
236
+ created_at: string;
237
+ updated_at: string;
238
+ is_closed: boolean;
239
+ decline_reason: string | null;
240
+ declined_by: ISigner | null;
241
+ }
242
+ ```
243
+
244
+ `DocumentStatus` covers `uploading`, `uploaded`, `metadata_processing`,
245
+ `metadata_ready`, `pending_signature`, `expired`, `certificating`,
246
+ `certificated`, `rejected_by_signer`, `rejected_by_user`, and `failed`.
247
+
248
+ Uploads must be PDFs, at most 25 MB and at most 2,000 pages. The SDK checks the
249
+ extension, size, and `%PDF-` header before sending. A new upload can have an
250
+ empty `pages` array until metadata processing finishes. Wait before creating a
251
+ `collect` assignment because its fields refer to rendered page IDs; a
252
+ `virtual` assignment may be created immediately.
253
+
254
+ ```ts
255
+ const prepared = await client.documents.waitUntilReady(uploaded.id, {
256
+ maxWaitMs: 30_000,
257
+ pollIntervalMs: 2_000,
258
+ });
259
+ ```
260
+
261
+ ### 2. Create or reuse the email signers
262
+
263
+ ```ts
264
+ const signerA = await client.signers.create({
265
+ full_name: 'John Doe',
266
+ email: 'john@example.com',
267
+ });
268
+ const signerB = await client.signers.create({
269
+ full_name: 'Jane Smith',
270
+ email: 'jane@example.com',
271
+ });
272
+ ```
273
+
274
+ The wire body is `{ full_name, email }`. Each response is an `ISigner`:
275
+
276
+ ```ts
277
+ {
278
+ resource?: string;
279
+ id: string;
280
+ full_name: string;
281
+ email: string | null;
282
+ whatsapp_phone_number?: string | null;
283
+ cpf?: string | null; // compatibility type; not echoed by the API
284
+ has_accepted_terms?: boolean;
285
+ has_signature?: boolean; // signer-self response only
286
+ has_initial?: boolean; // signer-self response only
287
+ is_signature_reusable?: boolean; // signer-self response only
288
+ metadata?: Record<string, unknown>;
289
+ }
290
+ ```
291
+
292
+ When an email is present, `signers.create()` first looks up that email in the
293
+ workspace and reuses the matching signer; a name-only or phone-only request
294
+ always creates a new signer.
295
+
296
+ ### 3. Price, then request signatures
297
+
298
+ Cost estimation takes channel descriptors, not signer IDs:
299
+
300
+ ```ts
301
+ const estimate = await client.assignments.estimateCost(uploaded.id, {
302
+ method: 'virtual',
303
+ signers: [{}, {}], // `{}` selects Email for each signer
304
+ });
305
+
306
+ if (!estimate.has_sufficient_resources) {
307
+ throw new Error(estimate.blocking_reason ?? estimate.message ?? 'Insufficient resources');
308
+ }
309
+ ```
310
+
311
+ The response is `ICostEstimate`:
312
+
313
+ ```ts
314
+ {
315
+ documents: number;
316
+ credits: number;
317
+ needs_extra_document: boolean;
318
+ extra_document_cost: number;
319
+ total_credits: number;
320
+ breakdown: Array<{ code: string; name: string; cost: number; quantity?: number; unit_cost?: number }>;
321
+ document_balance: number;
322
+ credit_balance: number;
323
+ has_sufficient_resources: boolean;
324
+ blocking_reason: 'PendingPayment' | 'InsufficientDocuments' | 'InsufficientCredits' | null;
325
+ message: string | null;
326
+ }
327
+ ```
328
+
329
+ Create the email assignment only after accepting that estimate:
330
+
331
+ ```ts
332
+ const assignment = await client.assignments.create(uploaded.id, {
333
+ method: 'virtual',
334
+ signers: [
335
+ { id: signerA.id, verification_method: 'Email', notification_methods: ['Email'] },
336
+ { id: signerB.id, verification_method: 'Email', notification_methods: ['Email'] },
337
+ ],
338
+ message: 'Please review and sign',
339
+ expires_at: '2027-12-31T23:59:00Z',
340
+ });
341
+ ```
342
+
343
+ The request returns an `IAssignment`:
344
+
345
+ ```ts
346
+ {
347
+ resource?: string;
348
+ id: string;
349
+ sender_email?: string;
350
+ method: 'virtual' | 'collect';
351
+ expires_at?: string | null;
352
+ expiration?: string;
353
+ message?: string | null;
354
+ signers: IAssignmentSigner[];
355
+ copy_receivers?: Array<Record<string, unknown>>;
356
+ items?: IAssignmentItem[];
357
+ summary?: {
358
+ signer_count: number;
359
+ completed_count: number;
360
+ signers: Array<ISigner & { completed?: boolean }>;
361
+ };
362
+ signing_urls?: Array<{ signer_id: string; url: string }>;
363
+ }
364
+ ```
365
+
366
+ The URLs and delivered messages contain signer credentials; treat them as
367
+ secrets.
368
+
369
+ ### 4. Complete the email signer flow
370
+
371
+ Assinafy sends each signer a link containing their access code and sends the
372
+ one-time verification code through the selected channel. Neither value is
373
+ returned as a standalone owner-side API field. A custom signer portal must
374
+ obtain both values from the signer-delivery flow; do not manufacture them or
375
+ log them.
376
+
377
+ ```ts
378
+ // Signer-side client: no account API credential is needed or sent.
379
+ const signerClient = new AssinafyClient({
380
+ baseUrl,
381
+ });
382
+
383
+ const self = await signerClient.signerDocuments.self(accessCode); // ISignerSelf
384
+
385
+ // Query: signer-access-code=<accessCode>
386
+ // Body: { 'verification-code': '<six-digit code>' }
387
+ await signerClient.signerDocuments.verifyEmail({
388
+ signerAccessCode: accessCode,
389
+ verificationCode,
390
+ }); // Promise<void>
391
+
392
+ const confirmed = await signerClient.signerDocuments.confirmData(
393
+ uploaded.id,
394
+ accessCode,
395
+ { full_name: self.full_name, email: self.email ?? undefined },
396
+ ); // ISigner
397
+
398
+ const signable = await signerClient.signerDocuments.getAssignment(accessCode, true);
399
+ // `getAssignment` returns IDocumentDetailsResponse and records that the signer
400
+ // viewed the assignment. Do not issue it merely as a health check.
401
+
402
+ await signerClient.signerDocuments.signMultiple([signable.id], accessCode);
403
+ // Wire body: { document_ids: [signable.id] }; acknowledgement has no data.
404
+ ```
405
+
406
+ Repeat this phase separately for each signer with that signer's own access code
407
+ and one-time code. The two signers in this example share the default step and
408
+ can sign in parallel.
409
+
410
+ `signMultiple` is only for `virtual` assignments. For `collect`, read
411
+ `signable.assignment.items`, then call `sign(documentId, assignmentId,
412
+ accessCode, entries)` with a non-empty array of
413
+ `{ itemId, fieldId, pageId, value }`. A virtual signer must confirm their data
414
+ before signing. A `DigitalCertificate` signer cannot call `sign`; that branch
415
+ uses Assinafy's certificate-start and certificate-complete flow, which is not
416
+ part of this SDK's current 89-operation surface.
417
+
418
+ ### 5. Observe completion and download artifacts
419
+
420
+ Subscribe to `document_ready` for event-driven completion, or fetch
421
+ `documents.details(documentId)` until `status === 'certificated'`. Webhook
422
+ deliveries can repeat, so use their numeric `id` as an
423
+ idempotency key. Once complete:
424
+
425
+ ```ts
426
+ const finalDocument = await client.documents.details(uploaded.id);
427
+ const signedPdf = await client.documents.download(uploaded.id, 'certificated');
428
+ const certificatePage = await client.documents.download(uploaded.id, 'certificate-page');
429
+ const bundleZip = await client.documents.download(uploaded.id, 'bundle');
430
+
431
+ // Validate an Assinafy signature hash when your workflow has extracted it.
432
+ const validation = await client.documents.verify(documentSignatureHash);
433
+ ```
434
+
435
+ `original`, `certificated`, and `certificate-page` are PDFs. `bundle` is a ZIP
436
+ containing those three artifacts and also `pades` when the document had an
437
+ ICP-Brasil certificate signer. The `pades` PDF exists only for documents that
438
+ had certificate signers. An artifact can return `404` before generation has
439
+ finished. `decline_reason` is included in document details only when the access
440
+ token belongs to the document creator.
441
+
442
+ ## Resource reference
152
443
 
153
444
  Most account-scoped methods accept an optional `accountId` that overrides the
154
445
  client default. Workspace `get`, `update`, `delete`, branding, and statistics
@@ -162,13 +453,15 @@ const doc = await client.documents.upload(
162
453
  { filePath: './contract.pdf' },
163
454
  { name: 'Service agreement', metadata: { type: 'service' } },
164
455
  );
456
+ // `name` and `metadata` are compatibility multipart parts outside the published
457
+ // file-only request schema.
165
458
  // `name` is optional and defaults to the file's own name. The API derives the
166
459
  // display name from the uploaded filename and appends `.pdf` when absent, so
167
460
  // the document above is stored as 'Service agreement.pdf'. Accents are
168
461
  // transliterated by the API ('Contrato de Serviço' → 'Contrato de Servico.pdf').
169
462
  // → {
170
463
  // resource: 'document', id: '1031…', account_id: '102d…', template_id: null,
171
- // name: 'contract.pdf', status: 'uploaded',
464
+ // name: 'Service agreement.pdf', status: 'uploaded',
172
465
  // artifacts: { original: 'https://…/download/original' },
173
466
  // signing_url: 'https://app…/sign/1031…',
174
467
  // pages: [], // populated once status reaches `metadata_ready`
@@ -179,7 +472,7 @@ const doc = await client.documents.upload(
179
472
  await client.documents.upload({ buffer, fileName: 'contract.pdf' });
180
473
 
181
474
  // 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' });
475
+ const { data, meta } = await client.documents.list({ page: 1, 'per-page': 20, sort: 'updated_at' });
183
476
 
184
477
  // Search is the lightweight alternative to list: same item shape, but the API
185
478
  // skips the expanded `assignment`/`pages`. Prefer it for name lookups.
@@ -194,7 +487,11 @@ await client.documents.waitUntilReady(doc.id, { maxWaitMs: 30_000 });
194
487
  // (Passing `name` to upload() avoids both the round-trip and the race.)
195
488
  await client.documents.rename(doc.id, 'Signed service agreement.pdf');
196
489
 
197
- await client.documents.download(doc.id, 'certificated'); // 'original' | 'certificated' | 'certificate-page' | 'bundle'
490
+ await client.documents.download(doc.id, 'certificated'); // signed PDF
491
+ await client.documents.download(doc.id, 'certificate-page');
492
+ await client.documents.download(doc.id, 'bundle'); // ZIP
493
+ // `pades` exists only when at least one signer used DigitalCertificate.
494
+ await client.documents.download(doc.id, 'pades');
198
495
  await client.documents.thumbnail(doc.id);
199
496
  await client.documents.downloadPage(doc.id, pageId);
200
497
 
@@ -222,13 +519,30 @@ const urgentTag = await client.tags.create({ name: 'Urgent' });
222
519
  await client.documents.listTags(doc.id);
223
520
  await client.documents.replaceTags(doc.id, [contractsTag.id, quarterTag.id]); // [] detaches all
224
521
  await client.documents.addTags(doc.id, [urgentTag.id]); // append
225
- await client.documents.detachTag(doc.id, urgentTag.id); // remove one
522
+ await client.documents.detachTag(doc.id, urgentTag.id); // { detached: true }
226
523
  ```
227
524
 
228
525
  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).
230
-
231
- List endpoints return `{ data, meta }` where `meta` is populated from the `X-Pagination-*` headers returned by the API.
526
+ with the PDF magic header (`%PDF-`) are accepted. The API also limits documents
527
+ to 2,000 pages.
528
+
529
+ Page and artifact URLs embedded in JSON responses still require the same
530
+ account authentication as their download operations. Prefer
531
+ `documents.downloadPage()` and `documents.download()` so the SDK applies the
532
+ credential and returns a `Buffer`. `bundle` contains `original`, `certificated`,
533
+ and `certificate-page`, plus `pades` when available.
534
+
535
+ List endpoints return `{ data, meta }`, where `meta` is populated from the
536
+ `X-Pagination-*` response headers. Two API behaviours are worth knowing because
537
+ both are silent rather than errors:
538
+
539
+ - Only the hyphenated `per-page` is read. `per_page` is accepted and ignored,
540
+ falling back to 20 rows — so the SDK rewrites `per_page` to `per-page` on
541
+ every list method, and an explicit `per-page` wins when both are given.
542
+ - `per-page` is clamped to **50**. Asking for 100 returns 50 rows with a `200`.
543
+ The exported `MAX_LIST_PAGE_SIZE` constant is that ceiling; page through with
544
+ `page` rather than requesting a larger page, and trust `meta.per_page` over
545
+ the value you asked for.
232
546
 
233
547
  ### Signers
234
548
 
@@ -236,38 +550,36 @@ List endpoints return `{ data, meta }` where `meta` is populated from the `X-Pag
236
550
  await client.signers.create({
237
551
  full_name: 'John Doe',
238
552
  email: 'john@example.com',
239
- whatsapp_phone_number: '+5548999990000',
240
- cpf: '123.456.789-00', // optional Brazilian tax ID — non-digits are stripped automatically
553
+ cpf: '123.456.789-00', // legacy compatibility input; non-digits are stripped
241
554
  });
242
555
  // → { id: '19e6…', full_name: 'John Doe', email: 'john@example.com',
243
- // whatsapp_phone_number: '+5548999990000', has_accepted_terms: false }
556
+ // whatsapp_phone_number: null, has_accepted_terms: false }
244
557
  // (note: `cpf` is accepted on input but never echoed back by the API)
245
558
 
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.
559
+ // Both contacts are optional. A name-only signer cannot be notified until a
560
+ // contact is added.
253
561
  await client.signers.create({ full_name: 'Contact Pending' });
254
562
 
255
- // PHP SDK compatibility aliases are also accepted
256
563
  await client.signers.create({
257
564
  full_name: 'Jane Doe',
258
565
  email: 'jane@example.com',
259
- phone: '+5548999991111', // alias for whatsapp_phone_number
260
566
  });
261
567
 
262
568
  await client.signers.get(signerId);
263
- await client.signers.list({ page: 1, per_page: 50, search: 'john' });
264
- await client.signers.update(signerId, { full_name: 'Johnny Doe' });
569
+ await client.signers.list({ page: 1, 'per-page': 50, search: 'john' });
570
+ await client.signers.update(signerId, {
571
+ full_name: 'Johnny Doe',
572
+ government_id: '390.533.447-05', // official update field; sent as digits
573
+ });
265
574
  await client.signers.delete(signerId);
266
575
 
267
576
  const existing = await client.signers.findByEmail('john@example.com');
268
577
  ```
269
578
 
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.
579
+ When an `email` is supplied, `signers.create()` is idempotent by email: it
580
+ reuses an existing signer when the same email is already present in the
581
+ workspace. Signers without email are always created fresh. See
582
+ [Paid signing branches](#paid-signing-branches) for phone-only signers.
271
583
 
272
584
  ### Assignments
273
585
 
@@ -281,8 +593,8 @@ await client.assignments.create(documentId, {
281
593
  method: 'virtual',
282
594
  signers: ['signer-1', 'signer-2'],
283
595
  message: 'Please review and sign',
284
- expires_at: '2024-12-31T23:59:00Z',
285
- copy_receivers: ['observer-id'],
596
+ expires_at: '2027-12-31T23:59:00Z',
597
+ copy_receivers: ['copy-recipient-signer-id'],
286
598
  });
287
599
 
288
600
  // Sequential signing: `step` controls signing order (parallel within a step).
@@ -294,31 +606,47 @@ await client.assignments.create(documentId, {
294
606
  ],
295
607
  });
296
608
 
609
+ // Collect fields use 150-DPI page-image pixels measured from the upper-left.
610
+ await client.assignments.create(documentId, {
611
+ method: 'collect',
612
+ signers: [{ id: signerId }],
613
+ entries: [{
614
+ page_id: pageId,
615
+ fields: [{
616
+ signer_id: signerId,
617
+ field_id: fieldId,
618
+ display_settings: {
619
+ left: 69, top: 282, width: 421, height: 45.86, fontSize: 22,
620
+ fontFamily: 'Arial', backgroundColor: '#D5EBFF',
621
+ },
622
+ }],
623
+ }],
624
+ });
625
+
297
626
  // Estimate cost (the endpoint prices channel descriptors, not signer IDs) → ICostEstimate
298
627
  await client.assignments.estimateCost(documentId, { signers: [{}] }); // default Email
299
- await client.assignments.estimateCost(documentId, {
300
- signers: [{ verification_method: 'Whatsapp' }],
301
- });
302
628
  // → {
303
629
  // documents: 1, credits: 0, needs_extra_document: false, extra_document_cost: 0,
304
630
  // total_credits: 0, breakdown: [], document_balance: 67, credit_balance: 0,
305
631
  // has_sufficient_resources: true, blocking_reason: null, message: null
306
632
  // }
307
633
 
308
- await client.assignments.resetExpiration(documentId, assignmentId, '2025-06-30T00:00:00Z');
309
- await client.assignments.resetExpiration(documentId, assignmentId, null); // remove expiration
634
+ await client.assignments.resetExpiration(documentId, assignmentId, '2027-06-30T00:00:00Z');
635
+ // Compatibility only: the published request requires a date-time string.
636
+ // Confirm target support before using `null` to clear an expiration.
637
+ await client.assignments.resetExpiration(documentId, assignmentId, null);
310
638
 
311
639
  await client.assignments.resendNotification(documentId, assignmentId, signerId);
312
640
  // → { is_sent: true, document_id: '…', signer_id: '…' }
313
641
 
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[]
642
+ const resendCost = await client.assignments.estimateResendCost(documentId, assignmentId, signerId);
643
+ // Official response: ICostEstimate. Older deployments can return the compact
644
+ // IResendCostEstimate branch with `total` and `has_sufficient_credits`; narrow
645
+ // with `'total_credits' in resendCost` before reading branch-specific fields.
319
646
  ```
320
647
 
321
- The `create` response is an `IAssignment`: `{ id, method, signers: [...], items: [...], signing_urls: [{ signer_id, url }], … }`.
648
+ The `create` response is an `IAssignment`: `{ id, method, signers: [...],
649
+ items: [{ display_settings, ... }], signing_urls: [{ signer_id, url }], … }`.
322
650
 
323
651
  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
652
 
@@ -329,15 +657,92 @@ await client.documents.delete(documentId); // w
329
657
  await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'No longer needed'); // signer-side
330
658
  ```
331
659
 
660
+ ### Paid signing branches
661
+
662
+ Keep the email flow as the default. Enable either branch below only after the
663
+ workspace has the required plan or feature and the returned cost estimate is
664
+ acceptable.
665
+
666
+ #### WhatsApp verification and notification
667
+
668
+ WhatsApp is available only on paid subscriptions and costs 0.45 credit per
669
+ notification. Create a phone-only signer or add a phone to an existing signer,
670
+ then request the `Whatsapp` channel explicitly:
671
+
672
+ ```ts
673
+ const phoneSigner = await client.signers.create({
674
+ full_name: 'Mobile Signer',
675
+ whatsapp_phone_number: '+5511999990000',
676
+ });
677
+
678
+ const whatsappCost = await client.assignments.estimateCost(documentId, {
679
+ method: 'virtual',
680
+ signers: [{ verification_method: 'Whatsapp', notification_methods: ['Whatsapp'] }],
681
+ });
682
+
683
+ const whatsappAssignment = await client.assignments.create(documentId, {
684
+ method: 'virtual',
685
+ signers: [{
686
+ id: phoneSigner.id,
687
+ verification_method: 'Whatsapp',
688
+ notification_methods: ['Whatsapp'],
689
+ }],
690
+ });
691
+
692
+ const notices = await client.assignments.listWhatsAppNotifications(
693
+ documentId,
694
+ whatsappAssignment.id,
695
+ );
696
+ // IWhatsAppNotification[]:
697
+ // [{ sent_at, header, body, buttons: [{ text, url? }], phone_number, signer_id }]
698
+ ```
699
+
700
+ The high-level helper selects this paid branch for a signer that has a phone
701
+ number but no email. Button URLs can contain signer credentials; do not log or
702
+ forward them outside the signing flow.
703
+
704
+ #### ICP-Brasil digital certificate
705
+
706
+ `DigitalCertificate` requires the account feature, a CPF or CNPJ in the
707
+ signer's `government_id`, and exactly one certificate signer in that signing
708
+ step. It costs two credits per certificate signer in addition to the selected
709
+ notification cost.
710
+
711
+ ```ts
712
+ const certificateSigner = await client.signers.update(signerId, {
713
+ government_id: '390.533.447-05',
714
+ });
715
+
716
+ const certificateCost = await client.assignments.estimateCost(documentId, {
717
+ method: 'virtual',
718
+ signers: [{ verification_method: 'DigitalCertificate', notification_methods: ['Email'] }],
719
+ });
720
+
721
+ await client.assignments.create(documentId, {
722
+ method: 'virtual',
723
+ signers: [{
724
+ id: certificateSigner.id,
725
+ step: 1,
726
+ verification_method: 'DigitalCertificate',
727
+ notification_methods: ['Email'],
728
+ }],
729
+ });
730
+ ```
731
+
732
+ Before opening the assignment, the signer must confirm identity data and accept
733
+ terms with `confirmData(..., { has_accepted_terms: true })` or `acceptTerms()`.
734
+ The regular `sign()` endpoint rejects certificate signers; they complete the
735
+ ICP-Brasil flow through Assinafy's browser integration. After completion,
736
+ `documents.download(documentId, 'pades')` returns the qualified PAdES artifact.
737
+
332
738
  ### Templates
333
739
 
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.
740
+ `templates.list()` is part of the current OpenAPI document. Existing
741
+ integrations can also use five template-management routes—`create`, `get`,
742
+ `update`, `delete`, and `downloadPage`—that are absent from that document. See
743
+ [compatibility notes](docs/COMPATIBILITY.md#template-management-extensions).
744
+ Template status casing can vary by deployment; normalize with
745
+ `template.status.toLowerCase()` when branching on it.
341
746
 
342
747
  ```ts
343
748
  // Create a template by uploading a PDF (multipart). The template starts in
@@ -354,7 +759,7 @@ const created = await client.templates.create(
354
759
  // pages: [], tags: [], created_at: '2026-…', updated_at: '2026-…'
355
760
  // }
356
761
 
357
- const { data, meta } = await client.templates.list({ search: 'NDA', per_page: 20 });
762
+ const { data, meta } = await client.templates.list({ search: 'NDA', 'per-page': 20 });
358
763
  const template = await client.templates.get(created.id); // includes pages[] + default_document_tags
359
764
  await client.templates.update(created.id, { name: 'NDA v2', message: 'Please sign' });
360
765
  const firstPage = template.pages?.[0];
@@ -383,8 +788,14 @@ await client.documents.estimateCostFromTemplate(templateId, [
383
788
  // has_sufficient_resources: true, blocking_reason: null, breakdown: [], … }
384
789
  ```
385
790
 
791
+ Template signer descriptors also accept
792
+ `verification_method: 'DigitalCertificate'` with the same prerequisites under
793
+ [ICP-Brasil digital certificate](#icp-brasil-digital-certificate).
794
+
386
795
  Template creation only uploads the PDF and provisions the default editor role —
387
796
  configure roles/fields in the Assinafy editor (or the web UI) afterwards.
797
+ The `download_url` values in template page objects are protected URLs; prefer
798
+ `templates.downloadPage()` so the API credential is attached.
388
799
 
389
800
  ### Tags
390
801
 
@@ -444,7 +855,8 @@ await client.workspaces.getStats(accountId, {
444
855
  });
445
856
 
446
857
  await client.workspaces.delete(accountId);
447
- // If the API reports deletion restrictions, explicitly opt in to overriding them:
858
+ // `force` cancels an active paid subscription as part of account deletion. It
859
+ // is not a general bypass for unrelated deletion restrictions.
448
860
  await client.workspaces.delete(restrictedAccountId, { force: true });
449
861
  ```
450
862
 
@@ -466,7 +878,7 @@ await client.fields.validate(fieldId, '400.676.228-36', { signerAccessCode });
466
878
  await client.fields.validateMultiple(
467
879
  [
468
880
  { field_id: 'f1', value: '1111111111111' },
469
- { field_id: 'f2', value: 'foo@bar.com' },
881
+ { field_id: 'f2', value: 'value@example.com' },
470
882
  ],
471
883
  { signerAccessCode },
472
884
  );
@@ -513,8 +925,17 @@ const daily = await client.users.getStats({
513
925
  granularity: 'daily',
514
926
  month: '2026-06',
515
927
  });
516
- // Each row includes period, documents_uploaded, documents_sent,
517
- // signature_requests by channel/view/completion, and documents_certified.
928
+ // Each row includes period, upload/send/certification totals, notification
929
+ // counts for email/WhatsApp/bypass, verification counts for
930
+ // email/WhatsApp/bypass/digital-certificate, viewed, and completed counts.
931
+
932
+ const preferences = await client.users.getNotificationPreferences();
933
+ await client.users.updateNotificationPreferences({
934
+ SignerDeclined: false,
935
+ DocumentExpired: false,
936
+ });
937
+ // Updates merge: omitted keys keep their current value. Both methods return
938
+ // the complete nine-key notification preference map.
518
939
  ```
519
940
 
520
941
  ### Webhooks
@@ -523,6 +944,7 @@ const daily = await client.users.getStats({
523
944
  await client.webhooks.register({
524
945
  url: 'https://example.com/webhooks/assinafy',
525
946
  email: 'admin@example.com',
947
+ is_active: true,
526
948
  // events defaults to the current SDK default set below
527
949
  events: [
528
950
  'document_ready',
@@ -533,13 +955,91 @@ await client.webhooks.register({
533
955
  ],
534
956
  });
535
957
 
536
- await client.webhooks.get(); // current subscription or null
958
+ await client.webhooks.get(); // IWebhookSubscription | null
537
959
  await client.webhooks.inactivate(); // stop deliveries (no delete route exists)
538
960
  await client.webhooks.listEventTypes();
539
- await client.webhooks.listDispatches({ delivered: false, page: 1, 'per-page': 20 });
540
- await client.webhooks.retryDispatch(dispatchId);
961
+ const history = await client.webhooks.listDispatches({
962
+ delivered: false,
963
+ page: 1,
964
+ 'per-page': 20,
965
+ }); // { data: IWebhookDispatch[], meta?: PaginationMeta }
966
+ const retried = await client.webhooks.retryDispatch(dispatchId); // IWebhookDispatch
967
+ ```
968
+
969
+ `register` sends `{ events, is_active, url, email }` and returns
970
+ `{ events, is_active, url, email, updated_at? }`. Assinafy delivers each event
971
+ as an HTTP `POST` with `Content-Type: application/json` and `Connection: close`.
972
+ Any `2xx` is success. There are at most two automatic attempts, separated by
973
+ three seconds. After ten consecutive failed events, ordinary delivery pauses
974
+ and about 5% of later events are attempted until one succeeds; use
975
+ `retryDispatch()` for an immediate manual redelivery. The dispatch history
976
+ retains only the first 2,000 characters of the receiver's response body.
977
+
978
+ Each history or retry result is an `IWebhookDispatch`:
979
+
980
+ ```ts
981
+ {
982
+ resource?: string;
983
+ id: string;
984
+ event: string;
985
+ activity_id: number;
986
+ endpoint: string | null;
987
+ payload: IWebhookPayload | Record<string, unknown> | null;
988
+ delivered: boolean;
989
+ http_status: number | null;
990
+ response_body: string | null;
991
+ error: string | null;
992
+ created_at: string;
993
+ updated_at?: string;
994
+ }
995
+ ```
996
+
997
+ Every delivery body uses this envelope:
998
+
999
+ ```ts
1000
+ {
1001
+ id: number; // use for idempotent processing
1002
+ event: string;
1003
+ message: string | null;
1004
+ payload: Record<string, unknown> | null;
1005
+ origin: { ip?: string; 'user-agent'?: string } | null;
1006
+ created_at: number; // Unix seconds
1007
+ subject: { type: 'User' | 'Signer' | 'Account' | 'Document' | 'Template'; [key: string]: unknown };
1008
+ object: { type: 'User' | 'Signer' | 'Account' | 'Document' | 'Template'; [key: string]: unknown };
1009
+ account_id: string;
1010
+ }
541
1011
  ```
542
1012
 
1013
+ Event-specific values are:
1014
+
1015
+ | `event` | `subject.type` | `object.type` | `payload` keys |
1016
+ | --- | --- | --- | --- |
1017
+ | `document_uploaded` | `User` | `Document` | — |
1018
+ | `document_metadata_ready` | `User` | `Document` | — |
1019
+ | `document_prepared` | `User` | `Document` | — |
1020
+ | `assignment_created` | `User` | `Document` | `user_name`, `user_email`, `user_telephone` |
1021
+ | `document_ready` | `Account` | `Document` | — |
1022
+ | `document_processing_failed` | `Account` | `Document` | `error_message` |
1023
+ | `signature_requested` | `User` | `Document` | `signer_email`, `signer_full_name`, or `signer_whatsapp_phone_number`, according to channel |
1024
+ | `signer_created` | `User` | `Signer` | `signer_full_name` |
1025
+ | `signer_email_verified` | `Signer` | `Document` | `signer_email` |
1026
+ | `signer_whatsapp_verified` | `Signer` | `Document` | `signer_whatsapp_phone_number` |
1027
+ | `signer_data_confirmed` | `Signer` | `Document` | `signer_email` |
1028
+ | `signer_viewed_document` | `Signer` | `Document` | `signer_full_name` |
1029
+ | `signer_signed_document` | `Signer` | `Document` | `signer_full_name` |
1030
+ | `signer_rejected_document` | `Signer` | `Document` | `signer_full_name` |
1031
+ | `user_rejected_document` | `User` | `Document` | `user_name` |
1032
+ | `template_created` | `User` | `Template` | — |
1033
+ | `template_processed` | `User` | `Template` | — |
1034
+ | `template_processing_failed` | `Account` | `Template` | `error_message` |
1035
+
1036
+ `payload`, `subject`, and `object` are event-dependent. Accept unknown fields
1037
+ for forward compatibility and acknowledge only after durable, idempotent
1038
+ processing. Non-`2xx` responses, timeouts, and connection failures all count as
1039
+ failed deliveries. `assignment_created` and `document_metadata_ready` have no
1040
+ guaranteed ordering. For account entities, Assinafy removes the `integration`
1041
+ property before delivery.
1042
+
543
1043
  ### Webhook verification
544
1044
 
545
1045
  `WebhookVerifier` is an opt-in HMAC-SHA256 utility for integrations whose
@@ -591,53 +1091,64 @@ only for compatibility with deployments that still expect the legacy query.
591
1091
 
592
1092
  ```ts
593
1093
  await client.signerDocuments.self(accessCode);
594
- await client.signerDocuments.acceptTerms(accessCode);
595
1094
  await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verificationCode: '123456' });
596
1095
 
597
1096
  await client.signerDocuments.getCurrent(signerId, accessCode);
598
- const { data } = await client.signerDocuments.list(signerId, accessCode, { per_page: 20 });
1097
+ const { data } = await client.signerDocuments.list(signerId, accessCode, { 'per-page': 20 });
599
1098
  // Signer-side counterpart of documents.search(), authorised by the access code.
600
1099
  const found = await client.signerDocuments.search(signerId, accessCode, 'invoice');
601
1100
  await client.signerDocuments.download(signerId, documentId, 'original');
1101
+ // Available only after an ICP-Brasil certificate signer completes signing.
1102
+ await client.signerDocuments.download(signerId, documentId, 'pades');
602
1103
 
603
1104
  await client.signerDocuments.confirmData(documentId, accessCode, {
604
1105
  email: 'me@example.com',
605
1106
  full_name: 'Example Signer',
606
1107
  government_id: '123.456.789-00',
1108
+ has_accepted_terms: true,
607
1109
  });
608
- await client.signerDocuments.acceptTerms(accessCode);
1110
+ // Alternatively, accept terms separately before getAssignment():
1111
+ // await client.signerDocuments.acceptTerms(accessCode);
609
1112
 
610
1113
  // Signature image management ({ reuse: true } persists it for future documents)
611
1114
  await client.signerDocuments.uploadSignature(accessCode, pngBuffer, { imageType: 'signature', reuse: true });
612
1115
  await client.signerDocuments.downloadSignature(accessCode, 'signature');
613
1116
 
614
1117
  // Sign / decline
615
- const assignment = await client.signerDocuments.getAssignment(accessCode);
1118
+ const signable = await client.signerDocuments.getAssignment(accessCode);
1119
+ // `sign()` is for collect assignments and requires every placed field value.
616
1120
  await client.signerDocuments.sign(documentId, assignmentId, accessCode, [
617
1121
  { itemId, fieldId, pageId, value: 'Signed by John' },
618
1122
  ]);
619
1123
  await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'Not authorized');
620
1124
 
621
- // Bulk operations
1125
+ // `signMultiple()` is for virtual assignments only.
622
1126
  await client.signerDocuments.signMultiple(['doc-1', 'doc-2'], accessCode);
623
1127
  await client.signerDocuments.declineMultiple(['doc-1'], 'Unfavorable terms', accessCode);
624
1128
  ```
625
1129
 
1130
+ `sign()` also requires virtual signers to have confirmed their data first, but
1131
+ virtual assignments should normally use `signMultiple()`. Certificate signers
1132
+ cannot use `sign()`; see [Paid signing branches](#paid-signing-branches).
1133
+
626
1134
  ## High-level helper
627
1135
 
628
- Uploads a PDF, waits for processing, reuses or creates signers by email, and kicks off a virtual assignment.
1136
+ Uploads a PDF, reuses or creates signers by email, creates a virtual assignment
1137
+ immediately, and optionally waits for processing before returning.
629
1138
 
630
1139
  ```ts
631
1140
  const result = await client.uploadAndRequestSignatures({
632
1141
  source: { filePath: './contract.pdf' },
633
1142
  signers: [
634
1143
  { name: 'John', email: 'john@example.com' },
635
- { name: 'Jane', email: 'jane@example.com', whatsapp_phone_number: '+5548999990000' },
1144
+ { name: 'Jane', email: 'jane@example.com' },
636
1145
  ],
637
1146
  message: 'Please sign',
638
- metadata: { year: 2026 },
1147
+ metadata: { year: 2026 }, // compatibility upload part; omit for file-only wire format
639
1148
  waitForReady: true,
640
- expiresAt: '2026-12-31T00:00:00Z',
1149
+ waitOptions: { maxWaitMs: 30_000, pollIntervalMs: 1_000 },
1150
+ expiresAt: '2027-12-31T00:00:00Z',
1151
+ copyReceivers: ['existing-copy-recipient-signer-id'],
641
1152
  });
642
1153
 
643
1154
  result.document; // fully-processed IDocumentDetailsResponse (waitForReady: true, the default);
@@ -646,10 +1157,20 @@ result.assignment; // IAssignment
646
1157
  result.signer_ids; // string[]
647
1158
  ```
648
1159
 
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.
1160
+ `waitForReady: false` skips post-assignment polling and returns the initial
1161
+ upload response. With the default `true`, the helper creates the assignment first and
1162
+ then waits for the current document details. Both production and sandbox allow
1163
+ virtual assignments in `uploaded` and `metadata_processing` and promote them
1164
+ automatically; only `collect` assignments require rendered pages.
1165
+ Every signer above uses the default email channel. A phone-only signer selects
1166
+ the paid WhatsApp branch described earlier. `copyReceivers` accepts existing
1167
+ signer IDs, not email addresses; check the returned assignment before treating
1168
+ a copy receiver as registered.
1169
+
1170
+ The helper is not transactional. A post-assignment polling error includes the
1171
+ created `documentId`, `assignmentId`, and `signerIds` in its `context` (and in
1172
+ `ValidationError.errors` for timeouts); inspect those IDs before deciding
1173
+ whether to retry the workflow.
653
1174
 
654
1175
  ## Errors
655
1176
 
@@ -674,47 +1195,10 @@ try {
674
1195
  }
675
1196
  ```
676
1197
 
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
1198
  ## Development
715
1199
 
716
1200
  ```bash
717
- bun install # or npm install
1201
+ bun install --frozen-lockfile
718
1202
  bun run typecheck # source, script, and test type checks
719
1203
  bun run lint
720
1204
  bun test # bun:test suites