assinafy 2.0.1 → 2.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83473f4fd075a7c665a16ee6ea6c38c64c548927930221d24ecd468cce1f4203
4
- data.tar.gz: 57c81beb685fe09baf9de80eb36abc628d224952b2b08e582d19e24ecb402c16
3
+ metadata.gz: 1d6db456898234647465325a55c4580054a8ab93f39aff3d6ca067ffe6be9e49
4
+ data.tar.gz: 55577984ae91aec1ad66715a9b34a8ae10e991f015d6f4e3fe01f5bebf2b9503
5
5
  SHA512:
6
- metadata.gz: dda868b4808b8885693edc57d8825eaf25a31dd628395180f651bd479a2b59ced524205683900812aa61654b2e79a7c69a110ca8c99c54ea084f21ea3515640a
7
- data.tar.gz: ba6c518504f42fddaa0f9cfe4f58b0bfe6e23a83255f2cee5ef1a76551fba72b628f8ea448ba93b24201b055fd41c2ee95882534c3eedfb334f49a5b23cb5439
6
+ metadata.gz: 93a92268707f3afa45e7f16c837ab3ded90b7ab8859060ff48a0d7eeb81a244dde3b53ada834852de4f83df7e72f738bbffc1468e8ae7de24dfffba56709edb6
7
+ data.tar.gz: 384b02e9dd54fb89ea78dc61b6f7a507e8e317431d30996b0dcfd16f4a6d3251a03821d0f6d59d28541b2425a24cbd98062c68b2945f0ef2fb252af8569a7ad4
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to the `assinafy` Ruby gem are documented here.
4
4
 
5
+ ## [2.1.0] - 2026-10-28
6
+
7
+ ### Added
8
+
9
+ - `ContractResource` and `client.contracts` cover listing, reading, updating, relating, renewing and terminating
10
+ the contract terms tracked on documents, including nullable fields and optimistic `updated_at` preconditions.
11
+ - User notification preferences support `ContractRenewalNotice`, `ContractExpiring`, `ContractExpired` and
12
+ `ContractRenewed`.
13
+
14
+ ### Changed
15
+
16
+ - New or changed webhook endpoint URLs must be absolute HTTPS URLs; invalid URLs raise `ValidationError` before a
17
+ request.
18
+ - Contract, document, assignment, webhook and reminder-cost response fields reflect the current API contract.
19
+
5
20
  ## [2.0.1] - 2026-10-19
6
21
 
7
22
  ### Fixed
data/README.en.md CHANGED
@@ -691,7 +691,7 @@ See [Errors](#errors) for the full hierarchy.
691
691
 
692
692
  ## Resource reference
693
693
 
694
- `Assinafy::Client` exposes thirteen accessors — twelve API resources plus the local `webhook_verifier` helper:
694
+ `Assinafy::Client` exposes fourteen accessors — thirteen API resources plus the local `webhook_verifier` helper:
695
695
 
696
696
  | Accessor | What it covers |
697
697
  | --------------------------- | ------------------------------------------------------------------ |
@@ -700,6 +700,7 @@ See [Errors](#errors) for the full hierarchy.
700
700
  | `client.accounts` | Account CRUD, theme, KPI stats, brand logo |
701
701
  | `client.users` | User profile, notification preferences, cross-account KPIs |
702
702
  | `client.documents` | Upload, list, search, rename, download, delete, verify, tags |
703
+ | `client.contracts` | Contract terms, amendments, renewals, terminations |
703
704
  | `client.signers` | Workspace signer CRUD + signer self-service endpoints |
704
705
  | `client.signer_documents` | Signer-authenticated multi-document operations + search |
705
706
  | `client.assignments` | List/create/sign/decline/resend/estimate assignments |
@@ -781,6 +782,27 @@ client.documents.signing_progress('document-id') # => { signed: 1, total: 2, p
781
782
  An Array `tags` filter is sent as comma-separated tag IDs. The tag arrays of `replace_tags`/`append_tags` take tag
782
783
  IDs.
783
784
 
785
+ ### Contracts
786
+
787
+ ```ruby
788
+ contract = client.contracts.update(
789
+ 'document-id',
790
+ status: 'active', counterparty_name: 'Example Ltda', value: 120_000, currency: 'BRL',
791
+ start_date: '2026-11-01', end_date: '2027-10-31', auto_renew: true,
792
+ renewal_term_months: 12, notice_period_days: 30
793
+ )
794
+
795
+ client.contracts.get('document-id')
796
+ client.contracts.list(status: ['active'], ending_within_days: 30)
797
+ client.contracts.related('document-id')
798
+ client.contracts.renew('document-id', term: contract['term'], updated_at: contract['updated_at'])
799
+ client.contracts.terminate('document-id', reason: 'Ended by the parties')
800
+ ```
801
+
802
+ `update` is partial and preserves explicit `nil` values as JSON `null`. Link an amendment document with
803
+ `parent_document_id` and `relation_type`. Reads require OAuth scope `documents:read`; writes require
804
+ `documents:write` and a plan with contract tracking. `get` and `related` return `nil` when no tracked terms exist.
805
+
784
806
  ### Signers (workspace CRUD)
785
807
 
786
808
  ```ruby
data/README.md CHANGED
@@ -966,6 +966,38 @@ verificacao['agreement_code'] # código impresso no certificado do documento
966
966
  Ela devolve o resultado da Assinafy; não valida independentemente a assinatura do PDF nem a
967
967
  cadeia de certificação.
968
968
 
969
+ ### Acompanhar o contrato
970
+
971
+ Quando o plano da conta inclui acompanhamento de contratos, registre os termos no documento e
972
+ use o `updated_at` devolvido pela API como precondição para evitar sobrescrever uma alteração
973
+ concorrente:
974
+
975
+ ```ruby
976
+ contrato = client.contracts.update(
977
+ documento['id'],
978
+ status: 'active',
979
+ counterparty_name: 'Empresa Exemplo Ltda',
980
+ value: 120_000,
981
+ currency: 'BRL',
982
+ start_date: '2026-11-01',
983
+ end_date: '2027-10-31',
984
+ auto_renew: true,
985
+ renewal_term_months: 12,
986
+ notice_period_days: 30
987
+ )
988
+
989
+ client.contracts.get(documento['id'])
990
+ client.contracts.list(status: ['active'], ending_within_days: 30)
991
+ client.contracts.related(documento['id'])
992
+ client.contracts.renew(documento['id'], term: contrato['term'], updated_at: contrato['updated_at'])
993
+ client.contracts.terminate(documento['id'], reason: 'Encerrado pelas partes')
994
+ ```
995
+
996
+ `update` é parcial e preserva `nil` como JSON `null` para limpar campos. Um documento de aditivo
997
+ pode ser ligado com `parent_document_id` e `relation_type`. As leituras usam o escopo OAuth
998
+ `documents:read`; alterações usam `documents:write` e exigem que o plano tenha acompanhamento de
999
+ contratos. `get` e `related` devolvem `nil` quando o documento não tem termos acompanhados.
1000
+
969
1001
  ---
970
1002
 
971
1003
  ## 12. Organizar e limpar
@@ -1072,6 +1104,7 @@ end
1072
1104
  | `client.accounts` | workspaces, tema, KPIs, logo |
1073
1105
  | `client.users` | perfil próprio, KPIs, preferências de notificação |
1074
1106
  | `client.documents` | upload, listagem, download, ciclo de vida, tags, verificação |
1107
+ | `client.contracts` | termos, aditivos, renovações e encerramentos de contratos |
1075
1108
  | `client.signers` | CRUD de signatários e todo o autoatendimento do signatário |
1076
1109
  | `client.signer_documents` | documentos do signatário, assinar/recusar em lote |
1077
1110
  | `client.assignments` | pedidos de assinatura, custos, reenvios, assinatura, recusa |
@@ -1,7 +1,7 @@
1
1
  # Assinafy Ruby SDK API Reference
2
2
 
3
- > Contract source: Assinafy API v1 OpenAPI 3.1.0, retrieved 2026-10-16 from
4
- > `https://api.assinafy.com.br/v1/docs/openapi.json` (85 paths, 112 operations, 45 schemas).
3
+ > Contract source: Assinafy API v1 OpenAPI 3.1.0, retrieved 2026-10-28 from
4
+ > `https://api.assinafy.com.br/v1/docs/openapi.json` (90 paths, 118 operations, 49 schemas).
5
5
  > `scripts/check_api_contract.rb` validates contract compatibility weekly.
6
6
 
7
7
  This is the SDK-facing contract reference. Paths below are wire paths; Ruby methods return the unwrapped
@@ -50,6 +50,12 @@ Required parameters and object properties are marked with `*`.
50
50
  | GET | `/v1/accounts/{accountId}` | `AccountResource#get` | Bearer token or `X-Api-Key` | path `accountId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`Account`](#account) } |
51
51
  | PUT | `/v1/accounts/{accountId}` | `AccountResource#update` | Bearer token or `X-Api-Key` | path `accountId*`: string | required; `application/json` object { `name`: string; `notification_sender_type`: string } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`Account`](#account) } |
52
52
  | DELETE | `/v1/accounts/{accountId}` | `AccountResource#delete` | Bearer token or `X-Api-Key` | path `accountId*`: string | optional; `application/json` object { `force`: boolean } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<any> } |
53
+ | GET | `/v1/accounts/{accountId}/contracts` | `ContractResource#list` | Bearer token or `X-Api-Key`; OAuth `documents:read` | path `accountId*`: string<br>query `status`: comma-separated array of `draft`, `active`, `expired`, `terminated`<br>query `counterparty`, `search`, `end_date_from`, `end_date_to`, `ending_within_days`, `external_system`, `external_id`, `sort`, `expand`, `page`, `per-page` | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`DocumentContract`](#documentcontract)> }; pagination headers become `{ data:, meta: }` |
54
+ | GET | `/v1/accounts/{accountId}/documents/{documentId}/contract` | `ContractResource#get` | Bearer token or `X-Api-Key`; OAuth `documents:read` | path `accountId*`: string<br>path `documentId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`DocumentContract`](#documentcontract) or [`DocumentContractAmendment`](#documentcontractamendment) }; SDK returns `nil` on `404` |
55
+ | PUT | `/v1/accounts/{accountId}/documents/{documentId}/contract` | `ContractResource#update` | Bearer token or `X-Api-Key`; OAuth `documents:write` | path `accountId*`: string<br>path `documentId*`: string | required non-empty [`DocumentContractInput`](#documentcontractinput); explicit `nil` is sent as JSON null | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`DocumentContract`](#documentcontract) or [`DocumentContractAmendment`](#documentcontractamendment) }; `403` without contract tracking, `409` on stale `updated_at` |
56
+ | GET | `/v1/accounts/{accountId}/documents/{documentId}/contract/related` | `ContractResource#related` | Bearer token or `X-Api-Key`; OAuth `documents:read` | path `accountId*`: string<br>path `documentId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: root [`DocumentContract`](#documentcontract) with `amendments` }; SDK returns `nil` on `404` |
57
+ | POST | `/v1/accounts/{accountId}/documents/{documentId}/contract/renew` | `ContractResource#renew` | Bearer token or `X-Api-Key`; OAuth `documents:write` | path `accountId*`: string<br>path `documentId*`: string | optional; `application/json` object { `end_date`: string (date, nullable); `value`: number (nullable); `term`: integer; `updated_at`: string (date-time) }; a valid request requires `term` or `end_date` | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`DocumentContract`](#documentcontract) }; `403` without contract tracking, `409` on stale `updated_at` |
58
+ | POST | `/v1/accounts/{accountId}/documents/{documentId}/contract/terminate` | `ContractResource#terminate` | Bearer token or `X-Api-Key`; OAuth `documents:write` | path `accountId*`: string<br>path `documentId*`: string | optional; `application/json` object { `reason`: string (nullable, max 500); `updated_at`: string (date-time) } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`DocumentContract`](#documentcontract) }; `403` without contract tracking, `409` on stale `updated_at` |
53
59
  | GET | `/v1/accounts/{accountId}/documents` | `DocumentResource#list` | Bearer token or `X-Api-Key` | path `accountId*`: string<br>query `status`: string<br>query `method`: string<br>query `search`: string<br>query `tags`: string<br>query `sort`: string<br>query `page`: integer<br>query `per-page`: integer | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`Document`](#document)> } |
54
60
  | POST | `/v1/accounts/{accountId}/documents` | `DocumentResource#upload` | Bearer token or `X-Api-Key` | path `accountId*`: string | required; `multipart/form-data` object { `file*`: string (binary) } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`Document`](#document) } |
55
61
  | GET | `/v1/accounts/{accountId}/documents/search` | `DocumentResource#search` | Bearer token or `X-Api-Key` | path `accountId*`: string<br>query `search`: string<br>query `status`: string<br>query `page`: integer<br>query `per-page`: integer | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`Document`](#document)> } |
@@ -111,7 +117,7 @@ Required parameters and object properties are marked with `*`.
111
117
  | POST | `/v1/documents/{documentId}/assignments/{assignmentId}` | `AssignmentResource#sign` | `signer-access-code` query parameter | path `documentId*`: string<br>path `assignmentId*`: string | required; `application/json` Array<object { `itemId*`: string; `fieldId*`: string; `pageId*`: string; `value*`: string }> | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: object } |
