@assinafy/sdk 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -3,16 +3,16 @@ import axios2 from "axios";
3
3
 
4
4
  // src/errors.ts
5
5
  var AssinafyError = class extends Error {
6
+ context;
6
7
  constructor(message, context = {}, options) {
7
- super(message);
8
+ super(message, options);
8
9
  this.name = "AssinafyError";
9
10
  this.context = context;
10
- if (options?.cause !== void 0) {
11
- this.cause = options.cause;
12
- }
13
11
  }
14
12
  };
15
13
  var ApiError = class _ApiError extends AssinafyError {
14
+ statusCode;
15
+ responseData;
16
16
  constructor(message, statusCode, responseData = null, options) {
17
17
  super(message, { statusCode, responseData }, options);
18
18
  this.name = "ApiError";
@@ -28,6 +28,7 @@ var ApiError = class _ApiError extends AssinafyError {
28
28
  }
29
29
  };
30
30
  var ValidationError = class extends AssinafyError {
31
+ errors;
31
32
  constructor(message = "Validation failed", errors = {}) {
32
33
  super(message, { errors });
33
34
  this.name = "ValidationError";
@@ -53,6 +54,23 @@ function handleAssinafyResponse(response) {
53
54
  }
54
55
  return response;
55
56
  }
57
+ function decodeBinaryErrorBody(data) {
58
+ let text;
59
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
60
+ text = data.toString("utf8");
61
+ } else if (data instanceof ArrayBuffer) {
62
+ text = Buffer.from(data).toString("utf8");
63
+ } else if (ArrayBuffer.isView(data)) {
64
+ text = Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
65
+ } else {
66
+ return data;
67
+ }
68
+ try {
69
+ return JSON.parse(text);
70
+ } catch {
71
+ return text.length > 0 ? { message: text } : null;
72
+ }
73
+ }
56
74
  function toSdkError(error, fallbackMessage) {
57
75
  if (error instanceof AssinafyError) {
58
76
  return error;
@@ -60,14 +78,15 @@ function toSdkError(error, fallbackMessage) {
60
78
  if (axios.isAxiosError(error)) {
61
79
  const status = error.response?.status;
62
80
  if (status) {
63
- return ApiError.fromResponse(status, error.response?.data ?? null);
81
+ const body = decodeBinaryErrorBody(error.response?.data ?? null);
82
+ return ApiError.fromResponse(status, body ?? null);
64
83
  }
65
84
  return new NetworkError(`${fallbackMessage}: ${error.message}`, { cause: error });
66
85
  }
67
86
  if (error instanceof Error) {
68
87
  return new AssinafyError(`${fallbackMessage}: ${error.message}`, {}, { cause: error });
69
88
  }
70
- return new AssinafyError(fallbackMessage, { cause: error });
89
+ return new AssinafyError(fallbackMessage, {}, { cause: error });
71
90
  }
72
91
  function createNoopLogger() {
73
92
  return {
@@ -86,12 +105,101 @@ function cleanParams(params) {
86
105
  }
87
106
  return out;
88
107
  }
108
+ function cleanListParams(params) {
109
+ const out = cleanParams(params);
110
+ if (out["per_page"] !== void 0) {
111
+ out["per-page"] ??= out["per_page"];
112
+ delete out["per_page"];
113
+ }
114
+ return out;
115
+ }
89
116
 
90
- // src/resources/documents.ts
117
+ // src/support/retry.ts
118
+ function header(headers, name) {
119
+ if (!headers) return void 0;
120
+ const lower = name.toLowerCase();
121
+ for (const [key, value] of Object.entries(headers)) {
122
+ if (key.toLowerCase() === lower && value != null) {
123
+ return Array.isArray(value) ? String(value[0]) : String(value);
124
+ }
125
+ }
126
+ return void 0;
127
+ }
128
+ function retryDelayFromHeaders(headers) {
129
+ const retryAfter = header(headers, "retry-after");
130
+ if (retryAfter !== void 0) {
131
+ const seconds = Number(retryAfter);
132
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
133
+ const date = Date.parse(retryAfter);
134
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
135
+ }
136
+ const reset = header(headers, "x-rate-limit-reset");
137
+ if (reset !== void 0) {
138
+ const seconds = Number(reset);
139
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
140
+ }
141
+ return void 0;
142
+ }
143
+ function backoffMs(attempt) {
144
+ return Math.min(1e3 * 2 ** Math.max(0, attempt - 1), 8e3);
145
+ }
146
+ function nextRetryDelayMs(headers, attempt, maxDelayMs = 3e4) {
147
+ const hinted = retryDelayFromHeaders(headers);
148
+ if (hinted !== void 0) return Math.min(hinted, maxDelayMs);
149
+ return Math.min(backoffMs(attempt), maxDelayMs);
150
+ }
151
+
152
+ // src/resources/upload.ts
91
153
  import { promises as fs } from "fs";
92
154
  import path from "path";
155
+ var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
156
+ async function loadSource(source) {
157
+ if ("buffer" in source) {
158
+ if (!source.fileName) {
159
+ throw new ValidationError("fileName is required when uploading a Buffer");
160
+ }
161
+ return { buffer: source.buffer, fileName: source.fileName };
162
+ }
163
+ if (!source.filePath) {
164
+ throw new ValidationError("filePath is required");
165
+ }
166
+ const buffer = await fs.readFile(source.filePath);
167
+ return { buffer, fileName: source.fileName ?? path.basename(source.filePath) };
168
+ }
169
+ function validateUpload(buffer, fileName) {
170
+ if (!buffer || buffer.byteLength === 0) {
171
+ throw new ValidationError("File buffer is empty", { fileName });
172
+ }
173
+ if (!fileName.toLowerCase().endsWith(".pdf")) {
174
+ throw new ValidationError("Only PDF files are supported", { fileName });
175
+ }
176
+ if (buffer.byteLength > MAX_UPLOAD_BYTES) {
177
+ throw new ValidationError("File size exceeds maximum allowed (25MB)", {
178
+ fileSize: buffer.byteLength,
179
+ maxSize: MAX_UPLOAD_BYTES
180
+ });
181
+ }
182
+ }
183
+ function toUploadFileName(name) {
184
+ return name.toLowerCase().endsWith(".pdf") ? name : `${name}.pdf`;
185
+ }
186
+ function buildUploadForm(buffer, fileName, options = {}) {
187
+ const form = new FormData();
188
+ const view = new Uint8Array(
189
+ buffer.buffer,
190
+ buffer.byteOffset,
191
+ buffer.byteLength
192
+ );
193
+ const partName = options.name === void 0 ? fileName : toUploadFileName(options.name);
194
+ form.append("file", new Blob([view], { type: "application/pdf" }), partName);
195
+ if (options.metadata) {
196
+ form.append("metadata", JSON.stringify(options.metadata));
197
+ }
198
+ return form;
199
+ }
93
200
 
94
201
  // src/resources/base.ts
202
+ var MULTIPART_CONTENT_TYPE = "multipart/form-data";
95
203
  var BaseResource = class {
96
204
  constructor(http, defaultAccountId, logger = createNoopLogger()) {
97
205
  this.http = http;
@@ -153,6 +261,35 @@ var BaseResource = class {
153
261
  throw toSdkError(err, label);
154
262
  }
155
263
  }
264
+ /**
265
+ * Upload a PDF as `multipart/form-data` and assert the API echoed an id.
266
+ *
267
+ * Shared by `documents.upload` and `templates.create`, which are the same
268
+ * sequence over different paths: load → validate → build form → POST →
269
+ * assert an id came back. Callers keep their own success logging.
270
+ *
271
+ * @param path - Account-scoped endpoint to POST to.
272
+ * @param source - The PDF, as a file path or in-memory buffer.
273
+ * @param formOptions - `name` (display name) and optional `metadata`.
274
+ * @param labels - `errorLabel` for the request failure, `missingId` for a
275
+ * `2xx` that returned no id.
276
+ */
277
+ async uploadPdf(path2, source, formOptions, labels) {
278
+ const { buffer, fileName } = await loadSource(source);
279
+ validateUpload(buffer, fileName);
280
+ this.logger.info("Uploading PDF", { path: path2, fileName, size: buffer.byteLength });
281
+ const form = buildUploadForm(buffer, fileName, formOptions);
282
+ const result = await this.call(
283
+ labels.errorLabel,
284
+ () => this.http.post(path2, form, { headers: { "Content-Type": MULTIPART_CONTENT_TYPE } })
285
+ );
286
+ if (!result?.id) {
287
+ throw new ValidationError(labels.missingId, {
288
+ response: result
289
+ });
290
+ }
291
+ return result;
292
+ }
156
293
  /** Execute a paginated list call and attach meta from `X-Pagination-*` headers. */
157
294
  async callList(label, request) {
158
295
  try {
@@ -195,7 +332,6 @@ function toInt(value) {
195
332
  }
196
333
 
197
334
  // src/resources/documents.ts
198
- var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
199
335
  var READY_STATUSES = /* @__PURE__ */ new Set([
200
336
  "metadata_ready",
201
337
  "pending_signature",
@@ -209,43 +345,153 @@ var FAILED_STATUSES = /* @__PURE__ */ new Set([
209
345
  ]);
210
346
  var DocumentResource = class extends BaseResource {
211
347
  /**
212
- * Upload a PDF to the workspace.
348
+ * Upload a PDF to the workspace (`POST /accounts/{accountId}/documents`).
349
+ *
350
+ * The document is created in `metadata_processing` status and becomes
351
+ * usable once it reaches `metadata_ready`; use
352
+ * {@link DocumentResource.waitUntilReady} to await that transition. Note
353
+ * that {@link DocumentResource.rename} and {@link DocumentResource.delete}
354
+ * return `400` while the document is still processing.
355
+ *
356
+ * @param source - The PDF to upload, as a file path or an in-memory buffer.
357
+ * @param options - Display name, metadata, and account override.
358
+ * @returns The created document. Response shape:
359
+ * ```jsonc
360
+ * {
361
+ * "resource": "document",
362
+ * "id": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
363
+ * "name": "Service agreement.pdf",
364
+ * "status": "metadata_processing",
365
+ * "created_at": "2026-07-15T16:15:33Z",
366
+ * "updated_at": "2026-07-15T16:15:33Z"
367
+ * }
368
+ * ```
369
+ * @throws {ValidationError} If the file is empty, not a `.pdf`, exceeds
370
+ * 25 MB, or the API returns no document ID.
371
+ * @throws {ApiError} If the API rejects the upload.
213
372
  *
214
373
  * @example
215
374
  * ```ts
216
375
  * await client.documents.upload({ filePath: './contract.pdf' });
217
- * await client.documents.upload({ buffer, fileName: 'contract.pdf' }, { metadata });
376
+ * await client.documents.upload(
377
+ * { buffer, fileName: 'contract.pdf' },
378
+ * { name: 'Service agreement', metadata: { orderId: 'A-1' } },
379
+ * );
380
+ * // → name is stored as 'Service agreement.pdf'
218
381
  * ```
219
382
  */
220
383
  async upload(source, options = {}) {
221
- const { buffer, fileName } = await loadSource(source);
222
- validateUpload(buffer, fileName);
223
384
  const accountId = this.accountId(options.accountId);
224
- const form = buildUploadForm(buffer, fileName, options.metadata);
225
- this.logger.info("Uploading document", { fileName, size: buffer.byteLength });
226
- const document = await this.call(
227
- "Document upload failed",
228
- () => this.http.post(`/accounts/${accountId}/documents`, form, {
229
- headers: { "Content-Type": "multipart/form-data" }
230
- })
385
+ const formOptions = {};
386
+ if (options.name !== void 0) formOptions.name = options.name;
387
+ if (options.metadata !== void 0) formOptions.metadata = options.metadata;
388
+ const document = await this.uploadPdf(
389
+ `/accounts/${accountId}/documents`,
390
+ source,
391
+ formOptions,
392
+ {
393
+ errorLabel: "Document upload failed",
394
+ missingId: "Upload succeeded but no document ID was returned"
395
+ }
231
396
  );
232
- if (!document?.id) {
233
- throw new ValidationError("Upload succeeded but no document ID was returned", {
234
- response: document
235
- });
236
- }
237
397
  this.logger.info("Document uploaded", { documentId: document.id });
238
398
  return document;
239
399
  }
240
400
  /**
241
401
  * List workspace documents. Pagination info (if any) is attached in `meta`.
242
- * Supports `status`, `method`, `tags`, `search`, `sort`, `page`, `per_page`.
402
+ * Supports `status`, `method`, `tags`, `search`, `sort`, `page`, `per-page`.
243
403
  */
244
404
  async list(params = {}, accountId) {
245
405
  const id = this.accountId(accountId);
246
406
  return this.callList(
247
407
  "Failed to list documents",
248
- () => this.http.get(`/accounts/${id}/documents`, { params: cleanParams(params) })
408
+ () => this.http.get(`/accounts/${id}/documents`, { params: cleanListParams(params) })
409
+ );
410
+ }
411
+ /**
412
+ * Search workspace documents
413
+ * (`GET /accounts/{accountId}/documents/search`).
414
+ *
415
+ * A lighter-weight alternative to {@link DocumentResource.list}: it returns
416
+ * a compact representation with no expanded `assignment` or `pages`, so
417
+ * prefer it for name lookups and pickers.
418
+ *
419
+ * @param params - `search`, `status`, `page`, `per-page`.
420
+ * @param accountId - Override the client's default account ID.
421
+ * @returns Matching documents, with pagination in `meta`. Each item:
422
+ * ```jsonc
423
+ * {
424
+ * "id": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
425
+ * "account_id": "d4e5f6a7b8c9d0e1f2a3b4c5d6e7",
426
+ * "template_id": null,
427
+ * "name": "Service agreement.pdf",
428
+ * "status": "pending_signature",
429
+ * "artifacts": { "original": "https://…" },
430
+ * "is_closed": false,
431
+ * "signing_url": "https://…",
432
+ * "decline_reason": null,
433
+ * "declined_by": null,
434
+ * "tags": [],
435
+ * "created_at": "2026-07-15T16:15:33Z",
436
+ * "updated_at": "2026-07-15T16:15:40Z"
437
+ * }
438
+ * ```
439
+ * @throws {ValidationError} If no account ID is available.
440
+ * @throws {ApiError} If the API rejects the request.
441
+ *
442
+ * @example
443
+ * ```ts
444
+ * const { data, meta } = await client.documents.search({
445
+ * search: 'agreement',
446
+ * status: 'pending_signature',
447
+ * 'per-page': 20,
448
+ * });
449
+ * ```
450
+ */
451
+ async search(params = {}, accountId) {
452
+ const id = this.accountId(accountId);
453
+ return this.callList(
454
+ "Failed to search documents",
455
+ () => this.http.get(`/accounts/${id}/documents/search`, { params: cleanListParams(params) })
456
+ );
457
+ }
458
+ /**
459
+ * Rename a document (`PATCH /documents/{documentId}`).
460
+ *
461
+ * Only valid while the document is still renameable: the API returns `400`
462
+ * ("Document cannot be renamed after the signature process has started")
463
+ * both once signing has begun **and** while the document is still in
464
+ * `metadata_processing` immediately after upload. Await
465
+ * {@link DocumentResource.waitUntilReady} before renaming a fresh upload.
466
+ *
467
+ * To set a name at upload time instead, pass `name` to
468
+ * {@link DocumentResource.upload} — that avoids the extra round-trip and
469
+ * the processing race entirely.
470
+ *
471
+ * @param documentId - The document to rename.
472
+ * @param name - The new display name (max 255 chars), e.g.
473
+ * `'Service agreement.pdf'`.
474
+ * @returns The updated document — **without** `pages` or `assignment`,
475
+ * which this endpoint does not return (unlike
476
+ * {@link DocumentResource.details}). Call `details()` if you need them.
477
+ * @throws {ValidationError} If `documentId` or `name` is missing.
478
+ * @throws {ApiError} `400` if the document is processing or already in
479
+ * signing; `404` if it does not exist.
480
+ *
481
+ * @example
482
+ * ```ts
483
+ * const doc = await client.documents.upload({ filePath: './c.pdf' });
484
+ * await client.documents.waitUntilReady(doc.id); // else 400
485
+ * await client.documents.rename(doc.id, 'Service agreement.pdf');
486
+ * ```
487
+ */
488
+ async rename(documentId, name) {
489
+ const id = this.requireId(documentId, "Document ID");
490
+ const newName = this.requireId(name, "Name");
491
+ this.logger.info("Renaming document", { documentId: id });
492
+ return this.call(
493
+ "Failed to rename document",
494
+ () => this.http.patch(`/documents/${id}`, { name: newName })
249
495
  );
250
496
  }
251
497
  /** Get document details. */
@@ -279,6 +525,9 @@ var DocumentResource = class extends BaseResource {
279
525
  }
280
526
  } catch (err) {
281
527
  if (err instanceof ValidationError) throw err;
528
+ if (err instanceof ApiError && err.statusCode < 500 && err.statusCode !== 429) {
529
+ throw err;
530
+ }
282
531
  this.logger.warn("Error checking document status", {
283
532
  error: err instanceof Error ? err.message : String(err)
284
533
  });
@@ -397,7 +646,12 @@ var DocumentResource = class extends BaseResource {
397
646
  () => this.http.post(`/accounts/${accId}/templates/${tmplId}/documents`, body)
398
647
  );
399
648
  }
400
- /** Estimate the credit cost of creating a document from a template. */
649
+ /**
650
+ * Estimate the credit cost of creating a document from a template.
651
+ *
652
+ * @returns an {@link ICostEstimate}: `total_credits`, balances, and a
653
+ * per-line `breakdown` of what the operation would consume.
654
+ */
401
655
  async estimateCostFromTemplate(templateId, signers, accountId) {
402
656
  const tmplId = this.requireId(templateId, "Template ID");
403
657
  const accId = this.accountId(accountId);
@@ -466,43 +720,6 @@ var DocumentResource = class extends BaseResource {
466
720
  return { signed, total, pending, percentage };
467
721
  }
468
722
  };
469
- async function loadSource(source) {
470
- if ("buffer" in source) {
471
- if (!source.fileName) {
472
- throw new ValidationError("fileName is required when uploading a Buffer");
473
- }
474
- return { buffer: source.buffer, fileName: source.fileName };
475
- }
476
- if (!source.filePath) {
477
- throw new ValidationError("filePath is required");
478
- }
479
- const buffer = await fs.readFile(source.filePath);
480
- return { buffer, fileName: source.fileName ?? path.basename(source.filePath) };
481
- }
482
- function validateUpload(buffer, fileName) {
483
- if (!buffer || buffer.byteLength === 0) {
484
- throw new ValidationError("File buffer is empty", { fileName });
485
- }
486
- if (!fileName.toLowerCase().endsWith(".pdf")) {
487
- throw new ValidationError("Only PDF files are supported", { fileName });
488
- }
489
- if (buffer.byteLength > MAX_UPLOAD_BYTES) {
490
- throw new ValidationError("File size exceeds maximum allowed (25MB)", {
491
- fileSize: buffer.byteLength,
492
- maxSize: MAX_UPLOAD_BYTES
493
- });
494
- }
495
- }
496
- function buildUploadForm(buffer, fileName, metadata) {
497
- const form = new FormData();
498
- const view = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
499
- form.append("file", new Blob([view], { type: "application/pdf" }), fileName);
500
- form.append("name", fileName);
501
- if (metadata) {
502
- form.append("metadata", JSON.stringify(metadata));
503
- }
504
- return form;
505
- }
506
723
  function sleep(ms) {
507
724
  return new Promise((resolve) => setTimeout(resolve, ms));
508
725
  }
@@ -541,7 +758,8 @@ var SignerResource = class extends BaseResource {
541
758
  () => this.http.post(`/accounts/${id}/signers`, normaliseSignerPayload(payload))
542
759
  );
543
760
  } catch (err) {
544
- if (err instanceof ApiError && err.statusCode === 409 && payload.email) {
761
+ const isDuplicateStatus = err instanceof ApiError && (err.statusCode === 409 || err.statusCode === 400);
762
+ if (isDuplicateStatus && payload.email) {
545
763
  const duplicate = await this.findByEmail(payload.email, id);
546
764
  if (duplicate) {
547
765
  this.logger.info("Signer already exists, using existing signer", {
@@ -562,12 +780,12 @@ var SignerResource = class extends BaseResource {
562
780
  () => this.http.get(`/accounts/${id}/signers/${sid}`)
563
781
  );
564
782
  }
565
- /** List signers for the workspace (supports `page`, `per_page`, `search`, `sort`). */
783
+ /** List signers for the workspace (supports `page`, `per-page`, `search`, `sort`). */
566
784
  async list(params = {}, accountId) {
567
785
  const id = this.accountId(accountId);
568
786
  return this.callList(
569
787
  "Failed to list signers",
570
- () => this.http.get(`/accounts/${id}/signers`, { params: cleanParams(params) })
788
+ () => this.http.get(`/accounts/${id}/signers`, { params: cleanListParams(params) })
571
789
  );
572
790
  }
573
791
  /** Update a signer. Fails if the signer has active assignments. */
@@ -588,11 +806,28 @@ var SignerResource = class extends BaseResource {
588
806
  () => this.http.delete(`/accounts/${id}/signers/${sid}`)
589
807
  );
590
808
  }
591
- /** Find a signer by email via the API's `search` parameter. Returns `null` if none match. */
809
+ /**
810
+ * Find a signer by exact email, using the API's `search` filter to narrow
811
+ * the page first. Returns `null` if none match.
812
+ *
813
+ * `search` is a substring match across signer fields, so the result is
814
+ * re-filtered here for an exact, case-insensitive email match.
815
+ *
816
+ * Page size is pinned to the API's maximum of 50: larger values are
817
+ * silently clamped to 50 by the server, so asking for more is misleading.
818
+ * An exact address realistically matches one signer, but a search term that
819
+ * matched more than 50 could in principle miss one — the API exposes no
820
+ * exact-email filter to rule that out.
821
+ *
822
+ * @param email - Exact email address to look for.
823
+ * @param accountId - Override the client's default account ID.
824
+ * @returns The matching {@link ISigner}, or `null`.
825
+ * @throws {ValidationError} If `email` is not a valid address.
826
+ */
592
827
  async findByEmail(email, accountId) {
593
828
  this.assertEmail(email);
594
829
  try {
595
- const { data } = await this.list({ search: email, per_page: 100 }, accountId);
830
+ const { data } = await this.list({ search: email, "per-page": 50 }, accountId);
596
831
  const lower = email.toLowerCase();
597
832
  return data.find((s) => (s.email ?? "").toLowerCase() === lower) ?? null;
598
833
  } catch (err) {
@@ -704,6 +939,58 @@ function normaliseSignerRef(ref, options) {
704
939
  throw new ValidationError("Invalid signer reference", { ref });
705
940
  }
706
941
  var AssignmentResource = class extends BaseResource {
942
+ /**
943
+ * List assignments across the workspace (`GET /assignments`).
944
+ *
945
+ * The account is passed as an `accountId` **query parameter** — the API
946
+ * responds `400` ("Um contexto de conta é necessário e não foi fornecido")
947
+ * without it. Note the camelCase spelling: `account_id` and an
948
+ * `X-Account-Id` header are both rejected.
949
+ *
950
+ * @param params - `page`, `per-page`.
951
+ * @param accountId - Override the client's default account ID.
952
+ * @returns Assignments, with pagination in `meta`. Each item:
953
+ * ```jsonc
954
+ * {
955
+ * "id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4",
956
+ * "sender_email": "sender@example.com",
957
+ * "method": "virtual",
958
+ * "expires_at": null,
959
+ * "message": "Please sign this contract",
960
+ * "signers": [
961
+ * {
962
+ * "id": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5",
963
+ * "full_name": "Ana Souza",
964
+ * "email": "signer@example.com",
965
+ * "whatsapp_phone_number": null,
966
+ * "has_accepted_terms": false,
967
+ * "completed": false,
968
+ * "notification_history": [],
969
+ * "verification_method": "Email",
970
+ * "notification_methods": ["Email"],
971
+ * "step": 1,
972
+ * "notified": true
973
+ * }
974
+ * ]
975
+ * }
976
+ * ```
977
+ * @throws {ValidationError} If no account ID is available.
978
+ * @throws {ApiError} If the API rejects the request.
979
+ *
980
+ * @example
981
+ * ```ts
982
+ * const { data, meta } = await client.assignments.list({ 'per-page': 20 });
983
+ * ```
984
+ */
985
+ async list(params = {}, accountId) {
986
+ const id = this.accountId(accountId);
987
+ return this.callList(
988
+ "Failed to list assignments",
989
+ () => this.http.get("/assignments", {
990
+ params: { accountId: id, ...cleanListParams(params) }
991
+ })
992
+ );
993
+ }
707
994
  /** Create a signing assignment for a document. */
708
995
  async create(documentId, payload) {
709
996
  const docId = this.requireId(documentId, "Document ID");
@@ -718,7 +1005,15 @@ var AssignmentResource = class extends BaseResource {
718
1005
  () => this.http.post(`/documents/${docId}/assignments`, body)
719
1006
  );
720
1007
  }
721
- /** Estimate the cost (in credits) of creating the assignment. */
1008
+ /**
1009
+ * Estimate the cost (in credits/documents) of creating the assignment.
1010
+ *
1011
+ * Signer entries may omit `id` and supply only `verification_method` /
1012
+ * `notification_methods` when only the channel mix matters for the estimate.
1013
+ *
1014
+ * @returns an {@link ICostEstimate} with `total_credits`, balances, and a
1015
+ * line-item `breakdown`.
1016
+ */
722
1017
  async estimateCost(documentId, payload) {
723
1018
  const docId = this.requireId(documentId, "Document ID");
724
1019
  return this.call(
@@ -753,7 +1048,11 @@ var AssignmentResource = class extends BaseResource {
753
1048
  () => this.http.put(`/documents/${docId}/assignments/${asgId}/signers/${sid}/resend`)
754
1049
  );
755
1050
  }
756
- /** Estimate the cost of resending a signer notification. */
1051
+ /**
1052
+ * Estimate the cost of resending a signer notification.
1053
+ *
1054
+ * @returns an {@link IResendCostEstimate} (`total`, `breakdown`, balances).
1055
+ */
757
1056
  async estimateResendCost(documentId, assignmentId, signerId) {
758
1057
  const docId = this.requireId(documentId, "Document ID");
759
1058
  const asgId = this.requireId(assignmentId, "Assignment ID");
@@ -777,26 +1076,10 @@ var AssignmentResource = class extends BaseResource {
777
1076
  () => this.http.get(`/documents/${docId}/assignments/${asgId}/whatsapp-notifications`)
778
1077
  );
779
1078
  }
780
- /**
781
- * Cancel a signature request. This endpoint is not listed in the public
782
- * Swagger but is exposed by the platform.
783
- */
784
- async cancel(documentId, reason, accountId) {
785
- const docId = this.requireId(documentId, "Document ID");
786
- const accId = this.accountId(accountId);
787
- this.logger.info("Cancelling signature request", { documentId: docId, reason });
788
- return this.call(
789
- "Failed to cancel signature request",
790
- () => this.http.post(
791
- `/accounts/${accId}/signature-requests/${docId}/cancel`,
792
- { document_id: docId, reason }
793
- )
794
- );
795
- }
796
1079
  };
797
1080
 
798
1081
  // src/resources/webhooks.ts
799
- var DEFAULT_EVENTS = [
1082
+ var DEFAULT_WEBHOOK_EVENTS = [
800
1083
  "document_ready",
801
1084
  "document_prepared",
802
1085
  "signer_signed_document",
@@ -804,7 +1087,21 @@ var DEFAULT_EVENTS = [
804
1087
  "document_processing_failed"
805
1088
  ];
806
1089
  var WebhookResource = class extends BaseResource {
807
- /** Register (or replace) the webhook subscription for the workspace. */
1090
+ /**
1091
+ * Register (or replace) the workspace's single webhook subscription
1092
+ * (`PUT /accounts/{id}/webhooks/subscriptions`). There is exactly one
1093
+ * subscription per workspace, keyed by URL.
1094
+ *
1095
+ * When `events` is omitted or empty, {@link DEFAULT_WEBHOOK_EVENTS} is used
1096
+ * (`document_ready`, `document_prepared`, `signer_signed_document`,
1097
+ * `signer_rejected_document`, `document_processing_failed`).
1098
+ *
1099
+ * @example
1100
+ * ```ts
1101
+ * await client.webhooks.register({ url: 'https://example.com/hook', email: 'ops@example.com' });
1102
+ * // → { url, email, events: [...], is_active: true, updated_at: '2026-…' }
1103
+ * ```
1104
+ */
808
1105
  async register(payload, accountId) {
809
1106
  if (!payload.url) throw new ValidationError("Webhook URL is required");
810
1107
  if (!payload.email) throw new ValidationError("Webhook email is required");
@@ -812,7 +1109,7 @@ var WebhookResource = class extends BaseResource {
812
1109
  const body = {
813
1110
  url: payload.url,
814
1111
  email: payload.email,
815
- events: payload.events && payload.events.length > 0 ? payload.events : DEFAULT_EVENTS,
1112
+ events: payload.events && payload.events.length > 0 ? payload.events : DEFAULT_WEBHOOK_EVENTS,
816
1113
  is_active: payload.is_active ?? true
817
1114
  };
818
1115
  this.logger.info("Registering webhook", { url: payload.url });
@@ -829,16 +1126,14 @@ var WebhookResource = class extends BaseResource {
829
1126
  () => this.http.get(`/accounts/${id}/webhooks/subscriptions`)
830
1127
  );
831
1128
  }
832
- /** Delete the current webhook subscription. */
833
- async delete(accountId) {
834
- const id = this.accountId(accountId);
835
- this.logger.info("Deleting webhook subscription");
836
- return this.callVoid(
837
- "Failed to delete webhook subscription",
838
- () => this.http.delete(`/accounts/${id}/webhooks/subscriptions`)
839
- );
840
- }
841
- /** Inactivate the current webhook subscription without deleting it. */
1129
+ /**
1130
+ * Inactivate the current webhook subscription.
1131
+ *
1132
+ * This is the only supported way to stop deliveries — the API has no
1133
+ * subscription-delete route. The subscription is retained (with its `url`
1134
+ * and `events`) and simply stops firing; re-enable it by calling
1135
+ * {@link WebhookResource.register} again with `is_active: true`.
1136
+ */
842
1137
  async inactivate(accountId) {
843
1138
  const id = this.accountId(accountId);
844
1139
  this.logger.info("Inactivating webhook subscription");
@@ -860,7 +1155,7 @@ var WebhookResource = class extends BaseResource {
860
1155
  return this.callList(
861
1156
  "Failed to list webhook dispatches",
862
1157
  () => this.http.get(`/accounts/${id}/webhooks`, {
863
- params: cleanParams(params)
1158
+ params: cleanListParams(params)
864
1159
  })
865
1160
  );
866
1161
  }
@@ -877,20 +1172,56 @@ var WebhookResource = class extends BaseResource {
877
1172
 
878
1173
  // src/resources/templates.ts
879
1174
  var TemplateResource = class extends BaseResource {
1175
+ /**
1176
+ * Create a template by uploading a PDF (`POST /accounts/{id}/templates`).
1177
+ *
1178
+ * The template is created in `Uploaded` status and transitions to `Ready`
1179
+ * once the platform finishes processing its pages. Configure roles/fields
1180
+ * afterwards in the Assinafy editor.
1181
+ *
1182
+ * @example
1183
+ * ```ts
1184
+ * const tmpl = await client.templates.create(
1185
+ * { filePath: './nda.pdf' },
1186
+ * { name: 'NDA template' },
1187
+ * );
1188
+ * // → { resource: 'template', id, name, status: 'Uploaded',
1189
+ * // roles: [{ id, name: 'TemplateEditor', assignment_type: 'Editor' }],
1190
+ * // pages: [], tags: [], created_at, updated_at }
1191
+ * ```
1192
+ */
1193
+ async create(source, options = {}) {
1194
+ const id = this.accountId(options.accountId);
1195
+ const formOptions = {};
1196
+ if (options.name !== void 0) formOptions.name = options.name;
1197
+ const template = await this.uploadPdf(
1198
+ `/accounts/${id}/templates`,
1199
+ source,
1200
+ formOptions,
1201
+ {
1202
+ errorLabel: "Failed to create template",
1203
+ missingId: "Template upload succeeded but no template ID was returned"
1204
+ }
1205
+ );
1206
+ this.logger.info("Template created", { templateId: template.id });
1207
+ return template;
1208
+ }
880
1209
  /** List templates for the workspace. */
881
1210
  async list(params = {}, accountId) {
882
1211
  const id = this.accountId(accountId);
883
1212
  return this.callList(
884
1213
  "Failed to list templates",
885
- () => this.http.get(`/accounts/${id}/templates`, { params: cleanParams(params) })
1214
+ () => this.http.get(`/accounts/${id}/templates`, { params: cleanListParams(params) })
886
1215
  );
887
1216
  }
888
1217
  /**
889
- * Get a template by ID.
1218
+ * Get a template by ID (`GET /accounts/{id}/templates/{template_id}`).
890
1219
  *
891
- * Note: the swagger only documents the list endpoint; this single-resource
892
- * `GET /accounts/{id}/templates/{id}` is exposed by the platform and used
893
- * by the official PHP SDK.
1220
+ * Returns the same shape as {@link TemplateResource.list} plus
1221
+ * `default_document_tags` (the tags auto-applied to every document created
1222
+ * from this template) and `resource`. Both endpoints return `pages` with
1223
+ * per-page `download_url`, so fetching a template again purely to read its
1224
+ * pages is unnecessary.
894
1225
  */
895
1226
  async get(templateId, accountId) {
896
1227
  const id = this.accountId(accountId);
@@ -901,9 +1232,40 @@ var TemplateResource = class extends BaseResource {
901
1232
  );
902
1233
  }
903
1234
  /**
904
- * `GET /accounts/{id}/templates/{template_id}/pages/{page_id}/download`
905
- * download a template page as a JPEG (used by template editors to render
906
- * thumbnails on the client).
1235
+ * Update a template's `name` and/or default `message`
1236
+ * (`PUT /accounts/{id}/templates/{template_id}`). Returns the updated template.
1237
+ *
1238
+ * @example
1239
+ * ```ts
1240
+ * await client.templates.update(templateId, { name: 'NDA v2', message: 'Please sign' });
1241
+ * ```
1242
+ */
1243
+ async update(templateId, payload, accountId) {
1244
+ const id = this.accountId(accountId);
1245
+ const tmplId = this.requireId(templateId, "Template ID");
1246
+ return this.call(
1247
+ "Failed to update template",
1248
+ () => this.http.put(
1249
+ `/accounts/${id}/templates/${tmplId}`,
1250
+ cleanParams(payload)
1251
+ )
1252
+ );
1253
+ }
1254
+ /** Delete a template (`DELETE /accounts/{id}/templates/{template_id}`). */
1255
+ async delete(templateId, accountId) {
1256
+ const id = this.accountId(accountId);
1257
+ const tmplId = this.requireId(templateId, "Template ID");
1258
+ return this.callVoid(
1259
+ "Failed to delete template",
1260
+ () => this.http.delete(`/accounts/${id}/templates/${tmplId}`)
1261
+ );
1262
+ }
1263
+ /**
1264
+ * Download a template page as a JPEG
1265
+ * (`GET /accounts/{id}/templates/{template_id}/pages/{page_id}/download`).
1266
+ *
1267
+ * Used by template editors to render page thumbnails on the client. The
1268
+ * matching `download_url` is also returned on each `template.pages[]` entry.
907
1269
  */
908
1270
  async downloadPage(templateId, pageId, accountId) {
909
1271
  const id = this.accountId(accountId);
@@ -927,7 +1289,7 @@ var TagResource = class extends BaseResource {
927
1289
  return this.call(
928
1290
  "Failed to list tags",
929
1291
  () => this.http.get(`/accounts/${id}/tags`, {
930
- params: cleanParams(params)
1292
+ params: cleanListParams(params)
931
1293
  })
932
1294
  );
933
1295
  }
@@ -1069,7 +1431,7 @@ var FieldsResource = class extends BaseResource {
1069
1431
  return this.call(
1070
1432
  "Failed to list field definitions",
1071
1433
  () => this.http.get(`/accounts/${id}/fields`, {
1072
- params: cleanParams(params)
1434
+ params: cleanListParams(params)
1073
1435
  })
1074
1436
  );
1075
1437
  }
@@ -1153,7 +1515,44 @@ var SignerDocumentsResource = class extends BaseResource {
1153
1515
  return this.callList(
1154
1516
  "Failed to list signer documents",
1155
1517
  () => this.http.get(`/signers/${sid}/documents`, {
1156
- params: { "signer-access-code": code, ...cleanParams(params) }
1518
+ params: { "signer-access-code": code, ...cleanListParams(params) }
1519
+ })
1520
+ );
1521
+ }
1522
+ /**
1523
+ * Search the documents awaiting a given signer
1524
+ * (`GET /signers/{signer_id}/documents/search?signer-access-code=…`).
1525
+ *
1526
+ * The signer-side counterpart of {@link DocumentResource.search}, scoped to
1527
+ * one signer and authorised by their access code rather than the API key.
1528
+ * Like {@link SignerDocumentsResource.list}, it requires
1529
+ * `signer-access-code`; the published spec omits that parameter, but the
1530
+ * endpoint is not usable without it.
1531
+ *
1532
+ * @param signerId - The signer whose documents are searched.
1533
+ * @param signerAccessCode - The signer's access code, from their signing link.
1534
+ * @param search - Free-text term matched against the document name.
1535
+ * @returns Matching documents for that signer, in the compact
1536
+ * {@link IDocumentListItem} shape, with pagination in `meta`.
1537
+ * @throws {ValidationError} If `signerId` or `signerAccessCode` is missing.
1538
+ * @throws {ApiError} If the access code is invalid or expired.
1539
+ *
1540
+ * @example
1541
+ * ```ts
1542
+ * const { data } = await client.signerDocuments.search(
1543
+ * signerId,
1544
+ * accessCode,
1545
+ * 'agreement',
1546
+ * );
1547
+ * ```
1548
+ */
1549
+ async search(signerId, signerAccessCode, search) {
1550
+ const sid = this.requireId(signerId, "Signer ID");
1551
+ const code = this.requireId(signerAccessCode, "signer-access-code");
1552
+ return this.callList(
1553
+ "Failed to search signer documents",
1554
+ () => this.http.get(`/signers/${sid}/documents/search`, {
1555
+ params: cleanParams({ "signer-access-code": code, search })
1157
1556
  })
1158
1557
  );
1159
1558
  }
@@ -1271,7 +1670,12 @@ var SignerDocumentsResource = class extends BaseResource {
1271
1670
  })
1272
1671
  );
1273
1672
  }
1274
- /** `GET /sign?signer-access-code=…` — fetch the assignment as the signer sees it. */
1673
+ /**
1674
+ * `GET /sign?signer-access-code=…` — fetch the assignment as the signer sees it.
1675
+ *
1676
+ * @param hasAcceptedTerms maps to the `has_accepted_terms` query param
1677
+ * (server default `false`); pass `true` once the signer has accepted terms.
1678
+ */
1275
1679
  async getAssignment(signerAccessCode, hasAcceptedTerms) {
1276
1680
  const code = this.requireId(signerAccessCode, "signer-access-code");
1277
1681
  return this.call(
@@ -1301,8 +1705,8 @@ var SignerDocumentsResource = class extends BaseResource {
1301
1705
  }
1302
1706
  /**
1303
1707
  * `PUT /documents/{documentId}/assignments/{assignmentId}/reject?signer-access-code=…`
1304
- * — signer-side decline. (Distinct from `assignments.cancel`, which is the
1305
- * workspace-side cancellation flow.)
1708
+ * — signer-side decline. (The workspace-side equivalent is to delete the
1709
+ * document via `documents.delete`; there is no workspace "cancel" endpoint.)
1306
1710
  */
1307
1711
  async decline(documentId, assignmentId, signerAccessCode, declineReason) {
1308
1712
  const did = this.requireId(documentId, "Document ID");
@@ -1368,6 +1772,21 @@ var WebhookVerifier = class {
1368
1772
  // src/client.ts
1369
1773
  var DEFAULT_BASE_URL = "https://api.assinafy.com.br/v1";
1370
1774
  var AssinafyClient = class _AssinafyClient {
1775
+ axiosInstance;
1776
+ defaultAccountId;
1777
+ logger;
1778
+ webhookSecret;
1779
+ documents;
1780
+ signers;
1781
+ workspaces;
1782
+ assignments;
1783
+ webhooks;
1784
+ templates;
1785
+ tags;
1786
+ auth;
1787
+ fields;
1788
+ signerDocuments;
1789
+ webhookVerifier;
1371
1790
  constructor(options) {
1372
1791
  if (!options.apiKey && !options.token) {
1373
1792
  throw new ValidationError(
@@ -1393,6 +1812,10 @@ var AssinafyClient = class _AssinafyClient {
1393
1812
  timeout: options.timeout ?? 3e4,
1394
1813
  headers
1395
1814
  });
1815
+ const maxRetries = options.maxRetries ?? 2;
1816
+ if (maxRetries > 0) {
1817
+ installRateLimitRetry(this.axiosInstance, maxRetries, this.logger);
1818
+ }
1396
1819
  this.documents = new DocumentResource(this.axiosInstance, this.defaultAccountId, this.logger);
1397
1820
  this.signers = new SignerResource(this.axiosInstance, this.defaultAccountId, this.logger);
1398
1821
  this.workspaces = new WorkspaceResource(this.axiosInstance, void 0, this.logger);
@@ -1431,6 +1854,7 @@ var AssinafyClient = class _AssinafyClient {
1431
1854
  if (baseUrl !== void 0) opts.baseUrl = baseUrl;
1432
1855
  if (webhookSecret !== void 0) opts.webhookSecret = webhookSecret;
1433
1856
  if (config.timeout !== void 0) opts.timeout = config.timeout;
1857
+ if (config.maxRetries !== void 0) opts.maxRetries = config.maxRetries;
1434
1858
  if (config.logger !== void 0) opts.logger = config.logger;
1435
1859
  return new _AssinafyClient(opts);
1436
1860
  }
@@ -1490,14 +1914,37 @@ var AssinafyClient = class _AssinafyClient {
1490
1914
  function normaliseBaseUrl(raw) {
1491
1915
  return raw.endsWith("/") ? raw.slice(0, -1) : raw;
1492
1916
  }
1917
+ function installRateLimitRetry(http, maxRetries, logger) {
1918
+ http.interceptors.response.use(
1919
+ (response) => response,
1920
+ async (error) => {
1921
+ if (!axios2.isAxiosError(error) || error.response?.status !== 429 || !error.config) {
1922
+ throw error;
1923
+ }
1924
+ const config = error.config;
1925
+ const attempt = (config._retryCount ?? 0) + 1;
1926
+ if (attempt > maxRetries) throw error;
1927
+ config._retryCount = attempt;
1928
+ const delayMs = nextRetryDelayMs(
1929
+ error.response.headers,
1930
+ attempt
1931
+ );
1932
+ logger.warn("Rate limited (429); retrying after delay", { attempt, maxRetries, delayMs });
1933
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1934
+ return http(config);
1935
+ }
1936
+ );
1937
+ }
1493
1938
  export {
1494
1939
  ApiError,
1495
1940
  AssignmentResource,
1496
1941
  AssinafyClient,
1497
1942
  AssinafyError,
1498
1943
  AuthenticationResource,
1944
+ DEFAULT_WEBHOOK_EVENTS,
1499
1945
  DocumentResource,
1500
1946
  FieldsResource,
1947
+ MAX_UPLOAD_BYTES,
1501
1948
  NetworkError,
1502
1949
  SignerDocumentsResource,
1503
1950
  SignerResource,