@assinafy/sdk 2.0.0 → 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/CHANGELOG.md +469 -0
- package/README.md +730 -110
- package/SECURITY.md +59 -0
- package/dist/index.d.mts +4272 -557
- package/dist/index.d.ts +4272 -557
- package/dist/index.js +5282 -583
- package/dist/index.mjs +5280 -583
- package/docs/API_COVERAGE.md +207 -0
- package/docs/COMPATIBILITY.md +362 -0
- package/docs/RELEASING.md +169 -0
- package/package.json +28 -15
package/README.md
CHANGED
|
@@ -2,16 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
TypeScript SDK for the [Assinafy API](https://api.assinafy.com.br/v1/docs) — a Brazilian digital signature platform.
|
|
4
4
|
|
|
5
|
-
Covers
|
|
5
|
+
Covers all 89 operations in the current official OpenAPI document: accounts,
|
|
6
|
+
authentication, users, documents, assignments, signers, signer-side flows,
|
|
7
|
+
templates, tags, fields, webhooks, branding, statistics, and the high-level
|
|
8
|
+
`uploadAndRequestSignatures` workflow. Five additional template-management
|
|
9
|
+
routes used by existing integrations and two legacy browser URL helpers are
|
|
10
|
+
retained for compatibility.
|
|
6
11
|
|
|
7
|
-
|
|
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.
|
|
8
15
|
|
|
9
16
|
## Requirements
|
|
10
17
|
|
|
11
|
-
- Node.js 22+ for the built-in `FormData` / `Blob` APIs used by uploads.
|
|
12
|
-
on 22 (maintenance LTS)
|
|
13
|
-
April 2026 and is
|
|
14
|
-
- or Bun 1.0
|
|
18
|
+
- Node.js 22+ for the built-in `FormData` / `Blob` APIs used by uploads. Packed
|
|
19
|
+
CJS and ESM imports are tested on 22 (maintenance LTS), 24 (active LTS), and
|
|
20
|
+
26 (Current); Node 20 reached end-of-life in April 2026 and is unsupported.
|
|
21
|
+
- or Bun 1.4.0 (the version pinned for development and CI)
|
|
15
22
|
|
|
16
23
|
## Installation
|
|
17
24
|
|
|
@@ -33,24 +40,30 @@ The package is published to both [npmjs.com](https://www.npmjs.com/package/@assi
|
|
|
33
40
|
```ts
|
|
34
41
|
import { AssinafyClient } from '@assinafy/sdk';
|
|
35
42
|
|
|
43
|
+
const baseUrl = process.env.ASSINAFY_BASE_URL ?? 'https://api.assinafy.com.br/v1';
|
|
36
44
|
const client = new AssinafyClient({
|
|
37
45
|
apiKey: process.env.ASSINAFY_API_KEY!,
|
|
38
46
|
accountId: process.env.ASSINAFY_ACCOUNT_ID!,
|
|
39
|
-
|
|
47
|
+
baseUrl,
|
|
40
48
|
});
|
|
41
49
|
|
|
42
50
|
const result = await client.uploadAndRequestSignatures({
|
|
43
51
|
source: { filePath: './contract.pdf' },
|
|
44
52
|
signers: [
|
|
45
|
-
{ name: 'John Doe',
|
|
46
|
-
{ name: 'Jane Smith',
|
|
53
|
+
{ name: 'John Doe', email: 'john@example.com' },
|
|
54
|
+
{ name: 'Jane Smith', email: 'jane@example.com' },
|
|
47
55
|
],
|
|
48
56
|
message: 'Please sign this contract',
|
|
49
57
|
});
|
|
50
58
|
|
|
51
59
|
console.log('Document ID:', result.document.id);
|
|
60
|
+
console.log('Assignment ID:', result.assignment.id);
|
|
52
61
|
```
|
|
53
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
|
+
|
|
54
67
|
## Authentication
|
|
55
68
|
|
|
56
69
|
The API supports two authentication methods. Prefer `apiKey` — it maps to the `X-Api-Key` header recommended by Assinafy for backend services.
|
|
@@ -59,35 +72,61 @@ The API supports two authentication methods. Prefer `apiKey` — it maps to the
|
|
|
59
72
|
// Preferred: X-Api-Key header
|
|
60
73
|
new AssinafyClient({ apiKey: 'k_xxx', accountId: 'acc_xxx' });
|
|
61
74
|
|
|
62
|
-
//
|
|
75
|
+
// Access token: Authorization: Bearer <token>
|
|
63
76
|
new AssinafyClient({ token: 'jwt_xxx', accountId: 'acc_xxx' });
|
|
64
77
|
```
|
|
65
78
|
|
|
79
|
+
Credentials are optional at construction time. A credentialless client uses a
|
|
80
|
+
separate, auth-free transport for public authentication and signer-access-code
|
|
81
|
+
operations, so an API key or Bearer token is never attached accidentally:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const publicClient = new AssinafyClient({
|
|
85
|
+
baseUrl: 'https://sandbox.assinafy.com.br/v1',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await publicClient.auth.login('me@example.com', 'password');
|
|
89
|
+
await publicClient.documents.getPublic(documentId);
|
|
90
|
+
await publicClient.signerDocuments.self(signerAccessCode);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Protected methods still require `apiKey` or `token`; the API returns its normal
|
|
94
|
+
`401` response if one is called without credentials.
|
|
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
|
+
|
|
66
101
|
## Configuration
|
|
67
102
|
|
|
68
103
|
| Option | Type | Default | Description |
|
|
69
104
|
| --------------- | -------- | --------------------------------------- | --------------------------------------------- |
|
|
70
105
|
| `apiKey` | string | — | Preferred credential (sent as `X-Api-Key`). |
|
|
71
|
-
| `token` | string | — |
|
|
106
|
+
| `token` | string | — | Access token (sent as `Authorization: Bearer`). |
|
|
72
107
|
| `accountId` | string | — | Default workspace/account ID. |
|
|
73
|
-
| `baseUrl` | string | `https://api.assinafy.com.br/v1` |
|
|
74
|
-
| `webhookSecret` | string | — |
|
|
108
|
+
| `baseUrl` | string | `https://api.assinafy.com.br/v1` | Absolute HTTP(S) API base without credentials, query, or fragment. |
|
|
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). |
|
|
75
110
|
| `timeout` | number | `30000` | Request timeout in milliseconds. |
|
|
76
|
-
| `maxRetries` | number | `2` | Auto-retries
|
|
111
|
+
| `maxRetries` | number | `2` | Auto-retries eligible HTTP 429 responses, honoring `Retry-After`. `0` disables. |
|
|
77
112
|
| `logger` | `Logger` | no-op | Optional `{debug,info,warn,error}` logger. |
|
|
78
113
|
|
|
79
114
|
### Rate limiting
|
|
80
115
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
116
|
+
On an HTTP `429`, the client automatically retries up to `maxRetries` times,
|
|
117
|
+
waiting for the server-provided `Retry-After` (or `X-Rate-Limit-Reset`) delay
|
|
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.
|
|
85
124
|
|
|
86
125
|
### Factories
|
|
87
126
|
|
|
88
127
|
```ts
|
|
89
128
|
// Positional factory
|
|
90
|
-
const client = AssinafyClient.create('api-key', 'account-id'
|
|
129
|
+
const client = AssinafyClient.create('api-key', 'account-id');
|
|
91
130
|
|
|
92
131
|
// From a plain object (accepts snake_case or camelCase keys)
|
|
93
132
|
const client = AssinafyClient.fromConfig({
|
|
@@ -98,7 +137,9 @@ const client = AssinafyClient.fromConfig({
|
|
|
98
137
|
|
|
99
138
|
## Endpoint coverage
|
|
100
139
|
|
|
101
|
-
|
|
140
|
+
All 89 operations documented at https://api.assinafy.com.br/v1/docs are
|
|
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).
|
|
102
143
|
|
|
103
144
|
| Resource | Endpoints |
|
|
104
145
|
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -107,16 +148,271 @@ Every public endpoint documented in https://api.assinafy.com.br/v1/docs is cover
|
|
|
107
148
|
| `client.assignments` | **list**, create, estimateCost, resetExpiration, resendNotification, estimateResendCost, listWhatsAppNotifications |
|
|
108
149
|
| `client.templates` | **create**, list, get, **update**, **delete**, downloadPage |
|
|
109
150
|
| `client.tags` | list, create, update, delete |
|
|
110
|
-
| `client.workspaces` | create, list, get, update, delete
|
|
151
|
+
| `client.workspaces` | create, list, get, update, delete, getTheme, downloadLogo, uploadLogo, deleteLogo, getStats |
|
|
111
152
|
| `client.webhooks` | register, get, inactivate, listEventTypes, listDispatches, retryDispatch |
|
|
112
153
|
| `client.fields` | create, list, get, update, delete, validate, validateMultiple, listTypes |
|
|
113
|
-
| `client.auth` | login, socialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword
|
|
154
|
+
| `client.auth` | getSocialLoginUrl, getSocialLoginCallbackUrl, login, socialLogin, linkSocialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword |
|
|
155
|
+
| `client.users` | getCurrent, getStats, getNotificationPreferences, updateNotificationPreferences |
|
|
114
156
|
| `client.signerDocuments` | getCurrent, list, **search**, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline |
|
|
115
157
|
| `client.webhookVerifier` | verify, extractEvent, getEventType, getEventData |
|
|
116
158
|
|
|
117
|
-
|
|
159
|
+
Every HTTP wrapper has TypeScript-checked request/response shapes and
|
|
160
|
+
method-level JSDoc covering the wire payload, return shape, validation,
|
|
161
|
+
relevant API errors, and a copyable example. Reusable and OpenAPI schema-level
|
|
162
|
+
payloads are exported as named types; small method-local option bags remain
|
|
163
|
+
inline in the generated declarations. Editors expose the reference on hover,
|
|
164
|
+
and declaration files ship with the package. The coverage ledger links those
|
|
165
|
+
typed methods back to each upstream operation without duplicating the schema.
|
|
166
|
+
|
|
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
|
+
```
|
|
118
403
|
|
|
119
|
-
|
|
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
|
|
412
|
+
|
|
413
|
+
Most account-scoped methods accept an optional `accountId` that overrides the
|
|
414
|
+
client default. Workspace `get`, `update`, `delete`, branding, and statistics
|
|
415
|
+
methods always require an explicit account ID.
|
|
120
416
|
|
|
121
417
|
### Documents
|
|
122
418
|
|
|
@@ -126,13 +422,15 @@ const doc = await client.documents.upload(
|
|
|
126
422
|
{ filePath: './contract.pdf' },
|
|
127
423
|
{ name: 'Service agreement', metadata: { type: 'service' } },
|
|
128
424
|
);
|
|
425
|
+
// `name` and `metadata` are compatibility multipart parts outside the published
|
|
426
|
+
// file-only request schema.
|
|
129
427
|
// `name` is optional and defaults to the file's own name. The API derives the
|
|
130
428
|
// display name from the uploaded filename and appends `.pdf` when absent, so
|
|
131
429
|
// the document above is stored as 'Service agreement.pdf'. Accents are
|
|
132
430
|
// transliterated by the API ('Contrato de Serviço' → 'Contrato de Servico.pdf').
|
|
133
431
|
// → {
|
|
134
432
|
// resource: 'document', id: '1031…', account_id: '102d…', template_id: null,
|
|
135
|
-
// name: '
|
|
433
|
+
// name: 'Service agreement.pdf', status: 'uploaded',
|
|
136
434
|
// artifacts: { original: 'https://…/download/original' },
|
|
137
435
|
// signing_url: 'https://app…/sign/1031…',
|
|
138
436
|
// pages: [], // populated once status reaches `metadata_ready`
|
|
@@ -143,7 +441,7 @@ const doc = await client.documents.upload(
|
|
|
143
441
|
await client.documents.upload({ buffer, fileName: 'contract.pdf' });
|
|
144
442
|
|
|
145
443
|
// List → { data: IDocumentListItem[], meta?: { current_page, per_page, total, last_page } }
|
|
146
|
-
const { data, meta } = await client.documents.list({ page: 1, per_page: 20, sort: '
|
|
444
|
+
const { data, meta } = await client.documents.list({ page: 1, per_page: 20, sort: 'updated_at' });
|
|
147
445
|
|
|
148
446
|
// Search is the lightweight alternative to list: same item shape, but the API
|
|
149
447
|
// skips the expanded `assignment`/`pages`. Prefer it for name lookups.
|
|
@@ -158,7 +456,11 @@ await client.documents.waitUntilReady(doc.id, { maxWaitMs: 30_000 });
|
|
|
158
456
|
// (Passing `name` to upload() avoids both the round-trip and the race.)
|
|
159
457
|
await client.documents.rename(doc.id, 'Signed service agreement.pdf');
|
|
160
458
|
|
|
161
|
-
await client.documents.download(doc.id, 'certificated'); //
|
|
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');
|
|
162
464
|
await client.documents.thumbnail(doc.id);
|
|
163
465
|
await client.documents.downloadPage(doc.id, pageId);
|
|
164
466
|
|
|
@@ -167,21 +469,37 @@ await client.documents.isFullySigned(doc.id);
|
|
|
167
469
|
await client.documents.getSigningProgress(doc.id);
|
|
168
470
|
await client.documents.delete(doc.id);
|
|
169
471
|
|
|
170
|
-
// Verify a signed document by its
|
|
472
|
+
// Verify a signed document by its Assinafy signature hash
|
|
171
473
|
await client.documents.verify('FE32EDDADE7CBDDCBB934E7402047450B0E59C02');
|
|
172
474
|
|
|
173
475
|
// Public endpoints (no auth)
|
|
174
476
|
await client.documents.getPublic(doc.id);
|
|
175
|
-
|
|
477
|
+
// Official request body: { email: 'jane@example.com' }
|
|
478
|
+
await client.documents.sendToken(doc.id, 'jane@example.com');
|
|
479
|
+
|
|
480
|
+
// Explicit compatibility overload for older deployments:
|
|
481
|
+
// { recipient: '+5548999990000', channel: 'whatsapp' }
|
|
482
|
+
await client.documents.sendToken(doc.id, '+5548999990000', 'whatsapp');
|
|
176
483
|
|
|
177
|
-
//
|
|
484
|
+
// The current OpenAPI contract requires existing tag IDs.
|
|
485
|
+
const contractsTag = await client.tags.create({ name: 'Contracts' });
|
|
486
|
+
const quarterTag = await client.tags.create({ name: '2026-Q1' });
|
|
487
|
+
const urgentTag = await client.tags.create({ name: 'Urgent' });
|
|
178
488
|
await client.documents.listTags(doc.id);
|
|
179
|
-
await client.documents.replaceTags(doc.id, [
|
|
180
|
-
await client.documents.addTags(doc.id, [
|
|
181
|
-
await client.documents.detachTag(doc.id,
|
|
489
|
+
await client.documents.replaceTags(doc.id, [contractsTag.id, quarterTag.id]); // [] detaches all
|
|
490
|
+
await client.documents.addTags(doc.id, [urgentTag.id]); // append
|
|
491
|
+
await client.documents.detachTag(doc.id, urgentTag.id); // → { detached: true }
|
|
182
492
|
```
|
|
183
493
|
|
|
184
|
-
Uploads are validated locally: only `.pdf` files up to 25 MB
|
|
494
|
+
Uploads are validated locally: only `.pdf` files up to 25 MB whose bytes begin
|
|
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.
|
|
185
503
|
|
|
186
504
|
List endpoints return `{ data, meta }` where `meta` is populated from the `X-Pagination-*` headers returned by the API.
|
|
187
505
|
|
|
@@ -191,35 +509,36 @@ List endpoints return `{ data, meta }` where `meta` is populated from the `X-Pag
|
|
|
191
509
|
await client.signers.create({
|
|
192
510
|
full_name: 'John Doe',
|
|
193
511
|
email: 'john@example.com',
|
|
194
|
-
|
|
195
|
-
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
|
|
196
513
|
});
|
|
197
514
|
// → { id: '19e6…', full_name: 'John Doe', email: 'john@example.com',
|
|
198
|
-
// whatsapp_phone_number:
|
|
515
|
+
// whatsapp_phone_number: null, has_accepted_terms: false }
|
|
199
516
|
// (note: `cpf` is accepted on input but never echoed back by the API)
|
|
200
517
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
whatsapp_phone_number: '+5548999990000',
|
|
205
|
-
});
|
|
518
|
+
// Both contacts are optional. A name-only signer cannot be notified until a
|
|
519
|
+
// contact is added.
|
|
520
|
+
await client.signers.create({ full_name: 'Contact Pending' });
|
|
206
521
|
|
|
207
|
-
// PHP SDK compatibility aliases are also accepted
|
|
208
522
|
await client.signers.create({
|
|
209
523
|
full_name: 'Jane Doe',
|
|
210
524
|
email: 'jane@example.com',
|
|
211
|
-
phone: '+5548999991111', // alias for whatsapp_phone_number
|
|
212
525
|
});
|
|
213
526
|
|
|
214
527
|
await client.signers.get(signerId);
|
|
215
528
|
await client.signers.list({ page: 1, per_page: 50, search: 'john' });
|
|
216
|
-
await client.signers.update(signerId, {
|
|
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
|
+
});
|
|
217
533
|
await client.signers.delete(signerId);
|
|
218
534
|
|
|
219
535
|
const existing = await client.signers.findByEmail('john@example.com');
|
|
220
536
|
```
|
|
221
537
|
|
|
222
|
-
When an `email` is supplied, `signers.create()` is idempotent by email
|
|
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.
|
|
223
542
|
|
|
224
543
|
### Assignments
|
|
225
544
|
|
|
@@ -233,8 +552,8 @@ await client.assignments.create(documentId, {
|
|
|
233
552
|
method: 'virtual',
|
|
234
553
|
signers: ['signer-1', 'signer-2'],
|
|
235
554
|
message: 'Please review and sign',
|
|
236
|
-
expires_at: '
|
|
237
|
-
copy_receivers: ['
|
|
555
|
+
expires_at: '2027-12-31T23:59:00Z',
|
|
556
|
+
copy_receivers: ['copy-recipient-signer-id'],
|
|
238
557
|
});
|
|
239
558
|
|
|
240
559
|
// Sequential signing: `step` controls signing order (parallel within a step).
|
|
@@ -246,31 +565,47 @@ await client.assignments.create(documentId, {
|
|
|
246
565
|
],
|
|
247
566
|
});
|
|
248
567
|
|
|
249
|
-
//
|
|
250
|
-
await client.assignments.
|
|
251
|
-
|
|
252
|
-
signers: [{
|
|
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
|
+
}],
|
|
253
583
|
});
|
|
584
|
+
|
|
585
|
+
// Estimate cost (the endpoint prices channel descriptors, not signer IDs) → ICostEstimate
|
|
586
|
+
await client.assignments.estimateCost(documentId, { signers: [{}] }); // default Email
|
|
254
587
|
// → {
|
|
255
588
|
// documents: 1, credits: 0, needs_extra_document: false, extra_document_cost: 0,
|
|
256
589
|
// total_credits: 0, breakdown: [], document_balance: 67, credit_balance: 0,
|
|
257
590
|
// has_sufficient_resources: true, blocking_reason: null, message: null
|
|
258
591
|
// }
|
|
259
592
|
|
|
260
|
-
await client.assignments.resetExpiration(documentId, assignmentId, '
|
|
261
|
-
|
|
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);
|
|
262
597
|
|
|
263
598
|
await client.assignments.resendNotification(documentId, assignmentId, signerId);
|
|
264
599
|
// → { is_sent: true, document_id: '…', signer_id: '…' }
|
|
265
600
|
|
|
266
|
-
await client.assignments.estimateResendCost(documentId, assignmentId, signerId);
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
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.
|
|
271
605
|
```
|
|
272
606
|
|
|
273
|
-
The `create` response is an `IAssignment`: `{ id, method, signers: [...],
|
|
607
|
+
The `create` response is an `IAssignment`: `{ id, method, signers: [...],
|
|
608
|
+
items: [{ display_settings, ... }], signing_urls: [{ signer_id, url }], … }`.
|
|
274
609
|
|
|
275
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.
|
|
276
611
|
|
|
@@ -281,11 +616,96 @@ await client.documents.delete(documentId); // w
|
|
|
281
616
|
await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'No longer needed'); // signer-side
|
|
282
617
|
```
|
|
283
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
|
+
|
|
284
697
|
### Templates
|
|
285
698
|
|
|
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.
|
|
705
|
+
|
|
286
706
|
```ts
|
|
287
707
|
// Create a template by uploading a PDF (multipart). The template starts in
|
|
288
|
-
//
|
|
708
|
+
// an uploaded state and becomes ready once its pages are processed.
|
|
289
709
|
const created = await client.templates.create(
|
|
290
710
|
{ filePath: './nda.pdf' }, // or { buffer, fileName: 'nda.pdf' }
|
|
291
711
|
{ name: 'NDA template' },
|
|
@@ -301,24 +721,40 @@ const created = await client.templates.create(
|
|
|
301
721
|
const { data, meta } = await client.templates.list({ search: 'NDA', per_page: 20 });
|
|
302
722
|
const template = await client.templates.get(created.id); // includes pages[] + default_document_tags
|
|
303
723
|
await client.templates.update(created.id, { name: 'NDA v2', message: 'Please sign' });
|
|
304
|
-
|
|
724
|
+
const firstPage = template.pages?.[0];
|
|
725
|
+
if (firstPage) await client.templates.downloadPage(created.id, firstPage.id); // → Buffer (JPEG)
|
|
305
726
|
await client.templates.delete(created.id);
|
|
306
727
|
|
|
307
|
-
// Create a
|
|
728
|
+
// Create a document from an existing, configured template. Fresh uploads have
|
|
729
|
+
// only an Editor role; add signer roles in Assinafy's editor first.
|
|
730
|
+
const configured = await client.templates.get(templateId);
|
|
731
|
+
const signerRole = configured.roles?.find(
|
|
732
|
+
(role) => typeof role.assignment_type === 'string'
|
|
733
|
+
&& role.assignment_type.toLowerCase() !== 'editor',
|
|
734
|
+
);
|
|
735
|
+
if (!signerRole) throw new Error('Template has no signer role');
|
|
308
736
|
await client.documents.createFromTemplate(
|
|
309
737
|
templateId,
|
|
310
|
-
[{ role_id:
|
|
738
|
+
[{ role_id: signerRole.id, id: signerId, verification_method: 'Email', notification_methods: ['Email'] }],
|
|
311
739
|
{ name: 'NDA - John Doe', message: 'Please sign at your earliest convenience.' },
|
|
312
740
|
);
|
|
313
741
|
|
|
314
742
|
// Estimate the cost before creating → ICostEstimate
|
|
315
|
-
await client.documents.estimateCostFromTemplate(templateId, [
|
|
743
|
+
await client.documents.estimateCostFromTemplate(templateId, [
|
|
744
|
+
{ role_id: 'role_id', verification_method: 'Email', notification_methods: ['Email'] },
|
|
745
|
+
]);
|
|
316
746
|
// → { documents: 1, total_credits: 0, document_balance: 67, credit_balance: 0,
|
|
317
747
|
// has_sufficient_resources: true, blocking_reason: null, breakdown: [], … }
|
|
318
748
|
```
|
|
319
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
|
+
|
|
320
754
|
Template creation only uploads the PDF and provisions the default editor role —
|
|
321
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.
|
|
322
758
|
|
|
323
759
|
### Tags
|
|
324
760
|
|
|
@@ -337,12 +773,50 @@ Attach/detach tags on a specific document via `client.documents.listTags / repla
|
|
|
337
773
|
|
|
338
774
|
### Workspaces
|
|
339
775
|
|
|
776
|
+
The official create/update request schemas define `name` and
|
|
777
|
+
`notification_sender_type`. The sandbox also accepts the color fields shown
|
|
778
|
+
below; they are retained as a documented compatibility extension.
|
|
779
|
+
|
|
340
780
|
```ts
|
|
341
|
-
|
|
781
|
+
// Colours are 6-char hex WITHOUT a leading '#' (unlike tags, which strip it).
|
|
782
|
+
// '#ff0066' is rejected — the account endpoints want exactly 6 characters.
|
|
783
|
+
await client.workspaces.create({
|
|
784
|
+
name: 'My Workspace',
|
|
785
|
+
notification_sender_type: 'Account',
|
|
786
|
+
primary_color: 'ff0066',
|
|
787
|
+
secondary_color: '0066ff',
|
|
788
|
+
});
|
|
789
|
+
// → { id, name, primary_color: 'ff0066', secondary_color: '0066ff', created_at }
|
|
342
790
|
await client.workspaces.list();
|
|
343
791
|
await client.workspaces.get(accountId);
|
|
344
|
-
await client.workspaces.update(accountId, {
|
|
792
|
+
await client.workspaces.update(accountId, {
|
|
793
|
+
name: 'Renamed',
|
|
794
|
+
notification_sender_type: 'User',
|
|
795
|
+
primary_color: '112233',
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// Branding
|
|
799
|
+
const theme = await client.workspaces.getTheme(accountId);
|
|
800
|
+
const logo = await client.workspaces.downloadLogo(accountId); // Buffer
|
|
801
|
+
await client.workspaces.uploadLogo(accountId, { filePath: './logo.png' });
|
|
802
|
+
await client.workspaces.uploadLogo(accountId, {
|
|
803
|
+
buffer: logoBuffer,
|
|
804
|
+
fileName: 'logo.png',
|
|
805
|
+
contentType: 'image/png',
|
|
806
|
+
});
|
|
807
|
+
await client.workspaces.deleteLogo(accountId);
|
|
808
|
+
|
|
809
|
+
// Latest 12 months by default; daily statistics require a YYYY-MM month.
|
|
810
|
+
await client.workspaces.getStats(accountId);
|
|
811
|
+
await client.workspaces.getStats(accountId, {
|
|
812
|
+
granularity: 'daily',
|
|
813
|
+
month: '2026-06',
|
|
814
|
+
});
|
|
815
|
+
|
|
345
816
|
await client.workspaces.delete(accountId);
|
|
817
|
+
// `force` cancels an active paid subscription as part of account deletion. It
|
|
818
|
+
// is not a general bypass for unrelated deletion restrictions.
|
|
819
|
+
await client.workspaces.delete(restrictedAccountId, { force: true });
|
|
346
820
|
```
|
|
347
821
|
|
|
348
822
|
### Field definitions
|
|
@@ -363,7 +837,7 @@ await client.fields.validate(fieldId, '400.676.228-36', { signerAccessCode });
|
|
|
363
837
|
await client.fields.validateMultiple(
|
|
364
838
|
[
|
|
365
839
|
{ field_id: 'f1', value: '1111111111111' },
|
|
366
|
-
{ field_id: 'f2', value: '
|
|
840
|
+
{ field_id: 'f2', value: 'value@example.com' },
|
|
367
841
|
],
|
|
368
842
|
{ signerAccessCode },
|
|
369
843
|
);
|
|
@@ -377,8 +851,14 @@ await client.fields.listTypes();
|
|
|
377
851
|
Most server-side integrations should just use `X-Api-Key` directly. Use these endpoints when you need to bootstrap a session for a human user.
|
|
378
852
|
|
|
379
853
|
```ts
|
|
854
|
+
// Browser OAuth: redirect the user to this URL. The callback helper returns the
|
|
855
|
+
// Assinafy callback URL for provider configuration; neither follows a redirect.
|
|
856
|
+
const oauthStart = client.auth.getSocialLoginUrl('google');
|
|
857
|
+
const oauthCallback = client.auth.getSocialLoginCallbackUrl();
|
|
858
|
+
|
|
380
859
|
const { access_token, user, accounts } = await client.auth.login('me@example.com', 'pw');
|
|
381
860
|
await client.auth.socialLogin({ provider: 'google', token: 'google-id-token', has_accepted_terms: true });
|
|
861
|
+
await client.auth.linkSocialLogin({ provider: 'google', token: 'google-id-token' });
|
|
382
862
|
|
|
383
863
|
// Personal API key
|
|
384
864
|
await client.auth.createApiKey('current-password');
|
|
@@ -391,12 +871,39 @@ await client.auth.requestPasswordReset('me@example.com');
|
|
|
391
871
|
await client.auth.resetPassword({ email, token: 'tk', new_password: 'next' });
|
|
392
872
|
```
|
|
393
873
|
|
|
874
|
+
### Authenticated user
|
|
875
|
+
|
|
876
|
+
```ts
|
|
877
|
+
const user = await client.users.getCurrent();
|
|
878
|
+
// → { id, name, email, telephone, government_id, is_email_verified,
|
|
879
|
+
// has_accepted_terms, created_at, to_be_deleted_at }
|
|
880
|
+
|
|
881
|
+
// Cross-account document funnel, latest 12 monthly periods by default.
|
|
882
|
+
const monthly = await client.users.getStats();
|
|
883
|
+
const daily = await client.users.getStats({
|
|
884
|
+
granularity: 'daily',
|
|
885
|
+
month: '2026-06',
|
|
886
|
+
});
|
|
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.
|
|
898
|
+
```
|
|
899
|
+
|
|
394
900
|
### Webhooks
|
|
395
901
|
|
|
396
902
|
```ts
|
|
397
903
|
await client.webhooks.register({
|
|
398
904
|
url: 'https://example.com/webhooks/assinafy',
|
|
399
905
|
email: 'admin@example.com',
|
|
906
|
+
is_active: true,
|
|
400
907
|
// events defaults to the current SDK default set below
|
|
401
908
|
events: [
|
|
402
909
|
'document_ready',
|
|
@@ -407,31 +914,122 @@ await client.webhooks.register({
|
|
|
407
914
|
],
|
|
408
915
|
});
|
|
409
916
|
|
|
410
|
-
await client.webhooks.get(); //
|
|
917
|
+
await client.webhooks.get(); // IWebhookSubscription | null
|
|
411
918
|
await client.webhooks.inactivate(); // stop deliveries (no delete route exists)
|
|
412
919
|
await client.webhooks.listEventTypes();
|
|
413
|
-
await client.webhooks.listDispatches({
|
|
414
|
-
|
|
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
|
|
415
926
|
```
|
|
416
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
|
+
|
|
417
1002
|
### Webhook verification
|
|
418
1003
|
|
|
419
|
-
|
|
1004
|
+
`WebhookVerifier` is an opt-in HMAC-SHA256 utility for integrations whose
|
|
1005
|
+
Assinafy environment provides a shared secret and signature header. The
|
|
1006
|
+
current official OpenAPI document does **not** define a webhook signature
|
|
1007
|
+
scheme or header name. Confirm the delivery contract for your environment
|
|
1008
|
+
before enabling this check; do not reject production callbacks based on an
|
|
1009
|
+
assumed header. The example below uses an application-configured header name.
|
|
420
1010
|
|
|
421
1011
|
```ts
|
|
422
1012
|
import express from 'express';
|
|
423
1013
|
|
|
1014
|
+
const webhookSecret = process.env.ASSINAFY_WEBHOOK_SECRET;
|
|
1015
|
+
const signatureHeader = process.env.ASSINAFY_SIGNATURE_HEADER;
|
|
1016
|
+
if (!webhookSecret || !signatureHeader) {
|
|
1017
|
+
throw new Error('This deployment has no confirmed webhook-signature contract');
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const webhookClient = new AssinafyClient({ webhookSecret });
|
|
1021
|
+
|
|
424
1022
|
app.post('/webhooks/assinafy', express.raw({ type: 'application/json' }), (req, res) => {
|
|
425
|
-
const signature = req.header(
|
|
1023
|
+
const signature = req.header(signatureHeader) ?? '';
|
|
426
1024
|
const rawBody = req.body as Buffer;
|
|
427
1025
|
|
|
428
|
-
if (!
|
|
1026
|
+
if (!webhookClient.webhookVerifier.verify(rawBody, signature)) {
|
|
429
1027
|
return res.status(401).send('Invalid signature');
|
|
430
1028
|
}
|
|
431
1029
|
|
|
432
|
-
const event =
|
|
433
|
-
const type =
|
|
434
|
-
const data =
|
|
1030
|
+
const event = webhookClient.webhookVerifier.extractEvent(rawBody);
|
|
1031
|
+
const type = webhookClient.webhookVerifier.getEventType(event);
|
|
1032
|
+
const data = webhookClient.webhookVerifier.getEventData(event);
|
|
435
1033
|
|
|
436
1034
|
switch (type) {
|
|
437
1035
|
case 'document_ready': break;
|
|
@@ -445,66 +1043,98 @@ app.post('/webhooks/assinafy', express.raw({ type: 'application/json' }), (req,
|
|
|
445
1043
|
|
|
446
1044
|
### Signer-side endpoints
|
|
447
1045
|
|
|
448
|
-
For building custom signer portals.
|
|
1046
|
+
For building custom signer portals. Most calls require the `signer-access-code`
|
|
1047
|
+
URL parameter that Assinafy emails/whatsapps to the signer. Artifact download is
|
|
1048
|
+
the documented public exception; its optional fourth access-code argument exists
|
|
1049
|
+
only for compatibility with deployments that still expect the legacy query.
|
|
449
1050
|
|
|
450
1051
|
```ts
|
|
451
1052
|
await client.signerDocuments.self(accessCode);
|
|
452
|
-
await client.signerDocuments.acceptTerms(accessCode);
|
|
453
1053
|
await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verificationCode: '123456' });
|
|
454
1054
|
|
|
455
1055
|
await client.signerDocuments.getCurrent(signerId, accessCode);
|
|
456
|
-
const { data } = await client.signerDocuments.list(signerId, accessCode, {
|
|
1056
|
+
const { data } = await client.signerDocuments.list(signerId, accessCode, { per_page: 20 });
|
|
457
1057
|
// Signer-side counterpart of documents.search(), authorised by the access code.
|
|
458
1058
|
const found = await client.signerDocuments.search(signerId, accessCode, 'invoice');
|
|
459
|
-
await client.signerDocuments.download(signerId, documentId, 'original'
|
|
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');
|
|
460
1062
|
|
|
461
1063
|
await client.signerDocuments.confirmData(documentId, accessCode, {
|
|
462
1064
|
email: 'me@example.com',
|
|
463
|
-
|
|
1065
|
+
full_name: 'Example Signer',
|
|
1066
|
+
government_id: '123.456.789-00',
|
|
464
1067
|
has_accepted_terms: true,
|
|
465
1068
|
});
|
|
1069
|
+
// Alternatively, accept terms separately before getAssignment():
|
|
1070
|
+
// await client.signerDocuments.acceptTerms(accessCode);
|
|
466
1071
|
|
|
467
|
-
// Signature image management
|
|
468
|
-
await client.signerDocuments.uploadSignature(accessCode, pngBuffer, { imageType: 'signature' });
|
|
1072
|
+
// Signature image management ({ reuse: true } persists it for future documents)
|
|
1073
|
+
await client.signerDocuments.uploadSignature(accessCode, pngBuffer, { imageType: 'signature', reuse: true });
|
|
469
1074
|
await client.signerDocuments.downloadSignature(accessCode, 'signature');
|
|
470
1075
|
|
|
471
1076
|
// Sign / decline
|
|
472
|
-
const
|
|
1077
|
+
const signable = await client.signerDocuments.getAssignment(accessCode);
|
|
1078
|
+
// `sign()` is for collect assignments and requires every placed field value.
|
|
473
1079
|
await client.signerDocuments.sign(documentId, assignmentId, accessCode, [
|
|
474
1080
|
{ itemId, fieldId, pageId, value: 'Signed by John' },
|
|
475
1081
|
]);
|
|
476
1082
|
await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'Not authorized');
|
|
477
1083
|
|
|
478
|
-
//
|
|
1084
|
+
// `signMultiple()` is for virtual assignments only.
|
|
479
1085
|
await client.signerDocuments.signMultiple(['doc-1', 'doc-2'], accessCode);
|
|
480
1086
|
await client.signerDocuments.declineMultiple(['doc-1'], 'Unfavorable terms', accessCode);
|
|
481
1087
|
```
|
|
482
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
|
+
|
|
483
1093
|
## High-level helper
|
|
484
1094
|
|
|
485
|
-
Uploads a PDF,
|
|
1095
|
+
Uploads a PDF, reuses or creates signers by email, creates a virtual assignment
|
|
1096
|
+
immediately, and optionally waits for processing before returning.
|
|
486
1097
|
|
|
487
1098
|
```ts
|
|
488
1099
|
const result = await client.uploadAndRequestSignatures({
|
|
489
1100
|
source: { filePath: './contract.pdf' },
|
|
490
1101
|
signers: [
|
|
491
1102
|
{ name: 'John', email: 'john@example.com' },
|
|
492
|
-
{ name: 'Jane', email: 'jane@example.com'
|
|
1103
|
+
{ name: 'Jane', email: 'jane@example.com' },
|
|
493
1104
|
],
|
|
494
1105
|
message: 'Please sign',
|
|
495
|
-
metadata: { year: 2026 },
|
|
1106
|
+
metadata: { year: 2026 }, // compatibility upload part; omit for file-only wire format
|
|
496
1107
|
waitForReady: true,
|
|
497
|
-
|
|
1108
|
+
waitOptions: { maxWaitMs: 30_000, pollIntervalMs: 1_000 },
|
|
1109
|
+
expiresAt: '2027-12-31T00:00:00Z',
|
|
1110
|
+
copyReceivers: ['existing-copy-recipient-signer-id'],
|
|
498
1111
|
});
|
|
499
1112
|
|
|
500
|
-
result.document; //
|
|
1113
|
+
result.document; // fully-processed IDocumentDetailsResponse (waitForReady: true, the default);
|
|
1114
|
+
// the raw IDocumentUploadResponse when waitForReady: false
|
|
501
1115
|
result.assignment; // IAssignment
|
|
502
1116
|
result.signer_ids; // string[]
|
|
503
1117
|
```
|
|
504
1118
|
|
|
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.
|
|
1133
|
+
|
|
505
1134
|
## Errors
|
|
506
1135
|
|
|
507
|
-
|
|
1136
|
+
HTTP methods reject with an `AssinafyError` subclass. Synchronous helpers such
|
|
1137
|
+
as `getSocialLoginUrl()` can throw `ValidationError` before any request.
|
|
508
1138
|
|
|
509
1139
|
```ts
|
|
510
1140
|
import { ApiError, ValidationError, NetworkError, AssinafyError } from '@assinafy/sdk';
|
|
@@ -524,27 +1154,17 @@ try {
|
|
|
524
1154
|
}
|
|
525
1155
|
```
|
|
526
1156
|
|
|
527
|
-
## Live smoke test
|
|
528
|
-
|
|
529
|
-
A real-network test script under [`scripts/live-smoke.ts`](scripts/live-smoke.ts) exercises the full API. Use it to sanity-check a workspace before shipping.
|
|
530
|
-
|
|
531
|
-
```bash
|
|
532
|
-
ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts # read-only
|
|
533
|
-
ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --write # also creates+deletes a signer
|
|
534
|
-
ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --upload # also uploads a PDF + a template, then deletes both
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
Set `ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1` to run it against the
|
|
538
|
-
sandbox instead of production.
|
|
539
|
-
|
|
540
1157
|
## Development
|
|
541
1158
|
|
|
542
1159
|
```bash
|
|
543
|
-
bun install
|
|
544
|
-
bun
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1160
|
+
bun install --frozen-lockfile
|
|
1161
|
+
bun run typecheck # source, script, and test type checks
|
|
1162
|
+
bun run lint
|
|
1163
|
+
bun test # bun:test suites
|
|
1164
|
+
bun run test:coverage
|
|
1165
|
+
bun run build # tsup → dist/ (CJS + ESM + .d.ts)
|
|
1166
|
+
bun run lint:pkg # publint + arethetypeswrong
|
|
1167
|
+
bun run verify # complete local release gate
|
|
548
1168
|
```
|
|
549
1169
|
|
|
550
1170
|
## License
|