112
118
  | PUT | `/v1/documents/{documentId}/assignments/{assignmentId}/reject` | `AssignmentResource#decline` | `signer-access-code` query parameter | path `documentId*`: string<br>path `assignmentId*`: string | required; `application/json` object { `decline_reason*`: string } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<any> } |
113
119
  | PUT | `/v1/documents/{documentId}/assignments/{assignmentId}/reset-expiration` | `AssignmentResource#reset_expiration` | Bearer token or `X-Api-Key` | path `documentId*`: string<br>path `assignmentId*`: string | required; `application/json` object { `expires_at*`: string (date-time) or `null` to clear } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`Assignment`](#assignment) } |
114
- | POST | `/v1/documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/estimate-resend-cost` | `AssignmentResource#estimate_resend_cost` | Bearer token or `X-Api-Key` | path `documentId*`: string<br>path `assignmentId*`: string<br>path `signerId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`CostEstimate`](#costestimate) } |
120
+ | POST | `/v1/documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/estimate-resend-cost` | `AssignmentResource#estimate_resend_cost` | Bearer token or `X-Api-Key` | path `documentId*`: string<br>path `assignmentId*`: string<br>path `signerId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`ResendCostEstimate`](#resendcostestimate) } |
115
121
  | GET | `/v1/documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/identity-verification` | `AssignmentResource#identity_review` | Bearer token or `X-Api-Key` | path `documentId*`, `assignmentId*`, `signerId*`: string | None | `200` envelope data [`SignerIdentityReview`](#signeridentityreview); `404` when no verification exists |
116
122
  | GET | `/v1/documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/identity-verification/images/{name}` | `AssignmentResource#identity_review_image` | Bearer token or `X-Api-Key` | path `documentId*`, `assignmentId*`, `signerId*`: string; `name*`: enum `document_front`, `document_back`, `document_pdf`, `face` | None | `200` binary `image/jpeg`, `image/png` or `application/pdf`, `Cache-Control: no-store`; `404` unless evidence exists and review is InReview |
117
123
  | POST | `/v1/documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/identity-verification/review` | `AssignmentResource#decide_identity_review` | Bearer token or `X-Api-Key` | path `documentId*`, `assignmentId*`, `signerId*`: string | required; `application/json` { `decision*`: enum `Approved`, `Rejected`; `note`: string or null (max 2000 characters) } | `200` envelope data [`SignerIdentityReview`](#signeridentityreview); `400` unless InReview and document is pending_signature or pending_identity_review |
@@ -151,8 +157,8 @@ Required parameters and object properties are marked with `*`.
151
157
  | PUT | `/v1/users/self/mfa/totp/confirm` | `AuthResource#confirm_totp_enrollment` | Bearer token or `X-Api-Key` | None | required; `application/json` object { `id*`: string (SDK `method_id:`); `code*`: string; `password`: string (password); `reauth_code`: string } — `password` or `reauth_code` only when replacing a confirmed method | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: object { `recovery_codes`: Array<string> (shown once) } } |
152
158
  | POST | `/v1/users/self/mfa/recovery-codes` | `AuthResource#regenerate_recovery_codes` | Bearer token or `X-Api-Key` | None | required; `application/json` object { `password`: string (password); `code`: string } — one of the two is required (checked locally) | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: object { `recovery_codes`: Array<string> } } |
153
159
  | DELETE | `/v1/users/self/mfa/{customId}` | `AuthResource#delete_mfa_method` | Bearer token or `X-Api-Key` | path `customId*`: string | required; `application/json` object { `password`: string (password); `code`: string } — one of the two is required (checked locally) | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: object { `is_mfa_enabled`: boolean } } |
154
- | GET | `/v1/users/self/notification-preferences` | `UserResource#notification_preferences` | Bearer token or `X-Api-Key` | None | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`NotificationPreferences`](#notificationpreferences) } |
155
- | PUT | `/v1/users/self/notification-preferences` | `UserResource#update_notification_preferences` | Bearer token or `X-Api-Key` | None | required; `application/json` [`NotificationPreferences`](#notificationpreferences) | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`NotificationPreferences`](#notificationpreferences) } |
160
+ | GET | `/v1/users/self/notification-preferences` | `UserResource#notification_preferences` | User bearer token or `X-Api-Key`; unavailable to third-party OAuth apps | None | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`NotificationPreferences`](#notificationpreferences) } |
161
+ | PUT | `/v1/users/self/notification-preferences` | `UserResource#update_notification_preferences` | User bearer token or `X-Api-Key`; unavailable to third-party OAuth apps | None | required; `application/json` [`NotificationPreferences`](#notificationpreferences) | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`NotificationPreferences`](#notificationpreferences) } |
156
162
  | GET | `/v1/users/self/stats` | `UserResource#stats` | Bearer token or `X-Api-Key` | query `granularity`: string<br>query `month`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`DocumentStatsRow`](#documentstatsrow)> } |
157
163
  | POST | `/v1/verify` | `SignerResource#verify_email` | `signer-access-code` query parameter | None | required; `application/json` object { `verification-code*`: string } | `200` `application/json` [`Envelope`](#envelope) |
158
164
  | GET | `/v1/webhooks/event-types` | `WebhookResource#list_event_types` | Bearer token or `X-Api-Key` | None | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`WebhookEventType`](#webhookeventtype)> } |
@@ -249,6 +255,21 @@ locally together with the `.pdf` extension and `%PDF-` header.
249
255
  with commas; matching documents carry every listed tag. `DocumentResource#replace_tags` and `#append_tags` accept
250
256
  arrays of tag IDs. Use a tag ID with `DocumentResource#detach_tag`.
251
257
 
258
+ ### Contract tracking
259
+
260
+ Contract terms belong to an existing document; setting terms never uploads, sends, or signs the document.
261
+ `ContractResource#update` creates or partially changes the terms. Explicit `nil` values remain JSON null so they
262
+ clear nullable fields. To record a signed amendment, renewal, or termination instrument, upload that document and
263
+ set its `parent_document_id`, `relation_type`, and changed terms; the delta applies after the instrument is
264
+ certificated. `#get` reads the terms or an instrument, and `#related` always reads the root family.
265
+
266
+ The hourly lifecycle moves active contracts through notice, expiry, and automatic renewal. `#renew` starts the
267
+ next term immediately; sending the current `term` makes a lost-response retry safe. `#terminate` closes the
268
+ contract and records its optional reason. Writers can send the `updated_at` they read as an optimistic-lock
269
+ precondition; the API refuses a stale write with `409`. Reads remain available without the contract-tracking
270
+ plan, while writes answer `403` when the plan lacks the feature. OAuth reads require `documents:read`, and writes
271
+ require `documents:write`.
272
+
252
273
  ### KPI statistics
253
274
 
254
275
  `UserResource#stats` and `AccountResource#stats` validate `granularity` (`monthly` or `daily`) and `month`
@@ -357,7 +378,9 @@ if lost, delete and recreate the endpoint. `#list_endpoints(url: registered_url)
357
378
  `#delete_endpoint_by_url(registered_url)` deletes that endpoint, returning `nil`. `WebhookResource#create_endpoint`
358
379
  and `#update_endpoint` accept only `url`, `email`, `events`, `name`, `is_active`, and `signing_enabled`.
359
380
  `#register`, `#get`, and `#inactivate` (`/webhooks/subscriptions`, `/webhooks/inactivate`) act on the account's
360
- oldest endpoint; `#register` accepts only `url`, `email`, `events`, and `is_active`.
381
+ oldest endpoint; `#register` accepts only `url`, `email`, `events`, and `is_active`. New or changed endpoint URLs
382
+ must be absolute HTTPS URLs; the SDK rejects other schemes locally, while the API also refuses private, loopback,
383
+ link-local, and other reserved destination addresses.
361
384
 
362
385
  Each delivery is a JSON `POST` carrying `webhook-id` (stable across attempts of one event to one endpoint; use it to
363
386
  deduplicate), `webhook-timestamp` (Unix seconds), and, when signing is enabled, `webhook-signature`. Any `2xx` is
@@ -385,7 +408,7 @@ return-value, and usage details.
385
408
  | `Assinafy::Client.from_config`, `.from_hash` | Construct a client from string- or symbol-keyed configuration. | [`client.rb`](../lib/assinafy/client.rb) |
386
409
  | `Client#faraday_connection` | Return the configured Faraday connection for advanced integration. | [`client.rb`](../lib/assinafy/client.rb) |
387
410
  | `Client#upload_and_request_signatures` | Upload, optionally wait, create signers, and create a virtual assignment. | [`client.rb`](../lib/assinafy/client.rb) |
388
- | `Client#auth`, `#oauth`, `#accounts`, `#users`, `#documents`, `#signers`, `#signer_documents`, `#assignments`, `#webhooks`, `#templates`, `#fields`, `#tags`, `#webhook_verifier` | Return the client's resource and helper instances. | [`client.rb`](../lib/assinafy/client.rb) |
411
+ | `Client#auth`, `#oauth`, `#accounts`, `#users`, `#documents`, `#contracts`, `#signers`, `#signer_documents`, `#assignments`, `#webhooks`, `#templates`, `#fields`, `#tags`, `#webhook_verifier` | Return the client's resource and helper instances. | [`client.rb`](../lib/assinafy/client.rb) |
389
412
  | `Assinafy::Configuration.new`, `.from_hash` | Build configuration directly or from string/symbol keys. | [`configuration.rb`](../lib/assinafy/configuration.rb) |
390
413
  | `Configuration#auth_headers` | Return the selected API-key, bearer, or empty authentication header set. | [`configuration.rb`](../lib/assinafy/configuration.rb) |
391
414
  | Configuration readers/writers: `api_key`, `token`, `account_id`, `base_url`, `webhook_secret`, `timeout`, `logger` | Read or update configuration values; `base_url=` and `timeout=` validate like the constructor. Construct a new client to apply changes. | [`configuration.rb`](../lib/assinafy/configuration.rb) |
@@ -439,6 +462,8 @@ A workspace account (organization).
439
462
  | `notification_sender_type` | string | No | No | enum: `["User", "Account"]` |
440
463
  | `roles` | Array<string> | No | No | — |
441
464
  | `is_delete_allowed` | boolean | No | No | — |
465
+ | `limits` | object | No | No | Current-plan limits; present only on the single-account response. More keys may be added. |
466
+ | `limits.webhook_endpoints` | integer | No | No | Maximum webhook endpoints: 1, or 3 with the Multiple Webhooks feature. |
442
467
  | `created_at` | string (date-time) | No | No | — |
443
468
 
444
469
  ### AccountTheme
@@ -466,7 +491,9 @@ A request for signers to sign a document.
466
491
  | --- | --- | --- | --- | --- |
467
492
  | `resource` | string | No | No | — |
468
493
  | `id` | string | No | No | — |
469
- | `sender_email` | string (email) | No | No | — |
494
+ | `document_id` | string | No | Yes | Document this assignment belongs to; null only after the document is deleted. |
495
+ | `created_at` | string (date-time) | No | No | — |
496
+ | `sender_email` | string (email) | No | Yes | Null once the sender has deleted their account. |
470
497
  | `method` | string | No | No | enum: `["virtual", "collect"]` |
471
498
  | `expires_at` | string (date-time) | No | Yes | — |
472
499
  | `message` | string | No | Yes | — |
@@ -608,12 +635,14 @@ A document and its current lifecycle state.
608
635
  | `artifacts` | object | No | No | Artifact download URLs keyed by name. Always `original`, plus `thumbnail` once one exists. A certificated document also carries `certificated`, `certificate-page` and `bundle`, and `pades` when it was signed with a digital certificate — the PAdES version holds the signers' ICP-Brasil signatures, which certification flattens out of the certificated PDF. |
609
636
  | `is_closed` | boolean | No | No | — |
610
637
  | `signing_url` | string | No | No | — |
638
+ | `verification_code` | string | No | Yes | Code printed on the certificate page; null until certificated. |
639
+ | `verification_url` | string (uri) | No | Yes | Public verification page; null until certificated. |
611
640
  | `decline_reason` | string | No | Yes | — |
612
641
  | `declined_by` | any JSON value | No | Yes | — |
613
642
  | `tags` | Array<object> | No | No | — |
614
643
  | `tags[].id` | string | No | No | — |
615
644
  | `tags[].name` | string | No | No | — |
616
- | `assignment` | object | No | Yes | Expanded assignment data when included via ?expand=assignment; null otherwise. |
645
+ | `assignment` | object | No | Yes | Included by list/details/signer-list/template-generation responses; search and upload omit it. Only rename follows `?expand=assignment`. |
617
646
  | `assignment.resource` | string | No | No | — |
618
647
  | `assignment.id` | string | No | No | — |
619
648
  | `assignment.sender_email` | string (email) | No | No | — |
@@ -626,6 +655,7 @@ A document and its current lifecycle state.
626
655
  | `assignment.summary` | [`AssignmentSummary`](#assignmentsummary) | No | No | — |
627
656
  | `assignment.signing_urls` | Array<[`SigningUrl`](#signingurl)> | No | No | — |
628
657
  | `pages` | Array<[`DocumentPage`](#documentpage)> | No | No | — |
658
+ | `has_contract` | boolean | No | No | Only on workspace `GET /documents/{documentId}` when contract tracking is available. A missing key means unknown. |
629
659
  | `created_at` | string (date-time) | No | No | — |
630
660
  | `updated_at` | string (date-time) | No | No | — |
631
661
 
@@ -644,6 +674,85 @@ A document activity event.
644
674
  | `origin.user-agent` | string | No | No | — |
645
675
  | `created_at` | string (date-time) | No | No | — |
646
676
 
677
+ ### DocumentContract
678
+
679
+ Contract terms tracked on a document. The single-contract and related-family reads can include amendments; the
680
+ workspace list can include the document when `expand=document` is requested.
681
+
682
+ | Property | Type | Required | Nullable | Constraints / description |
683
+ | --- | --- | --- | --- | --- |
684
+ | `resource` | string | No | No | `document_contract` in single-resource responses. |
685
+ | `document_id` | string | No | No | Document that owns the terms. |
686
+ | `status` | string | No | No | enum: `["draft", "active", "expired", "terminated"]` |
687
+ | `counterparty_name` | string | No | Yes | — |
688
+ | `counterparty_taxpayer_id` | string | No | Yes | Normalized CPF/CNPJ, digits only. |
689
+ | `value` | number | No | Yes | At most two decimal places. |
690
+ | `currency` | string | No | Yes | ISO 4217 code; defaults to BRL when a value is set. |
691
+ | `start_date` | string (date) | No | Yes | — |
692
+ | `end_date` | string (date) | No | Yes | — |
693
+ | `auto_renew` | boolean | No | No | — |
694
+ | `renewal_term_months` | integer | No | Yes | — |
695
+ | `notice_period_days` | integer | No | Yes | — |
696
+ | `term` | integer | No | No | Renewal term number, starting at 1. |
697
+ | `owner_id` | string | No | Yes | Active workspace member responsible for lifecycle email. |
698
+ | `metadata` | object | No | Yes | Up to 50 caller-defined string values. |
699
+ | `external_references` | Array<object> | No | No | IDs in outside systems, sorted by system and ID. |
700
+ | `external_references[].system` | string | No | No | Outside system name. |
701
+ | `external_references[].id` | string | No | No | ID in that system. |
702
+ | `document` | [`Document`](#document) | No | No | Only on list results with `expand=document`. |
703
+ | `amendments` | Array<[`DocumentContractAmendment`](#documentcontractamendment)> | No | No | Only on single-contract/family reads. |
704
+ | `created_at` | string (date-time) | No | No | — |
705
+ | `updated_at` | string (date-time) | No | No | Moves on every direct or lifecycle change. |
706
+
707
+ ### DocumentContractAmendment
708
+
709
+ A document linked as an amendment, renewal, or termination instrument. Its terms are a delta applied to the
710
+ parent after this document is certificated.
711
+
712
+ | Property | Type | Required | Nullable | Constraints / description |
713
+ | --- | --- | --- | --- | --- |
714
+ | `resource` | string | No | No | `document_contract_amendment` in single-resource responses. |
715
+ | `document_id` | string | No | No | The instrument document. |
716
+ | `document_name` | string | No | No | Instrument document name. |
717
+ | `parent_document_id` | string | No | No | Root contract document. |
718
+ | `relation_type` | string | No | No | enum: `["amendment", "renewal", "termination"]` |
719
+ | `number` | integer | No | Yes | Applied position; null while pending. |
720
+ | `status` | string | No | No | enum: `["pending", "applied"]` |
721
+ | `document_status` | string | No | No | Current lifecycle status of the instrument document. |
722
+ | `apply_error` | string | No | Yes | Why a signed instrument has not applied yet. |
723
+ | `currency` | string | No | Yes | Currency used by amount values in `terms` and `changes`. |
724
+ | `terms` | object | No | Yes | Only contract terms changed by this instrument. |
725
+ | `changes` | object | No | Yes | Changed fields as `{ from:, to: }`, previewed while pending and fixed once applied. |
726
+ | `applied_at` | string (date-time) | No | Yes | — |
727
+ | `created_at` | string (date-time) | No | No | — |
728
+ | `updated_at` | string (date-time) | No | No | — |
729
+
730
+ ### DocumentContractInput
731
+
732
+ Partial body for setting terms or linking an instrument. Only sent fields change. JSON null clears a nullable
733
+ term; for a pending instrument it removes that term from the delta. `external_references` replaces the whole list.
734
+
735
+ | Property | Type | Required | Nullable | Constraints / description |
736
+ | --- | --- | --- | --- | --- |
737
+ | `status` | string | No | No | enum: `["draft", "active", "expired", "terminated"]`; closed statuses are creation-only. |
738
+ | `counterparty_name` | string | No | Yes | Maximum 255 characters. |
739
+ | `counterparty_taxpayer_id` | string | No | Yes | Valid formatted or unformatted CPF/CNPJ. |
740
+ | `value` | number | No | Yes | 0–9,999,999,999,999.99; at most two decimal places. |
741
+ | `currency` | string | No | Yes | Three-letter code, stored uppercase. |
742
+ | `start_date` | string (date) | No | Yes | `YYYY-MM-DD`, year 1000 or later. |
743
+ | `end_date` | string (date) | No | Yes | On or after `start_date`. |
744
+ | `auto_renew` | boolean | No | Yes | Null resets to false. |
745
+ | `renewal_term_months` | integer | No | Yes | 1–600; defaults to 12 when unset. |
746
+ | `notice_period_days` | integer | No | Yes | 0–3650. |
747
+ | `owner_id` | string | No | Yes | Active member of the workspace. |
748
+ | `metadata` | object | No | Yes | Up to 50 keys of 1–40 letters, digits, `_`, `-`, or `.`, each holding at most 500 characters. |
749
+ | `external_references` | Array<object> | No | Yes | At most 20; null or an empty array clears the list. |
750
+ | `external_references[].system` | string | Yes | No | Up to 64 letters, digits, `_`, `-`, or `.`; stored lowercase. |
751
+ | `external_references[].id` | string | Yes | No | Up to 255 characters. |
752
+ | `parent_document_id` | string | No | Yes | Links to a contract family; null unlinks a pending instrument. |
753
+ | `relation_type` | string | No | No | Required for the first link; enum: `["amendment", "renewal", "termination"]`. |
754
+ | `updated_at` | string (date-time) | No | Yes | Optional optimistic-lock value; a stale value returns `409`. Null is last-write-wins. |
755
+
647
756
  ### DocumentPage
648
757
 
649
758
  | Property | Type | Required | Nullable | Constraints / description |
@@ -795,6 +904,10 @@ Owner-facing document notifications, keyed by notification type. `true` means th
795
904
  | `DocumentProcessingFailed` | boolean | No | No | An uploaded document could not be processed. |
796
905
  | `TemplateProcessingFailed` | boolean | No | No | A template could not be processed. |
797
906
  | `SignerWhatsappFailed` | boolean | No | No | A WhatsApp notification to a signer could not be delivered. |
907
+ | `ContractRenewalNotice` | boolean | No | No | A tracked contract entered its notice period. |
908
+ | `ContractExpiring` | boolean | No | No | A tracked contract ends in 45 or 30 days. |
909
+ | `ContractExpired` | boolean | No | No | A tracked contract ended without renewal. |
910
+ | `ContractRenewed` | boolean | No | No | A tracked contract was automatically renewed into its next term. |
798
911
 
799
912
  ### OAuthRevokeRequest
800
913
 
@@ -825,6 +938,20 @@ Body of `POST /v1/oauth/token`. Sent form-encoded per RFC 6749, or as JSON.
825
938
  | `subject_token_type` | string | No | No | enum: `["urn:ietf:params:oauth:token-type:access_token"]`; Token-exchange grant only. Required; only `urn:ietf:params:oauth:token-type:access_token` is supported. |
826
939
  | `requested_token_type` | string | No | No | enum: `["urn:ietf:params:oauth:token-type:access_token"]`; Token-exchange grant only. Optional; when present it must agree with the only type this server issues. |
827
940
 
941
+ ### ResendCostEstimate
942
+
943
+ Credits needed to resend one signer's notification. A reminder consumes no document allowance.
944
+
945
+ | Property | Type | Required | Nullable | Constraints / description |
946
+ | --- | --- | --- | --- | --- |
947
+ | `total` | number | No | No | Sum of the breakdown costs. |
948
+ | `breakdown` | Array<object> | No | No | Per-charge resend costs. |
949
+ | `breakdown[].code` | string | No | No | For example `NotificationWhatsappResend`. |
950
+ | `breakdown[].name` | string | No | No | Human-readable charge name. |
951
+ | `breakdown[].cost` | number | No | No | Credits charged for this item. |
952
+ | `credit_balance` | number | No | Yes | Available credits; null when credits are unlimited. |
953
+ | `has_sufficient_credits` | boolean | No | No | Whether the workspace can pay for the resend. |
954
+
828
955
  ### Signer
829
956
 
830
957
  A signing party belonging to a workspace account.
@@ -987,6 +1114,10 @@ A single webhook delivery-history entry.
987
1114
  | `endpoint` | string | No | Yes | URL that received the request. |
988
1115
  | `payload` | object | No | Yes | JSON payload sent to the endpoint. |
989
1116
  | `delivered` | boolean | No | No | Whether delivery succeeded. |
1117
+ | `webhook_id` | string | No | Yes | Stable `webhook-id` header across attempts of this event to this endpoint. |
1118
+ | `attempt` | integer | No | Yes | Attempt number, starting at 1. |
1119
+ | `kind` | string | No | No | enum: `["scheduled", "probe", "manual"]` |
1120
+ | `will_retry` | boolean | No | No | Whether another attempt follows this entry. |
990
1121
  | `http_status` | integer | No | Yes | HTTP status returned (null if connection failed). |
991
1122
  | `response_body` | string | No | Yes | Endpoint response body, truncated to 2000 chars. |
992
1123
  | `error` | string | No | Yes | Delivery error message, if any. |
@@ -999,12 +1130,15 @@ A URL that receives the account's webhook events. Every active endpoint subscrib
999
1130
 
1000
1131
  | Property | Type | Required | Nullable | Constraints / description |
1001
1132
  | --- | --- | --- | --- | --- |
1133
+ | `resource` | string | No | No | Always `activity_event_subscription` in single-resource responses. |
1002
1134
  | `id` | string | No | No | Endpoint ID. |
1003
1135
  | `name` | string | No | Yes | Label to tell endpoints apart. |
1004
- | `url` | string (uri) | No | No | URL that receives the events (http or https). |
1136
+ | `url` | string (uri) | No | No | HTTPS URL that receives the events. |
1005
1137
  | `email` | string (email) | No | No | Contact email for delivery-failure notices. |
1006
1138
  | `events` | Array<string> | No | No | Event types delivered to this endpoint. |
1007
1139
  | `is_active` | boolean | No | No | Whether events are delivered to this endpoint. |
1140
+ | `is_held` | boolean | No | No | Whether deliveries are paused after repeated failures. |
1141
+ | `held_since` | integer | No | Yes | Unix timestamp when the pause began; null while deliveries run. |
1008
1142
  | `signing_enabled` | boolean | No | No | Whether deliveries carry a `webhook-signature` header. |
1009
1143
  | `created_at` | string (date-time) | No | No | — |
1010
1144
  | `updated_at` | string (date-time) | No | No | — |
@@ -1054,6 +1188,8 @@ An account's webhook subscription configuration: the account's oldest webhook en
1054
1188
  | --- | --- | --- | --- | --- |
1055
1189
  | `events` | Array<string> | No | No | Event types subscribed for delivery. |
1056
1190
  | `is_active` | boolean | No | No | Whether webhook delivery is active. |
1191
+ | `is_held` | boolean | No | No | Whether deliveries are paused after repeated failures. |
1192
+ | `held_since` | integer | No | Yes | Unix timestamp when the pause began; null while deliveries run. |
1057
1193
  | `url` | string | No | Yes | Webhook endpoint URL. |
1058
1194
  | `email` | string | No | Yes | Contact email for delivery notices. |
1059
1195
  | `updated_at` | string (date-time) | No | Yes | — |
@@ -33,6 +33,8 @@ module Assinafy
33
33
  attr_reader :users
34
34
  # @return [Resources::DocumentResource]
35
35
  attr_reader :documents
36
+ # @return [Resources::ContractResource]
37
+ attr_reader :contracts
36
38
  # @return [Resources::SignerResource]
37
39
  attr_reader :signers
38
40
  # @return [Resources::SignerDocumentResource]
@@ -84,6 +86,7 @@ module Assinafy
84
86
  @accounts = Resources::AccountResource.new(@connection, account_id, @logger)
85
87
  @users = Resources::UserResource.new(@connection, nil, @logger)
86
88
  @documents = Resources::DocumentResource.new(@connection, account_id, @logger)
89
+ @contracts = Resources::ContractResource.new(@connection, account_id, @logger)
87
90
  @signers = Resources::SignerResource.new(@connection, account_id, @logger)
88
91
  @signer_documents = Resources::SignerDocumentResource.new(@connection, nil, @logger)
89
92
  @assignments = Resources::AssignmentResource.new(@connection, account_id, @logger)
@@ -190,6 +193,7 @@ module Assinafy
190
193
  # # },
191
194
  # # assignment: {
192
195
  # # "resource" => "assignment", "id" => "assignment-id",
196
+ # # "document_id" => "document-id", "created_at" => "2026-06-05T21:21:19Z",
193
197
  # # "sender_email" => "sender@example.com", "method" => "virtual",
194
198
  # # "expires_at" => nil, "message" => "Please review and sign",
195
199
  # # "signers" => [{ "id" => "signer-id", "full_name" => "Example Signer",
@@ -90,6 +90,7 @@ module Assinafy
90
90
  # 'name' => 'MT',
91
91
  # 'primary_color' => nil,
92
92
  # 'secondary_color' => nil,
93
+ # 'limits' => { 'webhook_endpoints' => 1 },
93
94
  # 'created_at' => '2026-05-12T18:05:11Z'
94
95
  # }
95
96
  def get(account_id_override = nil)
@@ -303,6 +303,8 @@ module Assinafy
303
303
  # data: [
304
304
  # {
305
305
  # 'id' => 'assignment-id',
306
+ # 'document_id' => 'document-id',
307
+ # 'created_at' => '2026-06-05T21:21:19Z',
306
308
  # 'sender_email' => 'sender@example.com',
307
309
  # 'method' => 'virtual',
308
310
  # 'expires_at' => nil,
@@ -342,6 +344,7 @@ module Assinafy
342
344
  # # "message" => "Please sign" }
343
345
  # # => {
344
346
  # # "resource" => "assignment", "id" => "assignment-id",
347
+ # # "document_id" => "document-id", "created_at" => "2026-06-05T21:21:19Z",
345
348
  # # "sender_email" => "sender@example.com", "method" => "virtual",
346
349
  # # "expires_at" => nil, "message" => "Please sign",
347
350
  # # "signers" => [{ "id" => "signer-id", "full_name" => "Example Signer",
@@ -475,9 +478,7 @@ module Assinafy
475
478
  # @param document_id [String]
476
479
  # @param assignment_id [String]
477
480
  # @param signer_id [String]
478
- # @return [Hash] cost breakdown (documents, credits, needs_extra_document, extra_document_cost,
479
- # total_credits, breakdown[], document_balance, credit_balance, has_sufficient_resources,
480
- # blocking_reason, message)
481
+ # @return [Hash] resend cost (`total`, `breakdown`, `credit_balance`, `has_sufficient_credits`)
481
482
  # @raise [Assinafy::ApiError] on an unsuccessful API response
482
483
  # @raise [Assinafy::NetworkError] on transport or TLS failure
483
484
  # @raise [Assinafy::ValidationError] on invalid required input
@@ -486,13 +487,11 @@ module Assinafy
486
487
  # resource.estimate_resend_cost("document-id", "assignment-id", "signer-id")
487
488
  # # (no request body)
488
489
  # # => {
489
- # # "documents" => 1, "credits" => 0.45, "needs_extra_document" => false,
490
- # # "extra_document_cost" => 0, "total_credits" => 0.45,
491
- # # "breakdown" => [{ "code" => "NotificationWhatsapp",
492
- # # "name" => "Whatsapp Notification", "cost" => 0.45,
493
- # # "quantity" => 1, "unit_cost" => 0.45 }],
494
- # # "document_balance" => 10, "credit_balance" => 100,
495
- # # "has_sufficient_resources" => true, "blocking_reason" => nil, "message" => nil
490
+ # # "total" => 0.45,
491
+ # # "breakdown" => [{ "code" => "NotificationWhatsappResend",
492
+ # # "name" => "Whatsapp Notification Resend", "cost" => 0.45 }],
493
+ # # "credit_balance" => 100,
494
+ # # "has_sufficient_credits" => true
496
495
  # # }
497
496
  def estimate_resend_cost(document_id, assignment_id, signer_id)
498
497
  doc_id = require_id(document_id, 'Document ID')
@@ -0,0 +1,450 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assinafy
4
+ module Resources
5
+ # Contract terms tracked on workspace documents, including amendments,
6
+ # renewals, terminations, and lifecycle queries.
7
+ class ContractResource < BaseResource
8
+ STATUSES = %w[draft active expired terminated].freeze
9
+ RELATION_TYPES = %w[amendment renewal termination].freeze
10
+ SORTS = %w[end_date -end_date updated_at -updated_at].freeze
11
+ LIST_FIELDS = %w[
12
+ status counterparty search end_date_from end_date_to ending_within_days
13
+ external_system external_id sort expand page per-page
14
+ ].freeze
15
+ UPDATE_FIELDS = %w[
16
+ status counterparty_name counterparty_taxpayer_id value currency start_date end_date
17
+ auto_renew renewal_term_months notice_period_days owner_id metadata external_references
18
+ parent_document_id relation_type updated_at
19
+ ].freeze
20
+ RENEW_FIELDS = %w[end_date value term updated_at].freeze
21
+ TERMINATE_FIELDS = %w[reason updated_at].freeze
22
+
23
+ # List tracked contracts in a workspace, most recently updated first.
24
+ #
25
+ # @param params [Hash] optional filters: `status`, `counterparty`, `search`,
26
+ # `end_date_from`, `end_date_to`, `ending_within_days`, `external_system`,
27
+ # `external_id`, `sort`, `expand`, `page`, and `per_page`
28
+ # @param account_id_override [String, nil]
29
+ # @return [Hash{Symbol=>Array,Hash}] `{ data: [...], meta: { ... } }`
30
+ # @raise [Assinafy::ApiError] on an unsuccessful API response
31
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
32
+ # @raise [Assinafy::ValidationError] on invalid filters or account ID
33
+ # @see GET /accounts/{accountId}/contracts
34
+ # @example List active contracts ending in the next 30 days
35
+ # client.contracts.list(status: ['active'], ending_within_days: 30, expand: 'document')
36
+ # # Request: GET /accounts/{accountId}/contracts?status=active&ending_within_days=30&expand=document
37
+ # # => {
38
+ # # data: [{
39
+ # # 'resource' => 'document_contract', 'document_id' => 'document-id',
40
+ # # 'status' => 'active', 'counterparty_name' => 'Example Ltda',
41
+ # # 'counterparty_taxpayer_id' => '11222333000181', 'value' => 120000.50,
42
+ # # 'currency' => 'BRL', 'start_date' => '2026-11-01', 'end_date' => '2027-10-31',
43
+ # # 'auto_renew' => true, 'renewal_term_months' => 12, 'notice_period_days' => 30,
44
+ # # 'term' => 1, 'owner_id' => 'user-id', 'metadata' => { 'crm_stage' => 'won' },
45
+ # # 'external_references' => [{ 'system' => 'hubspot', 'id' => '98765' }],
46
+ # # 'document' => { 'id' => 'document-id', 'name' => 'agreement.pdf', 'status' => 'certificated' },
47
+ # # 'created_at' => '2026-10-23T12:00:00Z', 'updated_at' => '2026-10-23T12:00:00Z'
48
+ # # }],
49
+ # # meta: { current_page: 1, per_page: 20, total: 1, last_page: 1 }
50
+ # # }
51
+ def list(params = {}, account_id_override = nil)
52
+ acc_id = account_id(account_id_override)
53
+ query = contract_list_params(params)
54
+
55
+ call_list('Failed to list contracts') do
56
+ http_get("accounts/#{acc_id}/contracts", query)
57
+ end
58
+ end
59
+
60
+ # Fetch the contract terms or pending/applied amendment linked to a document.
61
+ # Returns `nil` when the document has no tracked contract terms.
62
+ #
63
+ # @param document_id [String]
64
+ # @param account_id_override [String, nil]
65
+ # @return [Hash, nil] a DocumentContract or DocumentContractAmendment
66
+ # @raise [Assinafy::ApiError] on an unsuccessful response other than 404
67
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
68
+ # @raise [Assinafy::ValidationError] on an invalid ID
69
+ # @see GET /accounts/{accountId}/documents/{documentId}/contract
70
+ # @example Read a contract and its amendments
71
+ # client.contracts.get('document-id')
72
+ # # Request: GET /accounts/{accountId}/documents/document-id/contract
73
+ # # => {
74
+ # # 'resource' => 'document_contract', 'document_id' => 'document-id', 'status' => 'active',
75
+ # # 'counterparty_name' => 'Example Ltda', 'counterparty_taxpayer_id' => '11222333000181',
76
+ # # 'value' => 130000, 'currency' => 'BRL', 'start_date' => '2026-11-01',
77
+ # # 'end_date' => '2028-06-30', 'auto_renew' => true, 'renewal_term_months' => 12,
78
+ # # 'notice_period_days' => 30, 'term' => 2, 'owner_id' => 'user-id',
79
+ # # 'metadata' => { 'crm_stage' => 'won' },
80
+ # # 'external_references' => [{ 'system' => 'hubspot', 'id' => '98765' }],
81
+ # # 'amendments' => [{
82
+ # # 'resource' => 'document_contract_amendment', 'document_id' => 'amendment-id',
83
+ # # 'document_name' => 'Amendment 1.pdf', 'parent_document_id' => 'document-id',
84
+ # # 'relation_type' => 'amendment', 'number' => 1, 'status' => 'applied',
85
+ # # 'document_status' => 'certificated', 'apply_error' => nil, 'currency' => 'BRL',
86
+ # # 'terms' => { 'value' => 130000, 'end_date' => '2028-06-30' },
87
+ # # 'changes' => { 'value' => { 'from' => 120000.50, 'to' => 130000 } },
88
+ # # 'applied_at' => '2027-05-12T18:20:00Z',
89
+ # # 'created_at' => '2027-05-10T15:00:00Z', 'updated_at' => '2027-05-12T18:20:00Z'
90
+ # # }],
91
+ # # 'created_at' => '2026-10-23T12:00:00Z', 'updated_at' => '2027-05-12T18:20:00Z'
92
+ # # }
93
+ def get(document_id, account_id_override = nil)
94
+ call_optional('Failed to fetch contract') do
95
+ http_get(contract_path(document_id, account_id_override))
96
+ end
97
+ end
98
+
99
+ # Create or partially update contract terms, or link a document as an
100
+ # amendment, renewal, or termination instrument. Explicit `nil` values
101
+ # are sent as JSON null because the API uses them to clear terms.
102
+ #
103
+ # @param document_id [String]
104
+ # @param payload [Hash] at least one DocumentContractInput property
105
+ # @param account_id_override [String, nil]
106
+ # @return [Hash] the saved DocumentContract or DocumentContractAmendment
107
+ # @raise [Assinafy::ApiError] including 403 without the plan and 409 on a stale `updated_at`
108
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
109
+ # @raise [Assinafy::ValidationError] on invalid IDs or payload fields
110
+ # @see PUT /accounts/{accountId}/documents/{documentId}/contract
111
+ # @example Set a document's contract terms
112
+ # client.contracts.update(
113
+ # 'document-id', status: 'active', counterparty_name: 'Example Ltda',
114
+ # value: 120000.50, currency: 'BRL', start_date: '2026-11-01', end_date: '2027-10-31',
115
+ # auto_renew: true, renewal_term_months: 12, notice_period_days: 30,
116
+ # metadata: { crm_stage: 'won' },
117
+ # external_references: [{ system: 'hubspot', id: '98765' }]
118
+ # )
119
+ # # Request body:
120
+ # # { "status":"active", "counterparty_name":"Example Ltda", "value":120000.5,
121
+ # # "currency":"BRL", "start_date":"2026-11-01", "end_date":"2027-10-31",
122
+ # # "auto_renew":true, "renewal_term_months":12, "notice_period_days":30,
123
+ # # "metadata":{"crm_stage":"won"},
124
+ # # "external_references":[{"system":"hubspot","id":"98765"}] }
125
+ # # => { 'resource' => 'document_contract', 'document_id' => 'document-id', 'status' => 'active',
126
+ # # 'counterparty_name' => 'Example Ltda', 'counterparty_taxpayer_id' => nil,
127
+ # # 'value' => 120000.50, 'currency' => 'BRL', 'start_date' => '2026-11-01',
128
+ # # 'end_date' => '2027-10-31', 'auto_renew' => true, 'renewal_term_months' => 12,
129
+ # # 'notice_period_days' => 30, 'term' => 1, 'owner_id' => nil,
130
+ # # 'metadata' => { 'crm_stage' => 'won' },
131
+ # # 'external_references' => [{ 'system' => 'hubspot', 'id' => '98765' }],
132
+ # # 'created_at' => '2026-10-23T12:00:00Z', 'updated_at' => '2026-10-23T12:00:00Z' }
133
+ # @example Link a signed amendment and preserve nulls
134
+ # client.contracts.update(
135
+ # 'amendment-id', parent_document_id: 'document-id', relation_type: 'amendment',
136
+ # value: 130000, counterparty_name: nil
137
+ # )
138
+ # # Request body includes "counterparty_name": null.
139
+ def update(document_id, payload, account_id_override = nil)
140
+ body = contract_payload(payload, UPDATE_FIELDS, 'Contract payload')
141
+ raise ValidationError.new('Contract payload must change at least one field') if body.empty?
142
+
143
+ validate_contract_input!(body)
144
+
145
+ call('Failed to update contract') do
146
+ http_put(contract_path(document_id, account_id_override), body)
147
+ end
148
+ end
149
+
150
+ # Fetch the root contract and every related amendment, renewal, and
151
+ # termination document. Returns `nil` when the document has no family.
152
+ #
153
+ # @param document_id [String]
154
+ # @param account_id_override [String, nil]
155
+ # @return [Hash, nil] root DocumentContract with `amendments`
156
+ # @raise [Assinafy::ApiError] on an unsuccessful response other than 404
157
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
158
+ # @raise [Assinafy::ValidationError] on an invalid ID
159
+ # @see GET /accounts/{accountId}/documents/{documentId}/contract/related
160
+ # @example Read a family from one of its amendments
161
+ # client.contracts.related('amendment-id')
162
+ # # Request: GET /accounts/{accountId}/documents/amendment-id/contract/related
163
+ # # => { 'resource' => 'document_contract', 'document_id' => 'document-id', 'status' => 'active',
164
+ # # 'counterparty_name' => 'Example Ltda', 'counterparty_taxpayer_id' => '11222333000181',
165
+ # # 'value' => 130000, 'currency' => 'BRL', 'start_date' => '2026-11-01',
166
+ # # 'end_date' => '2028-06-30', 'auto_renew' => true, 'renewal_term_months' => 12,
167
+ # # 'notice_period_days' => 30, 'term' => 2, 'owner_id' => 'user-id',
168
+ # # 'metadata' => nil, 'external_references' => [],
169
+ # # 'amendments' => [{ 'document_id' => 'amendment-id', 'relation_type' => 'amendment',
170
+ # # 'number' => 1, 'status' => 'applied', 'terms' => { 'value' => 130000 } }],
171
+ # # 'created_at' => '2026-10-23T12:00:00Z', 'updated_at' => '2027-05-12T18:20:00Z' }
172
+ def related(document_id, account_id_override = nil)
173
+ call_optional('Failed to fetch related contracts') do
174
+ http_get("#{contract_path(document_id, account_id_override)}/related")
175
+ end
176
+ end
177
+
178
+ # Start the contract's next term. Send the current `term` for an
179
+ # idempotent term-based renewal, or an explicit later `end_date`.
180
+ # Explicit `value: nil` clears the contract value.
181
+ #
182
+ # @param document_id [String]
183
+ # @param payload [Hash] `term` or `end_date`, plus optional `value` and `updated_at`
184
+ # @param account_id_override [String, nil]
185
+ # @return [Hash] the renewed DocumentContract
186
+ # @raise [Assinafy::ApiError] including 403 without the plan and 409 on a stale `updated_at`
187
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
188
+ # @raise [Assinafy::ValidationError] on invalid IDs or payload fields
189
+ # @see POST /accounts/{accountId}/documents/{documentId}/contract/renew
190
+ # @example Renew from the current term
191
+ # client.contracts.renew('document-id', term: 1, value: 126000)
192
+ # # Request body: { "term": 1, "value": 126000 }
193
+ # # => { 'resource' => 'document_contract', 'document_id' => 'document-id', 'status' => 'active',
194
+ # # 'counterparty_name' => 'Example Ltda', 'counterparty_taxpayer_id' => '11222333000181',
195
+ # # 'value' => 126000, 'currency' => 'BRL', 'start_date' => '2026-11-01',
196
+ # # 'end_date' => '2028-10-31', 'auto_renew' => true, 'renewal_term_months' => 12,
197
+ # # 'notice_period_days' => 30, 'term' => 2, 'owner_id' => 'user-id',
198
+ # # 'metadata' => nil, 'external_references' => [],
199
+ # # 'created_at' => '2026-10-23T12:00:00Z', 'updated_at' => '2027-10-15T09:30:00Z' }
200
+ def renew(document_id, payload, account_id_override = nil)
201
+ body = contract_payload(payload, RENEW_FIELDS, 'Contract renewal payload')
202
+ validate_renewal!(body)
203
+
204
+ call('Failed to renew contract') do
205
+ http_post("#{contract_path(document_id, account_id_override)}/renew", body)
206
+ end
207
+ end
208
+
209
+ # Terminate a contract and stop lifecycle alerts. The optional reason is
210
+ # recorded in the document activity event.
211
+ #
212
+ # @param document_id [String]
213
+ # @param payload [Hash] optional `reason` and optimistic-lock `updated_at`
214
+ # @param account_id_override [String, nil]
215
+ # @return [Hash] the terminated DocumentContract
216
+ # @raise [Assinafy::ApiError] including 403 without the plan and 409 on a stale `updated_at`
217
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
218
+ # @raise [Assinafy::ValidationError] on invalid IDs or payload fields
219
+ # @see POST /accounts/{accountId}/documents/{documentId}/contract/terminate
220
+ # @example Terminate with a reason
221
+ # client.contracts.terminate('document-id', reason: 'Supplier replaced')
222
+ # # Request body: { "reason": "Supplier replaced" }
223
+ # # => { 'resource' => 'document_contract', 'document_id' => 'document-id', 'status' => 'terminated',
224
+ # # 'counterparty_name' => 'Example Ltda', 'counterparty_taxpayer_id' => '11222333000181',
225
+ # # 'value' => 120000.50, 'currency' => 'BRL', 'start_date' => '2026-11-01',
226
+ # # 'end_date' => '2027-10-31', 'auto_renew' => true, 'renewal_term_months' => 12,
227
+ # # 'notice_period_days' => 30, 'term' => 1, 'owner_id' => 'user-id',
228
+ # # 'metadata' => nil, 'external_references' => [],
229
+ # # 'created_at' => '2026-10-23T12:00:00Z', 'updated_at' => '2027-03-02T14:00:00Z' }
230
+ def terminate(document_id, payload = {}, account_id_override = nil)
231
+ body = contract_payload(payload, TERMINATE_FIELDS, 'Contract termination payload')
232
+ validate_optional_string!(body, 'reason', 'Termination reason', max_length: 500)
233
+ validate_optional_string!(body, 'updated_at', 'updated_at')
234
+
235
+ call('Failed to terminate contract') do
236
+ http_post("#{contract_path(document_id, account_id_override)}/terminate", body)
237
+ end
238
+ end
239
+
240
+ private
241
+
242
+ def contract_path(document_id, account_id_override)
243
+ doc_id = require_id(document_id, 'Document ID')
244
+ "accounts/#{account_id(account_id_override)}/documents/#{doc_id}/contract"
245
+ end
246
+
247
+ def contract_list_params(params)
248
+ query = query_params(require_payload(params, 'Contract filters'))
249
+ reject_unknown_fields!(query, LIST_FIELDS, 'contract filters')
250
+ validate_status_filter!(query)
251
+ %w[counterparty search external_system external_id].each do |field|
252
+ validate_optional_string!(query, field, field)
253
+ end
254
+ %w[end_date_from end_date_to].each { |field| validate_optional_date!(query, field) }
255
+ if query['end_date_from'] && query['end_date_to'] && query['end_date_from'] > query['end_date_to']
256
+ raise ValidationError.new('end_date_from must not be after end_date_to')
257
+ end
258
+
259
+ if query.key?('ending_within_days')
260
+ validate_integer!(query['ending_within_days'], 'ending_within_days', 0, 3650)
261
+ end
262
+ validate_integer!(query['page'], 'page', 1) if query.key?('page')
263
+ validate_integer!(query['per-page'], 'per_page', 1, 100) if query.key?('per-page')
264
+ validate_enum!(query, 'sort', SORTS)
265
+ validate_enum!(query, 'expand', %w[document])
266
+ query
267
+ end
268
+
269
+ def validate_status_filter!(query)
270
+ return unless query.key?('status')
271
+
272
+ statuses = query['status'].is_a?(Array) ? query['status'] : query['status'].to_s.split(',')
273
+ unless !statuses.empty? && statuses.all? { |status| status.is_a?(String) && STATUSES.include?(status) }
274
+ raise ValidationError.new("status must contain only: #{STATUSES.join(', ')}")
275
+ end
276
+
277
+ query['status'] = statuses.join(',')
278
+ end
279
+
280
+ def contract_payload(payload, allowed, name)
281
+ raw = require_payload(payload, name)
282
+ body = body_params(raw)
283
+ raw.each { |key, value| body[key.to_s] = nil if value.nil? }
284
+ reject_unknown_fields!(body, allowed, name.downcase)
285
+ body
286
+ end
287
+
288
+ def validate_contract_input!(body)
289
+ validate_enum!(body, 'status', STATUSES)
290
+ validate_enum!(body, 'relation_type', RELATION_TYPES)
291
+ validate_contract_strings!(body)
292
+ validate_contract_dates!(body)
293
+ validate_contract_numbers!(body)
294
+ validate_contract_collections!(body)
295
+ end
296
+
297
+ def validate_contract_strings!(body)
298
+ validate_optional_string!(body, 'counterparty_name', 'counterparty_name', max_length: 255)
299
+ validate_optional_string!(body, 'counterparty_taxpayer_id', 'counterparty_taxpayer_id')
300
+ validate_optional_string!(body, 'owner_id', 'owner_id')
301
+ validate_optional_timestamp!(body, 'updated_at')
302
+ validate_parent_document_id!(body)
303
+ validate_currency!(body)
304
+ end
305
+
306
+ def validate_contract_dates!(body)
307
+ %w[start_date end_date].each { |field| validate_optional_date!(body, field, minimum_year: 1000) }
308
+ if body['start_date'] && body['end_date'] && body['start_date'] > body['end_date']
309
+ raise ValidationError.new('end_date must not be before start_date')
310
+ end
311
+ end
312
+
313
+ def validate_contract_numbers!(body)
314
+ if body.key?('value') && !body['value'].nil?
315
+ validate_number!(body['value'], 'value', 0, 9_999_999_999_999.99)
316
+ end
317
+ validate_boolean!(body, 'auto_renew')
318
+ validate_integer!(body['renewal_term_months'], 'renewal_term_months', 1, 600) if body['renewal_term_months']
319
+ validate_integer!(body['notice_period_days'], 'notice_period_days', 0, 3650) if body['notice_period_days']
320
+ end
321
+
322
+ def validate_contract_collections!(body)
323
+ validate_metadata!(body['metadata']) if body.key?('metadata') && !body['metadata'].nil?
324
+ if body.key?('external_references') && !body['external_references'].nil?
325
+ validate_external_references!(body['external_references'])
326
+ end
327
+ end
328
+
329
+ def validate_renewal!(body)
330
+ unless body['term'] || body['end_date']
331
+ raise ValidationError.new('Contract renewal requires term or end_date')
332
+ end
333
+
334
+ validate_optional_date!(body, 'end_date')
335
+ validate_integer!(body['term'], 'term', 1) if body.key?('term')
336
+ validate_number!(body['value'], 'value', 0, 9_999_999_999_999.99) if body.key?('value') && !body['value'].nil?
337
+ validate_optional_timestamp!(body, 'updated_at')
338
+ end
339
+
340
+ def validate_parent_document_id!(body)
341
+ return unless body.key?('parent_document_id') && !body['parent_document_id'].nil?
342
+
343
+ require_id(body['parent_document_id'], 'parent_document_id')
344
+ end
345
+
346
+ def validate_currency!(body)
347
+ return unless body.key?('currency') && !body['currency'].nil?
348
+
349
+ currency = require_string(body['currency'], 'currency')
350
+ raise ValidationError.new('currency must be a three-letter code') unless currency.match?(/\A[A-Za-z]{3}\z/)
351
+ end
352
+
353
+ def validate_optional_date!(body, field, minimum_year: nil)
354
+ return unless body.key?(field) && !body[field].nil?
355
+
356
+ value = require_string(body[field], field)
357
+ date = Date.iso8601(value)
358
+ raise ArgumentError unless value.match?(/\A\d{4}-\d{2}-\d{2}\z/) && (!minimum_year || date.year >= minimum_year)
359
+ rescue ArgumentError
360
+ raise ValidationError.new("#{field} must be YYYY-MM-DD")
361
+ end
362
+
363
+ def validate_optional_string!(body, field, name, max_length: nil)
364
+ return unless body.key?(field) && !body[field].nil?
365
+
366
+ require_string(body[field], name, max_length: max_length)
367
+ end
368
+
369
+ def validate_enum!(body, field, allowed)
370
+ return unless body.key?(field)
371
+
372
+ value = require_string(body[field], field)
373
+ return if allowed.include?(value)
374
+
375
+ raise ValidationError.new("#{field} must be one of: #{allowed.join(', ')}")
376
+ end
377
+
378
+ def validate_boolean!(body, field)
379
+ require_boolean(body[field], field) if body.key?(field) && !body[field].nil?
380
+ end
381
+
382
+ def validate_optional_timestamp!(body, field)
383
+ return unless body.key?(field) && !body[field].nil?
384
+
385
+ value = require_string(body[field], field)
386
+ DateTime.iso8601(value)
387
+ return if value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\z/)
388
+
389
+ raise ArgumentError
390
+ rescue ArgumentError
391
+ raise ValidationError.new("#{field} must be an ISO 8601 timestamp with a timezone")
392
+ end
393
+
394
+ def validate_integer!(value, name, minimum, maximum = nil)
395
+ valid = value.is_a?(Integer) && value >= minimum && (maximum.nil? || value <= maximum)
396
+ return if valid
397
+
398
+ range = maximum ? "between #{minimum} and #{maximum}" : "at least #{minimum}"
399
+ raise ValidationError.new("#{name} must be an Integer #{range}")
400
+ end
401
+
402
+ def validate_number!(value, name, minimum, maximum = nil)
403
+ number = value.is_a?(Integer) || value.is_a?(Float)
404
+ finite = number && (!value.respond_to?(:finite?) || value.finite?)
405
+ valid = finite && value.round(2) == value && value >= minimum && (maximum.nil? || value <= maximum)
406
+ return if valid
407
+
408
+ raise ValidationError.new("#{name} must be a finite number in the documented range")
409
+ end
410
+
411
+ def validate_metadata!(metadata)
412
+ unless metadata.is_a?(Hash) && metadata.length <= 50
413
+ raise ValidationError.new('metadata must be a Hash with at most 50 entries')
414
+ end
415
+
416
+ metadata.each do |key, value|
417
+ valid_key = key.to_s.match?(/\A[A-Za-z0-9_.-]{1,40}\z/)
418
+ valid_value = value.is_a?(String) && value.length <= 500
419
+ next if valid_key && valid_value
420
+
421
+ raise ValidationError.new('metadata keys or values are invalid')
422
+ end
423
+ end
424
+
425
+ def validate_external_references!(references)
426
+ unless references.is_a?(Array) && references.length <= 20
427
+ raise ValidationError.new('external_references must be an Array with at most 20 entries')
428
+ end
429
+
430
+ references.each do |reference|
431
+ unless reference.is_a?(Hash)
432
+ raise ValidationError.new('Each external reference must be a Hash')
433
+ end
434
+
435
+ ref = reference.transform_keys(&:to_s)
436
+ system = require_string(ref['system'], 'External reference system', max_length: 64)
437
+ require_string(ref['id'], 'External reference ID', max_length: 255)
438
+ unless system.match?(/\A[A-Za-z0-9_.-]+\z/)
439
+ raise ValidationError.new('External reference system contains invalid characters')
440
+ end
441
+ end
442
+ end
443
+
444
+ def reject_unknown_fields!(body, allowed, name)
445
+ unknown = body.keys - allowed
446
+ raise ValidationError.new("Unknown #{name}: #{unknown.join(', ')}") unless unknown.empty?
447
+ end
448
+ end
449
+ end
450
+ end
@@ -40,6 +40,8 @@ module Assinafy
40
40
  # },
41
41
  # 'is_closed' => false,
42
42
  # 'signing_url' => 'https://app-sandbox.assinafy.com.br/sign/document-id',
43
+ # 'verification_code' => nil,
44
+ # 'verification_url' => nil,
43
45
  # 'decline_reason' => nil,
44
46
  # 'declined_by' => nil,
45
47
  # 'tags' => [],
@@ -233,6 +235,7 @@ module Assinafy
233
235
  # 'declined_by' => nil,
234
236
  # 'tags' => [],
235
237
  # 'assignment' => nil,
238
+ # 'has_contract' => false,
236
239
  # 'pages' => [
237
240
  # { 'id' => 'page-id', 'number' => 1, 'height' => 1651,
238
241
  # 'width' => 1275, 'download_url' => 'https://sandbox.assinafy.com.br/v1/documents/.../download' }
@@ -16,6 +16,10 @@ module Assinafy
16
16
  DocumentProcessingFailed
17
17
  TemplateProcessingFailed
18
18
  SignerWhatsappFailed
19
+ ContractRenewalNotice
20
+ ContractExpiring
21
+ ContractExpired
22
+ ContractRenewed
19
23
  ].freeze
20
24
 
21
25
  # Fetch the authenticated user's profile. The current OpenAPI response is
@@ -117,11 +121,11 @@ module Assinafy
117
121
  end
118
122
  end
119
123
 
120
- # Fetch all owner-facing document email preferences. All nine keys are
124
+ # Fetch all owner-facing document and contract email preferences. All thirteen keys are
121
125
  # returned and default to true. Account and security email is not
122
126
  # configurable through this endpoint.
123
127
  #
124
- # @return [Hash{String=>Boolean}] all nine documented preference codes
128
+ # @return [Hash{String=>Boolean}] all thirteen documented preference codes
125
129
  # @raise [Assinafy::ApiError] on an unsuccessful API response
126
130
  # @raise [Assinafy::NetworkError] on transport or TLS failure
127
131
  # @raise [Assinafy::ValidationError] on invalid required input
@@ -139,7 +143,11 @@ module Assinafy
139
143
  # 'DocumentExpirationReset' => true,
140
144
  # 'DocumentProcessingFailed' => true,
141
145
  # 'TemplateProcessingFailed' => true,
142
- # 'SignerWhatsappFailed' => true
146
+ # 'SignerWhatsappFailed' => true,
147
+ # 'ContractRenewalNotice' => true,
148
+ # 'ContractExpiring' => true,
149
+ # 'ContractExpired' => true,
150
+ # 'ContractRenewed' => true
143
151
  # }
144
152
  def notification_preferences
145
153
  call('Failed to fetch notification preferences') do
@@ -147,10 +155,10 @@ module Assinafy
147
155
  end
148
156
  end
149
157
 
150
- # Merge selected owner-facing document email preferences.
158
+ # Merge selected owner-facing document and contract email preferences.
151
159
  #
152
160
  # Omitted keys keep their current values. The API returns the full
153
- # nine-key map shown by {#notification_preferences}.
161
+ # thirteen-key map shown by {#notification_preferences}.
154
162
  #
155
163
  # @param preferences [Hash{String,Symbol=>Boolean}] non-empty partial map
156
164
  # @return [Hash{String=>Boolean}] the full updated map
@@ -174,7 +182,11 @@ module Assinafy
174
182
  # 'DocumentExpirationReset' => true,
175
183
  # 'DocumentProcessingFailed' => true,
176
184
  # 'TemplateProcessingFailed' => true,
177
- # 'SignerWhatsappFailed' => true
185
+ # 'SignerWhatsappFailed' => true,
186
+ # 'ContractRenewalNotice' => true,
187
+ # 'ContractExpiring' => true,
188
+ # 'ContractExpired' => true,
189
+ # 'ContractRenewed' => true
178
190
  # }
179
191
  def update_notification_preferences(preferences)
180
192
  preferences = require_payload(preferences, 'Notification preferences')
@@ -28,7 +28,8 @@ module Assinafy
28
28
  # @option payload [Array<String>] :events event-type IDs (see {#list_event_types})
29
29
  # @option payload [Boolean] :is_active default `true` when omitted
30
30
  # @param account_id_override [String, nil]
31
- # @return [Hash] the subscription object: { events:, is_active:, url:, email:, updated_at: }
31
+ # @return [Hash] the subscription object: { events:, is_active:, is_held:, held_since:, url:, email:,
32
+ # updated_at: }
32
33
  # @raise [Assinafy::ApiError] on an unsuccessful API response
33
34
  # @raise [Assinafy::NetworkError] on transport or TLS failure
34
35
  # @raise [Assinafy::ValidationError] on invalid required input
@@ -51,6 +52,8 @@ module Assinafy
51
52
  # # {
52
53
  # # 'events' => ["document_ready", "document_prepared"],
53
54
  # # 'is_active' => true,
55
+ # # 'is_held' => false,
56
+ # # 'held_since' => nil,
54
57
  # # 'url' => "https://example.com/webhook",
55
58
  # # 'email' => "ops@example.com",
56
59
  # # 'updated_at' => "2026-06-05T21:13:24Z"
@@ -84,6 +87,8 @@ module Assinafy
84
87
  # # {
85
88
  # # 'events' => ["document_ready", "signer_signed_document"],
86
89
  # # 'is_active' => false,
90
+ # # 'is_held' => false,
91
+ # # 'held_since' => nil,
87
92
  # # 'url' => "https://example.com/sdk-smoke-webhook",
88
93
  # # 'email' => "webhook@example.com",
89
94
  # # 'updated_at' => "2026-06-05T21:13:24Z"
@@ -113,6 +118,8 @@ module Assinafy
113
118
  # # {
114
119
  # # 'events' => ["document_ready", "document_prepared"],
115
120
  # # 'is_active' => false,
121
+ # # 'is_held' => false,
122
+ # # 'held_since' => nil,
116
123
  # # 'url' => "https://example.com/webhook",
117
124
  # # 'email' => "ops@example.com",
118
125
  # # 'updated_at' => "2026-06-05T21:13:24Z"
@@ -178,6 +185,10 @@ module Assinafy
178
185
  # # endpoint: "https://example.com/webhook",
179
186
  # # payload: { 'id' => 15431, event: "signature_requested", object: {}, subject: {}, payload: {} },
180
187
  # # delivered: false,
188
+ # # webhook_id: "webhook-id-placeholder",
189
+ # # attempt: 2,
190
+ # # kind: "scheduled",
191
+ # # will_retry: false,
181
192
  # # http_status: 404,
182
193
  # # response_body: "{\"success\":false,...}",
183
194
  # # error: "Client error: `POST https://example.com/webhook` resulted in a 404 ...",
@@ -217,6 +228,10 @@ module Assinafy
217
228
  # # endpoint: "https://example.com/webhook",
218
229
  # # payload: { 'id' => 15431, event: "signature_requested", object: {}, subject: {} },
219
230
  # # delivered: true,
231
+ # # webhook_id: "webhook-id-placeholder",
232
+ # # attempt: 1,
233
+ # # kind: "manual",
234
+ # # will_retry: false,
220
235
  # # http_status: 200,
221
236
  # # response_body: "OK",
222
237
  # # error: nil,
@@ -247,12 +262,15 @@ module Assinafy
247
262
  # # => unwrapped data payload returned:
248
263
  # # [
249
264
  # # {
265
+ # # 'resource' => "activity_event_subscription",
250
266
  # # 'id' => "webhook-endpoint-id",
251
267
  # # 'name' => "ERP",
252
268
  # # 'url' => "https://example.com/webhooks/assinafy",
253
269
  # # 'email' => "ops@example.com",
254
270
  # # 'events' => ["document_ready", "signer_signed_document"],
255
271
  # # 'is_active' => true,
272
+ # # 'is_held' => false,
273
+ # # 'held_since' => nil,
256
274
  # # 'signing_enabled' => true,
257
275
  # # 'created_at' => "2026-10-01T12:00:00Z",
258
276
  # # 'updated_at' => "2026-10-01T12:00:00Z"
@@ -274,7 +292,7 @@ module Assinafy
274
292
  # especially for OAuth apps, which cannot call {#endpoint_secret}.
275
293
  #
276
294
  # @param payload [Hash]
277
- # @option payload [String] :url http(s) URL that receives the events (required)
295
+ # @option payload [String] :url HTTPS URL that receives the events (required)
278
296
  # @option payload [String] :email contact email for delivery-failure notices (required)
279
297
  # @option payload [Array<String>] :events event-type IDs (see {#list_event_types}) (required)
280
298
  # @option payload [String] :name label to tell endpoints apart
@@ -304,8 +322,10 @@ module Assinafy
304
322
  # # "signing_enabled": true
305
323
  # # }
306
324
  # # => unwrapped data payload returned (same shape as {#list_endpoints} entries):
307
- # # { 'id' => "webhook-endpoint-id", 'name' => "ERP", 'url' => "https://example.com/webhooks/assinafy",
308
- # # 'email' => "ops@example.com", 'events' => [...], 'is_active' => true, 'signing_enabled' => true,
325
+ # # { 'resource' => "activity_event_subscription", 'id' => "webhook-endpoint-id", 'name' => "ERP",
326
+ # # 'url' => "https://example.com/webhooks/assinafy", 'email' => "ops@example.com",
327
+ # # 'events' => [...], 'is_active' => true, 'is_held' => false, 'held_since' => nil,
328
+ # # 'signing_enabled' => true,
309
329
  # # 'created_at' => "2026-10-01T12:00:00Z", 'updated_at' => "2026-10-01T12:00:00Z",
310
330
  # # 'secret' => 'whsec_secret-placeholder' } # secret appears only on creation with signing enabled
311
331
  def create_endpoint(payload, account_id_override = nil)
@@ -481,7 +501,7 @@ module Assinafy
481
501
  end
482
502
 
483
503
  def validate_webhook_fields!(body)
484
- require_string(body[:url], 'Webhook URL') if body.key?(:url)
504
+ validate_webhook_url!(body[:url]) if body.key?(:url)
485
505
  Utils.require_email(body[:email]) if body.key?(:email)
486
506
  require_string(body[:name], 'Webhook name') if body.key?(:name)
487
507
  %i[is_active signing_enabled].each { |key| require_boolean(body[key], key.to_s) if body.key?(key) }
@@ -492,6 +512,16 @@ module Assinafy
492
512
 
493
513
  raise ValidationError.new('Webhook events must be non-empty Strings')
494
514
  end
515
+
516
+ def validate_webhook_url!(value)
517
+ url = require_string(value, 'Webhook URL')
518
+ uri = URI.parse(url)
519
+ return if uri.is_a?(URI::HTTPS) && uri.host && !uri.host.empty?
520
+
521
+ raise ValidationError.new('Webhook URL must be an absolute HTTPS URL')
522
+ rescue URI::InvalidURIError
523
+ raise ValidationError.new('Webhook URL must be an absolute HTTPS URL')
524
+ end
495
525
  end
496
526
  end
497
527
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Assinafy
4
- VERSION = '2.0.1'
4
+ VERSION = '2.1.0'
5
5
  USER_AGENT = "Assinafy-Ruby-SDK/v#{VERSION}".freeze
6
6
  end
data/lib/assinafy.rb CHANGED
@@ -17,6 +17,7 @@ require_relative 'assinafy/resources/oauth_resource'
17
17
  require_relative 'assinafy/resources/account_resource'
18
18
  require_relative 'assinafy/resources/user_resource'
19
19
  require_relative 'assinafy/resources/document_resource'
20
+ require_relative 'assinafy/resources/contract_resource'
20
21
  require_relative 'assinafy/resources/signer_resource'
21
22
  require_relative 'assinafy/resources/signer_document_resource'
22
23
  require_relative 'assinafy/resources/assignment_resource'
data/sig/assinafy.rbs CHANGED
@@ -283,6 +283,23 @@ module Assinafy
283
283
  def signing_progress: (String document_id) -> signing_progress
284
284
  end
285
285
 
286
+ class ContractResource < BaseResource
287
+ STATUSES: Array[String]
288
+ RELATION_TYPES: Array[String]
289
+ SORTS: Array[String]
290
+ LIST_FIELDS: Array[String]
291
+ UPDATE_FIELDS: Array[String]
292
+ RENEW_FIELDS: Array[String]
293
+ TERMINATE_FIELDS: Array[String]
294
+
295
+ def list: (?params params, ?String? account_id_override) -> list_result
296
+ def get: (String document_id, ?String? account_id_override) -> api_object?
297
+ def update: (String document_id, params payload, ?String? account_id_override) -> api_object
298
+ def related: (String document_id, ?String? account_id_override) -> api_object?
299
+ def renew: (String document_id, params payload, ?String? account_id_override) -> api_object
300
+ def terminate: (String document_id, ?params payload, ?String? account_id_override) -> api_object
301
+ end
302
+
286
303
  class SignerResource < BaseResource
287
304
  EMAIL_REGEX: Regexp
288
305
  SIGNATURE_TYPES: Array[String]
@@ -445,6 +462,7 @@ module Assinafy
445
462
  attr_reader accounts: Resources::AccountResource
446
463
  attr_reader users: Resources::UserResource
447
464
  attr_reader documents: Resources::DocumentResource
465
+ attr_reader contracts: Resources::ContractResource
448
466
  attr_reader signers: Resources::SignerResource
449
467
  attr_reader signer_documents: Resources::SignerDocumentResource
450
468
  attr_reader assignments: Resources::AssignmentResource
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: assinafy
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Assinafy SDK Contributors
@@ -134,8 +134,8 @@ dependencies:
134
134
  - !ruby/object:Gem::Version
135
135
  version: '3.0'
136
136
  description: Ruby SDK for Assinafy. Covers the documented authentication, OAuth 2.1,
137
- account, user, document, signer, assignment, webhook, template, tag, and field APIs,
138
- plus the high-level upload_and_request_signatures helper.
137
+ account, user, document, contract, signer, assignment, webhook, template, tag, and
138
+ field APIs, plus the high-level upload_and_request_signatures helper.
139
139
  email:
140
140
  - sdk@assinafy.com.br
141
141
  executables: []
@@ -157,6 +157,7 @@ files:
157
157
  - lib/assinafy/resources/assignment_resource.rb
158
158
  - lib/assinafy/resources/auth_resource.rb
159
159
  - lib/assinafy/resources/base_resource.rb
160
+ - lib/assinafy/resources/contract_resource.rb
160
161
  - lib/assinafy/resources/document_resource.rb
161
162
  - lib/assinafy/resources/field_resource.rb
162
163
  - lib/assinafy/resources/oauth_resource.rb