assinafy 1.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97dcce395cbee3bd4a632650550becfd07d6fa291da434678d1a31de4488d103
4
- data.tar.gz: 4cde25cfc6221e23378892c4fa19d7779046b65642bc33dc734756f712c2aab8
3
+ metadata.gz: d2a0b97db3a9ad302307e31bccf8f06751fe310dd253417bb129ad921b0a4b3f
4
+ data.tar.gz: 94eb893e22a78777a0b4102d572e274e0f22c5b50cd3768d348f2deac69541ab
5
5
  SHA512:
6
- metadata.gz: 437fad120c4ec641767a2241f0e03589e14ea31b3407b603c85e7284476526711cd8d8f8732573d2ab4348aa9387cbfa077e43061ff91e1728eeda766b6c51f2
7
- data.tar.gz: 9c2e1b81591e038d449145fa3f97f7b2b90f98fbfede435f01cd14fbfbb6b9d62805df762e39a01c0e9de7a8d9e729371d3b88e63b5049fb7ac29b0c10d01b88
6
+ metadata.gz: 43838b1c27e22fc5210a8e877a4e1897f50030a6e32801cf490f101de6d18805ed79bff013120801ce90ab53369531142bf678cbe0e04f543139aa72790bab11
7
+ data.tar.gz: f0e442b567d84643b2237ec21edf5b59de9155268451986dcdcc19f841ccf1514e1fd11e112fedc0ff1b8c04439feb411fec7ec37bc06128a86409996745798e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to the `assinafy` Ruby gem are documented here.
4
4
 
5
+ ## [2.0.0] - 2026-10-16
6
+
7
+ ### Added
8
+
9
+ - `IdDocumentFaceMatchBasic` biometric verification in assignments, cost estimates and template generation,
10
+ paired with either Email or Whatsapp notifications.
11
+ - `SignerResource#identity_verification` and `#start_identity_verification` for hosted identity verification
12
+ with explicit biometric-processing consent and signer-only authentication.
13
+ - `AssignmentResource#identity_review`, `#identity_review_image` and `#decide_identity_review` for account
14
+ reviewers to inspect evidence and approve or reject a biometric identity review.
15
+ - `WebhookResource#list_endpoints(url:)` filters by exact URL, and `#delete_endpoint_by_url` removes the endpoint
16
+ registered with that URL.
17
+
18
+ ## [1.11.1] - 2026-10-12
19
+
20
+ ### Added
21
+
22
+ - `Assinafy::ApiError#rate_limit` returns `{ limit:, remaining:, reset: }` from the `X-Rate-Limit-*` headers, and
23
+ `#retry_after` returns the `Retry-After` seconds of a `429 Too Many Requests`.
24
+ - `SignerDocumentResource#list_as_workspace` and `#search_as_workspace` list and search a signer's documents with
25
+ the workspace API key or token.
26
+
27
+ ### Changed
28
+
29
+ - `DocumentResource#send_token` always sends `{ "channel": "email", "recipient": "<signer email>" }`, as the API
30
+ requires, and returns `{ document, channel, recipient }`. `recipient:` (or the earlier `email:`) must be an email
31
+ address; calls without a recipient, with a `whatsapp` channel, or with a phone number now raise
32
+ `ValidationError`.
33
+ - `SignerDocumentResource#list` and `#search` require the signer access code and never send workspace
34
+ credentials. Calls that relied on omitting the code to use the API key now raise `ValidationError`; switch them
35
+ to `#list_as_workspace` / `#search_as_workspace`.
36
+ - `FieldResource#validate` and `#validate_multiple` raise `ValidationError` for an explicit `signer_access_code:
37
+ nil` instead of falling back to workspace credentials. Omitting the keyword still uses workspace credentials.
38
+ - `AssignmentResource#estimate_cost` accepts payloads without `signers` or `entries`, which the API treats as
39
+ optional.
40
+ - `DocumentResource#search` and `SignerDocumentResource#search` accept a `nil` search term and omit `search`.
41
+ - `FieldResource#create` sends an explicit `regex: nil` as JSON `null`.
42
+ - `DocumentResource#rename` rejects names longer than 255 characters before sending.
43
+
5
44
  ## 1.11.0
6
45
 
7
46
  ### Added
data/README.en.md CHANGED
@@ -388,6 +388,7 @@ the missing side is inferred from the other, and when both are omitted both defa
388
388
  | `Email` *(default)* | One-time code (OTP) by email before signing | `Email` | 0 credits |
389
389
  | `Whatsapp` | One-time code (OTP) over WhatsApp; needs `whatsapp_phone_number` and a paid plan | `Whatsapp` | 0.45 credits (the WhatsApp notification) |
390
390
  | `DigitalCertificate` | Signs with their own ICP-Brasil certificate (A1/A3), producing a qualified PAdES signature | `Email` **or** `Whatsapp` | 0.5 credits + its notification (0 or 0.45) |
391
+ | `IdDocumentFaceMatchBasic` | Government ID photos, a photo holding the ID and liveness; requires its account feature and a valid CPF in `government_id` | `Email` **or** `Whatsapp` | Per-signer fee + notification; request an estimate |
391
392
 
392
393
  - Send exactly one channel in `notification_methods` (a one-element array).
393
394
  - The certificate charge appears in the cost breakdown under the code `SignatureDigitalCertificate`. Resending a
@@ -517,6 +518,48 @@ signing.assignments.decline(
517
518
  )
518
519
  ```
519
520
 
521
+ **Biometric signers** use `verification_method: 'IdDocumentFaceMatchBasic'` with exactly one Email or Whatsapp
522
+ notification in virtual/collect assignments or template generation. Store a valid CPF in the signer's
523
+ `government_id` first and call `estimate_cost` or `estimate_cost_from_template` for the current price.
524
+ The OTP example above applies to Email/Whatsapp. For biometrics, read the verification state and collect explicit
525
+ biometric-processing consent before starting:
526
+
527
+ ```ruby
528
+ state = signing.signers.identity_verification(signer_access_code: access_code)
529
+ # => { 'can_start' => true, 'status' => nil, 'url' => nil, 'expires_at' => nil, 'completed_at' => nil }
530
+
531
+ # After the signer explicitly consents to biometric processing:
532
+ session = signing.signers.start_identity_verification(consent: true, signer_access_code: access_code)
533
+ # => { 'id' => 'verification-id', 'url' => 'https://verify.assinafy.com.br/c/session#t=token-placeholder',
534
+ # 'expires_at' => '2026-10-16T13:00:00Z' }
535
+ ```
536
+
537
+ Open the capture URL unchanged, including its `#t=` fragment. It is a bearer link: keep it out of logs and browser
538
+ storage. For an existing Pending session, use the URL from `identity_verification`. `can_start` is true only
539
+ before any session or after Expired; restarting an Expired session costs nothing extra. Declined or Abandoned
540
+ requires a new document and assignment.
541
+
542
+ InReview lets the signer view, confirm data and sign while account review holds certification. After all signers
543
+ sign, the document can be `pending_identity_review`. The account reviewer uses workspace credentials:
544
+
545
+ ```ruby
546
+ review = client.assignments.identity_review(document.fetch('id'), assignment.fetch('id'), signer.fetch('id'))
547
+ # => SignerIdentityReview: status, reason_codes, evidence_available, expected, document, matches,
548
+ # checks, images, ruleset_version, evidence_sha256 and decision metadata
549
+ image = client.assignments.identity_review_image(document.fetch('id'), assignment.fetch('id'), signer.fetch('id'), 'face')
550
+ # => JPEG, PNG or PDF bytes, depending on the name listed in review['images']; do not cache
551
+
552
+ # After the reviewer checks the evidence:
553
+ client.assignments.decide_identity_review(
554
+ document.fetch('id'), assignment.fetch('id'), signer.fetch('id'), decision: 'Approved', note: 'Identity checked'
555
+ )
556
+ ```
557
+
558
+ `decision: 'Rejected'` cancels the document and notifies signers and copy receivers. The optional note accepts at
559
+ most 2000 characters, goes to activities and the provider, and is never shown to the signer. Images exist only
560
+ during InReview. Observe signer_identity_review_required, signer_identity_review_approved,
561
+ signer_identity_review_rejected, signer_identity_verified and signer_identity_verification_failed events.
562
+
520
563
  **A1/A3 certificate signers** complete their signature in Assinafy's hosted signing flow, opened from
521
564
  `assignment['signing_urls']`: they accept the terms, confirm their data, and sign with their A1 or A3 certificate
522
565
  through the Web PKI browser extension. The ordinary signing endpoint rejects certificate signers, and the SDK does
@@ -538,7 +581,7 @@ endpoint = client.webhooks.create_endpoint(
538
581
  name: 'ERP',
539
582
  signing_enabled: true
540
583
  )
541
- secret = client.webhooks.endpoint_secret(endpoint.fetch('id'))['secret'] # => "whsec_..."
584
+ secret = endpoint.fetch('secret') # returned once on creation; store it immediately, including for OAuth apps
542
585
  ```
543
586
 
544
587
  - `create_endpoint` requires `url`, `email`, and `events`; `name`, `is_active` (default `true`), and
@@ -548,7 +591,9 @@ secret = client.webhooks.endpoint_secret(endpoint.fetch('id'))['secret'] # => "
548
591
  endpoint has none and keeps the current one otherwise; `false` discards it.
549
592
  - `rotate_endpoint_secret(id)` returns a new secret; the old one stops working immediately.
550
593
  - Reading and rotating secrets is not available to OAuth applications — use an API key or a user session token.
551
- Both answer `400` when signing is disabled.
594
+ Both answer `400` when signing is disabled. An OAuth app that loses its secret must delete and recreate the endpoint.
595
+ - `list_endpoints(url: registered_url)` filters by exact URL; `delete_endpoint_by_url(registered_url)` removes
596
+ that endpoint and returns `nil` (`404` when no endpoint matches).
552
597
 
553
598
  **Delivery contract.** Each delivery is a `POST` with an `application/json` body and the headers `webhook-id`
554
599
  (identical on every attempt of the same event to the same endpoint — deduplicate on it), `webhook-timestamp`
@@ -633,6 +678,7 @@ rescue Assinafy::ValidationError => e
633
678
  warn e.errors.inspect
634
679
  rescue Assinafy::ApiError => e
635
680
  warn "Assinafy returned #{e.status_code}: #{e.message}"
681
+ sleep(e.retry_after) if e.status_code == 429 && e.retry_after # also e.rate_limit => { limit:, remaining:, reset: }
636
682
  rescue Assinafy::NetworkError => e
637
683
  warn "Network failure: #{e.message}"
638
684
  end
@@ -712,7 +758,7 @@ client.documents.download_page('document-id', 'page-id')
712
758
  client.documents.delete('document-id')
713
759
  client.documents.verify('signature-hash')
714
760
  client.documents.public_info('document-id')
715
- client.documents.send_token('document-id', email: 'alice@example.com')
761
+ client.documents.send_token('document-id', recipient: 'signer@example.com')
716
762
  client.documents.list_tags('document-id')
717
763
  client.documents.replace_tags('document-id', ['tag-id-1', 'tag-id-2'])
718
764
  client.documents.append_tags('document-id', ['tag-id-3'])
@@ -806,6 +852,8 @@ query parameter.
806
852
  client.signer_documents.current('signer-id', signer_access_code: 'code')
807
853
  client.signer_documents.list('signer-id', { status: 'pending_signature' }, signer_access_code: 'code')
808
854
  client.signer_documents.search('signer-id', 'contract', signer_access_code: 'code')
855
+ client.signer_documents.list_as_workspace('signer-id') # same route with your API key
856
+ client.signer_documents.search_as_workspace('signer-id', 'contract')
809
857
  client.signer_documents.sign_multiple(%w[document-id-1 document-id-2], signer_access_code: 'code')
810
858
  client.signer_documents.decline_multiple(%w[document-id-1 document-id-2], decline_reason: 'No', signer_access_code: 'code')
811
859
  client.signer_documents.download('signer-id', 'document-id', 'pades') # public: no access code needed
data/README.md CHANGED
@@ -519,6 +519,7 @@ dois, ambos assumem `Email`.
519
519
  | `Email` *(padrão)* | código de uso único (OTP) por e-mail, exigido antes de assinar | `Email` | 0 crédito |
520
520
  | `Whatsapp` | código de uso único (OTP) por WhatsApp; exige `whatsapp_phone_number` e plano pago | `Whatsapp` | 0,45 crédito (a notificação por WhatsApp) |
521
521
  | `DigitalCertificate` | o signatário assina com o **próprio certificado ICP-Brasil (A1/A3)**, gerando uma assinatura **PAdES qualificada** | `Email` **ou** `Whatsapp` | 0,5 crédito + a notificação (0 ou 0,45) |
522
+ | `IdDocumentFaceMatchBasic` | documento de identidade, foto segurando o documento e prova de vida; exige o recurso na conta e CPF válido em `government_id` | `Email` **ou** `Whatsapp` | tarifa por signatário + notificação; consulte a estimativa |
522
523
 
523
524
  - Informe **exatamente um** canal em `notification_methods` (um array de um elemento).
524
525
  - O custo do certificado digital aparece na estimativa com o código `SignatureDigitalCertificate`.
@@ -717,6 +718,55 @@ signatario_client.assignments.decline(
717
718
  signatario_client.signer_documents.decline_multiple([doc['id']], decline_reason: 'Não', signer_access_code: codigo)
718
719
  ```
719
720
 
721
+ ### Signatários com verificação biométrica
722
+
723
+ Na criação do assignment (virtual ou collect), ou em `create_from_template`, envie
724
+ `verification_method: 'IdDocumentFaceMatchBasic'` e exatamente um canal de notificação.
725
+ Cadastre o CPF válido do signatário em `government_id` antes de criar o assignment. Use
726
+ `estimate_cost` ou `estimate_cost_from_template` para obter o custo antes de enviar.
727
+
728
+ O fluxo de OTP acima se aplica a `Email` e `Whatsapp`. Para biometria, consulte o estado com o
729
+ código de acesso do signatário e abra uma sessão somente após obter o consentimento explícito:
730
+
731
+ ```ruby
732
+ estado = signatario_client.signers.identity_verification(signer_access_code: codigo)
733
+ # => { 'can_start' => true, 'status' => nil, 'url' => nil, 'expires_at' => nil, 'completed_at' => nil }
734
+
735
+ # Após o consentimento explícito do signatário para o processamento biométrico:
736
+ sessao = signatario_client.signers.start_identity_verification(consent: true, signer_access_code: codigo)
737
+ # => { 'id' => 'verification-id', 'url' => 'https://verify.assinafy.com.br/c/session#t=token-placeholder',
738
+ # 'expires_at' => '2026-10-16T13:00:00Z' }
739
+ ```
740
+
741
+ Abra `sessao['url']` sem alterar a URL nem o fragmento `#t=`; ela concede acesso à captura e não
742
+ deve aparecer em logs ou armazenamento do navegador. Se já houver uma sessão `Pending`, use a
743
+ URL retornada por `identity_verification`. `can_start` só é verdadeiro antes da primeira sessão
744
+ ou após `Expired`; reiniciar uma sessão expirada não cobra novamente. `Declined` e `Abandoned`
745
+ exigem um novo documento e assignment.
746
+
747
+ Em `InReview`, o signatário continua visualizando, confirmando dados e assinando, mas a certificação
748
+ aguarda a revisão da conta. Depois de todas as assinaturas, o documento pode ficar em
749
+ `pending_identity_review`. Com as credenciais do workspace, o revisor consulta e decide:
750
+
751
+ ```ruby
752
+ revisao = client.assignments.identity_review(doc['id'], assignment['id'], signatario['id'])
753
+ # => SignerIdentityReview: status, reason_codes, evidence_available, expected, document,
754
+ # matches, checks, images, ruleset_version, evidence_sha256 e dados da decisão
755
+ imagem = client.assignments.identity_review_image(doc['id'], assignment['id'], signatario['id'], 'face')
756
+ # => bytes JPEG, PNG ou PDF, conforme o nome retornado em revisao['images']; não armazenar em cache
757
+
758
+ # Após o revisor conferir as evidências:
759
+ client.assignments.decide_identity_review(
760
+ doc['id'], assignment['id'], signatario['id'], decision: 'Approved', note: 'Identidade conferida'
761
+ )
762
+ ```
763
+
764
+ `decision: 'Rejected'` cancela o documento e notifica signatários e destinatários de cópia.
765
+ A nota opcional aceita até 2000 caracteres, fica na atividade e vai ao provedor; não é mostrada
766
+ ao signatário. As imagens só ficam disponíveis durante `InReview`. Acompanhe também os eventos
767
+ `signer_identity_review_required`, `signer_identity_review_approved`, `signer_identity_review_rejected`,
768
+ `signer_identity_verified` e `signer_identity_verification_failed`.
769
+
720
770
  ### Signatários com certificado A1/A3
721
771
 
722
772
  O signatário por certificado digital conclui a assinatura no fluxo hospedado da Assinafy, aberto
@@ -749,9 +799,11 @@ endpoint = client.webhooks.create_endpoint(
749
799
  endpoint['id'] # => "webhook-endpoint-id"
750
800
 
751
801
  client.webhooks.list_endpoints # do mais antigo ao mais novo
802
+ client.webhooks.list_endpoints(url: 'https://app.example.com/webhooks/assinafy') # URL exata
752
803
  client.webhooks.get_endpoint('webhook-endpoint-id')
753
804
  client.webhooks.update_endpoint('webhook-endpoint-id', is_active: false) # só os campos enviados
754
805
  client.webhooks.delete_endpoint('webhook-endpoint-id') # => nil, libera a vaga
806
+ client.webhooks.delete_endpoint_by_url('https://app.example.com/webhooks/assinafy') # => nil
755
807
  ```
756
808
 
757
809
  - `create_endpoint` exige `url`, `email` e `events`; `name`, `is_active` (padrão `true`) e
@@ -763,6 +815,10 @@ client.webhooks.delete_endpoint('webhook-endpoint-id') # =>
763
815
 
764
816
  ### Segredo de assinatura
765
817
 
818
+ Com `signing_enabled: true`, `create_endpoint` devolve o segredo uma única vez em
819
+ `endpoint['secret']`. Guarde esse valor na criação, inclusive em aplicativos OAuth. Se o segredo
820
+ de um aplicativo OAuth for perdido, exclua e recrie o endpoint.
821
+
766
822
  ```ruby
767
823
  segredo = client.webhooks.endpoint_secret('webhook-endpoint-id')
768
824
  segredo['secret'] # => "whsec_..."
@@ -964,7 +1020,9 @@ Toda exceção do SDK deriva de `Assinafy::Error` e carrega um `#context` com de
964
1020
  depuração. `ApiError` trata as duas formas de corpo de erro da API — o erro de framework
965
1021
  (`{"name":..., "code":..., "status":...}`) e o envelope de aplicação
966
1022
  (`{"status":..., "data":null, "message":...}`) — inclusive quando um `200` traz um `status`
967
- interno de falha. Num `403`, `e.context[:www_authenticate]` traz o desafio OAuth, quando houver.
1023
+ interno de falha. Num `403`, `e.context[:www_authenticate]` traz o desafio OAuth, quando houver. Num `429`,
1024
+ `e.retry_after` traz os segundos de `Retry-After` e `e.rate_limit` traz `{ limit:, remaining:, reset: }` dos
1025
+ cabeçalhos `X-Rate-Limit-*`.
968
1026
 
969
1027
  ---
970
1028
 
@@ -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-07 from
4
- > `https://api.assinafy.com.br/v1/docs/openapi.json` (81 paths, 106 operations, 44 schemas).
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).
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
@@ -82,8 +82,9 @@ Required parameters and object properties are marked with `*`.
82
82
  | POST | `/v1/accounts/{accountId}/templates/{templateId}/documents/estimate-cost` | `DocumentResource#estimate_cost_from_template` | Bearer token or `X-Api-Key` | path `accountId*`: string<br>path `templateId*`: string | required; `application/json` object { `signers*`: Array<object { `role_id*`: string; `verification_method`: string; `notification_methods`: Array<string> }> } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`CostEstimate`](#costestimate) } |
83
83
  | GET | `/v1/accounts/{accountId}/theme` | `AccountResource#theme` | Bearer token or `X-Api-Key` | path `accountId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`AccountTheme`](#accounttheme) } |
84
84
  | GET | `/v1/accounts/{accountId}/webhooks` | `WebhookResource#list_dispatches` | Bearer token or `X-Api-Key` | path `accountId*`: string<br>query `endpoint_id`: string<br>query `event`: string<br>query `delivered`: string<br>query `from`: integer<br>query `to`: integer<br>query `page`: integer<br>query `per-page`: integer | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`WebhookDispatch`](#webhookdispatch)> } |
85
- | GET | `/v1/accounts/{accountId}/webhooks/endpoints` | `WebhookResource#list_endpoints` | Bearer token or `X-Api-Key`; OAuth `account:read` | path `accountId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`WebhookEndpoint`](#webhookendpoint)> } |
86
- | POST | `/v1/accounts/{accountId}/webhooks/endpoints` | `WebhookResource#create_endpoint` | Bearer token or `X-Api-Key`; OAuth `webhooks:write` | path `accountId*`: string | required; `application/json` object { `url*`: string (uri); `email*`: string (email); `events*`: Array<string>; `name`: string; `is_active`: boolean; `signing_enabled`: boolean }; unknown keys raise `ValidationError` locally | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`WebhookEndpoint`](#webhookendpoint) }; `400` duplicate URL, `403` past the plan limit (1 endpoint, 3 on paid plans) |
85
+ | GET | `/v1/accounts/{accountId}/webhooks/endpoints` | `WebhookResource#list_endpoints` | Bearer token or `X-Api-Key`; OAuth `account:read` | path `accountId*`: string; query `url`: exact registered URL | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`WebhookEndpoint`](#webhookendpoint)> } |
86
+ | POST | `/v1/accounts/{accountId}/webhooks/endpoints` | `WebhookResource#create_endpoint` | Bearer token or `X-Api-Key`; OAuth `webhooks:write` | path `accountId*`: string | required; `application/json` object { `url*`: string (uri); `email*`: string (email); `events*`: Array<string>; `name`: string; `is_active`: boolean; `signing_enabled`: boolean }; unknown keys raise `ValidationError` locally | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`WebhookEndpoint`](#webhookendpoint) with `secret`: string when signing_enabled is true (returned once) }; `400` duplicate URL, `403` past the plan limit (1 endpoint, 3 on paid plans) |
87
+ | DELETE | `/v1/accounts/{accountId}/webhooks/endpoints` | `WebhookResource#delete_endpoint_by_url` | Bearer token or `X-Api-Key`; OAuth `webhooks:write` | path `accountId*`: string; query `url*`: exact registered URL | None | `200` envelope data `[]`; SDK returns `nil`; `404` when no endpoint matches |
87
88
  | GET | `/v1/accounts/{accountId}/webhooks/endpoints/{endpointId}` | `WebhookResource#get_endpoint` | Bearer token or `X-Api-Key`; OAuth `account:read` | path `accountId*`: string<br>path `endpointId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`WebhookEndpoint`](#webhookendpoint) } |
88
89
  | PUT | `/v1/accounts/{accountId}/webhooks/endpoints/{endpointId}` | `WebhookResource#update_endpoint` | Bearer token or `X-Api-Key`; OAuth `webhooks:write` | path `accountId*`: string<br>path `endpointId*`: string | required; `application/json` object { `url`: string (uri); `email`: string (email); `events`: Array<string>; `name`: string; `is_active`: boolean; `signing_enabled`: boolean } — partial; at least one field | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`WebhookEndpoint`](#webhookendpoint) } |
89
90
  | DELETE | `/v1/accounts/{accountId}/webhooks/endpoints/{endpointId}` | `WebhookResource#delete_endpoint` | Bearer token or `X-Api-Key`; OAuth `webhooks:write` | path `accountId*`: string<br>path `endpointId*`: string | None | `200` `application/json` [`Envelope`](#envelope); SDK returns `nil` |
@@ -109,8 +110,11 @@ Required parameters and object properties are marked with `*`.
109
110
  | POST | `/v1/documents/{documentId}/assignments/estimate-cost` | `AssignmentResource#estimate_cost` | Bearer token or `X-Api-Key` | path `documentId*`: string | required; `application/json` object { `method`: string; `signers`: Array<object { `verification_method`: string; `notification_methods`: Array<string> }>; `entries`: Array<object> } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`CostEstimate`](#costestimate) } |
110
111
  | 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 } |
111
112
  | 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> } |
112
- | 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) } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`Assignment`](#assignment) } |
113
+ | 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) } |
113
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) } |
115
+ | 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
+ | 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
+ | 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 |
114
118
  | PUT | `/v1/documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/resend` | `AssignmentResource#resend_notification` | 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`: object { `is_sent`: boolean; `document_id`: string; `signer_id`: string } } |
115
119
  | GET | `/v1/documents/{documentId}/assignments/{assignmentId}/whatsapp-notifications` | `AssignmentResource#whatsapp_notifications` | Bearer token or `X-Api-Key` | path `documentId*`: string<br>path `assignmentId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`WhatsappNotification`](#whatsappnotification)> } |
116
120
  | GET | `/v1/documents/{documentId}/download/{artifactName}` | `DocumentResource#download` | Bearer token or `X-Api-Key` | path `documentId*`: string<br>path `artifactName*`: string | None | `200` `application/pdf` string (binary) |
@@ -124,7 +128,7 @@ Required parameters and object properties are marked with `*`.
124
128
  | POST | `/v1/oauth/revoke` | `OAuthResource#revoke` | Public (client authenticates with `client_id` in the body) | None | required; `application/x-www-form-urlencoded` (RFC 7009; also accepts `application/json`) object { `token*`: string; `client_id*`: string; `token_type_hint`: string enum `access_token`, `refresh_token`; `client_secret`: string } | `200` empty body — every token outcome reports success |
125
129
  | GET | `/v1/oauth/userinfo` | `OAuthResource#userinfo` | Bearer token or `X-Api-Key`; requires the `openid` scope | None | None | `200` `application/json` **flat** object { `sub`: string; `name`: string (nullable); `email`: string (email, nullable); `email_verified`: boolean (nullable) } — not enveloped |
126
130
  | GET | `/v1/public/documents/{documentId}` | `DocumentResource#public_info` | Public | path `documentId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`Document`](#document) } |
127
- | PUT | `/v1/public/documents/{documentId}/send-token` | `DocumentResource#send_token` | Public | path `documentId*`: string | optional; `application/json` object { `email`: string (email) } | `200` `application/json` [`Envelope`](#envelope) |
131
+ | PUT | `/v1/public/documents/{documentId}/send-token` | `DocumentResource#send_token` | Public | path `documentId*`: string | required; `application/json` object { `channel*`: `"email"`; `recipient*`: string (signer email) } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: object { `document`: object { `resource`, `id`, `name`, `page_count`, `created_by` }; `channel`: string; `recipient`: string } } |
128
132
  | GET | `/v1/sign` | `AssignmentResource#signer_document` | `signer-access-code` query parameter | query `has_accepted_terms`: boolean | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`Document`](#document) } |
129
133
  | POST | `/v1/signature` | `SignerResource#upload_signature` | `signer-access-code` query parameter | query `type`: string<br>query `reuse`: boolean | required; `image/png` string (binary) | `200` `application/json` [`Envelope`](#envelope) |
130
134
  | GET | `/v1/signature/{signatureType}` | `SignerResource#download_signature` | `signer-access-code` query parameter | path `signatureType*`: string | None | `200` `image/*` string (binary) |
@@ -132,9 +136,11 @@ Required parameters and object properties are marked with `*`.
132
136
  | PUT | `/v1/signers/documents/decline-multiple` | `SignerDocumentResource#decline_multiple` | `signer-access-code` query parameter | None | required; `application/json` object { `document_ids*`: Array<string>; `decline_reason*`: string } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<any> } |
133
137
  | PUT | `/v1/signers/documents/sign-multiple` | `SignerDocumentResource#sign_multiple` | `signer-access-code` query parameter | None | required; `application/json` object { `document_ids*`: Array<string> } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<any> } |
134
138
  | GET | `/v1/signers/self` | `SignerResource#self_data` | `signer-access-code` query parameter | None | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`SignerSelf`](#signerself) } |
139
+ | GET | `/v1/signers/identity-verification` | `SignerResource#identity_verification` | `signer-access-code` query parameter | None | None | `200` envelope data { `can_start`: boolean; `status`: string or null; `url`: string or null (bearer capture URL, preserve fragment); `expires_at`: date-time or null; `completed_at`: date-time or null } |
140
+ | POST | `/v1/signers/identity-verification` | `SignerResource#start_identity_verification` | `signer-access-code` query parameter | None | required; `application/json` { `consent*`: true } | `200` envelope data { `id`: string; `url`: string (bearer capture URL, preserve fragment); `expires_at`: date-time or null }; `400` unless no session exists or the last session Expired |
135
141
  | GET | `/v1/signers/{signerId}/document` | `SignerDocumentResource#current` | `signer-access-code` query parameter | path `signerId*`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`Document`](#document) } |
136
- | GET | `/v1/signers/{signerId}/documents` | `SignerDocumentResource#list` | `signer-access-code` query parameter | path `signerId*`: string<br>query `page`: integer<br>query `per-page`: integer | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`Document`](#document)> } |
137
- | GET | `/v1/signers/{signerId}/documents/search` | `SignerDocumentResource#search` | `signer-access-code` query parameter | path `signerId*`: string<br>query `search`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`Document`](#document)> } |
142
+ | GET | `/v1/signers/{signerId}/documents` | `SignerDocumentResource#list` (signer code); `#list_as_workspace` (Bearer token or `X-Api-Key`) | `signer-access-code` query parameter, or workspace credentials | path `signerId*`: string<br>query `page`: integer<br>query `per-page`: integer | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`Document`](#document)> } |
143
+ | GET | `/v1/signers/{signerId}/documents/search` | `SignerDocumentResource#search` (signer code); `#search_as_workspace` (Bearer token or `X-Api-Key`) | `signer-access-code` query parameter, or workspace credentials | path `signerId*`: string<br>query `search`: string | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: Array<[`Document`](#document)> } |
138
144
  | GET | `/v1/signers/{signerId}/documents/{documentId}/download/{artifactName}` | `SignerDocumentResource#download` | Public | path `signerId*`: string<br>path `documentId*`: string<br>path `artifactName*`: string | None | `200` `application/pdf` string (binary) |
139
145
  | GET | `/v1/users/api-keys` | `AuthResource#get_api_key` | Bearer token or `X-Api-Key` | None | None | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`ApiKey`](#apikey) } |
140
146
  | POST | `/v1/users/api-keys` | `AuthResource#create_api_key` | Bearer token or `X-Api-Key` | None | required; `application/json` object { `password*`: string (password) } | `200` `application/json` [`Envelope`](#envelope) plus object { `data`: [`ApiKey`](#apikey) } |
@@ -191,6 +197,10 @@ raise `Assinafy::ApiError`:
191
197
  - `error_name` and `error_code` expose framework `name` and `code` values when present.
192
198
  - `context` contains `status_code` and `response_data` for structured logging or support diagnostics, plus
193
199
  `www_authenticate` when the response carries that header.
200
+ - `rate_limit` returns `{ limit:, remaining:, reset: }` from the `X-Rate-Limit-Limit`, `X-Rate-Limit-Remaining`
201
+ and `X-Rate-Limit-Reset` headers (reset in seconds), and `retry_after` returns the `Retry-After` seconds of a
202
+ `429 Too Many Requests`; both are `nil` when the headers are absent. Wait at least `retry_after` seconds before
203
+ retrying. Requests are limited per minute, per user when authenticated and per IP address otherwise.
194
204
 
195
205
  Transport, timeout, and TLS failures raise `Assinafy::NetworkError`; caller-side validation failures raise
196
206
  `Assinafy::ValidationError` before a request is sent.
@@ -290,7 +300,7 @@ look successful while the connection stays active.
290
300
 
291
301
  Verification and notification methods are coupled per signer: the omitted side is inferred from the other, and
292
302
  when both are omitted both are `Email`. Exactly one notification method is allowed. Allowed pairs: `Email` with
293
- `Email`, `Whatsapp` with `Whatsapp`, and `DigitalCertificate` with either. `Email` costs 0 credits; `Whatsapp`
303
+ `Email`, `Whatsapp` with `Whatsapp`, and `DigitalCertificate` or `IdDocumentFaceMatchBasic` with either. `Email` costs 0 credits; `Whatsapp`
294
304
  costs 0.45 credits (its WhatsApp notification, paid plans only); `DigitalCertificate` costs 0.5 credits per signer
295
305
  on top of its notification, shown in cost estimates under the breakdown code `SignatureDigitalCertificate`.
296
306
 
@@ -307,10 +317,38 @@ Certificate signers complete the signature in Assinafy's hosted signing flow, re
307
317
  `/signers/certificate/complete`), whose authentication and request/response schemas are not part of the OpenAPI
308
318
  contract. Once the flow completes, the `pades` artifact returns the qualified signature.
309
319
 
320
+ ### Biometric identity verification
321
+
322
+ `IdDocumentFaceMatchBasic` requires its account feature and a valid CPF in the signer's `government_id`.
323
+ It uses government ID photos, a photo holding the ID and liveness, and adds a per-signer fee to the chosen
324
+ Email or Whatsapp notification. Call `estimate_cost` or `estimate_cost_from_template` for the current total.
325
+ Both assignment methods and template generation accept this verification method.
326
+
327
+ With the signer access code, read `signers.identity_verification`; after the signer explicitly consents to
328
+ biometric processing, call `signers.start_identity_verification(consent: true, signer_access_code: code)`.
329
+ Open the returned capture URL unchanged, including its fragment; it is a bearer link, so keep it out of logs
330
+ and browser storage. The status response backfills pending provider decisions. `can_start` is true only before
331
+ any session or after Expired; Pending, InReview, Approved, Declined and Abandoned cannot restart. A restart
332
+ replaces an Expired session without another charge. Declined or Abandoned requires a new document and assignment.
333
+
334
+ During InReview the signer may view, confirm data and sign. Certification waits for account approval; once all
335
+ signers sign, the document can be `pending_identity_review`. Read `assignments.identity_review`, download only
336
+ names from its `images` with `identity_review_image`, and submit `decide_identity_review(decision: 'Approved')`
337
+ or `decision: 'Rejected'` while the document is pending_signature or pending_identity_review. Rejection cancels
338
+ the document and notifies signers and copy receivers. Optional `note:` (at most 2000 characters) is recorded in
339
+ activities and sent to the provider; it is never shown to the signer. Images are fetched from the provider,
340
+ available only during InReview, and must not be cached. The review retains summaries, with `evidence_available`
341
+ false when none is retained or fetched. Direct approvals expose evidence through the signer_identity_verified
342
+ activity instead. Observe signer_identity_review_required, signer_identity_review_approved,
343
+ signer_identity_review_rejected, signer_identity_verified and signer_identity_verification_failed events.
344
+
310
345
  ### Webhook endpoints and deliveries
311
346
 
312
347
  An account can have 1 webhook endpoint, or up to 3 on paid plans; each has a distinct URL, its own events, and its
313
- own signing setting, and every active endpoint subscribed to an event receives it. `WebhookResource#create_endpoint`
348
+ own signing setting, and every active endpoint subscribed to an event receives it. Creation with
349
+ `signing_enabled: true` returns a `secret` once: store it immediately. OAuth apps cannot read or rotate it later;
350
+ if lost, delete and recreate the endpoint. `#list_endpoints(url: registered_url)` finds an exact URL match and
351
+ `#delete_endpoint_by_url(registered_url)` deletes that endpoint, returning `nil`. `WebhookResource#create_endpoint`
314
352
  and `#update_endpoint` accept only `url`, `email`, `events`, `name`, `is_active`, and `signing_enabled`.
315
353
  `#register`, `#get`, and `#inactivate` (`/webhooks/subscriptions`, `/webhooks/inactivate`) act on the account's
316
354
  oldest endpoint; `#register` accepts only `url`, `email`, `events`, and `is_active`.
@@ -351,6 +389,7 @@ return-value, and usage details.
351
389
  | `SignerResource#validate_create!` | Validate and normalize a signer-create body without a network request. | [`signer_resource.rb`](../lib/assinafy/resources/signer_resource.rb) |
352
390
  | `SignerResource#find_by_email` | Page through a successful search and return a case-insensitive match or `nil`. | [`signer_resource.rb`](../lib/assinafy/resources/signer_resource.rb) |
353
391
  | `SignerDocumentResource#document` | Alias for `#current`. | [`signer_document_resource.rb`](../lib/assinafy/resources/signer_document_resource.rb) |
392
+ | `SignerDocumentResource#list_as_workspace`, `#search_as_workspace` | List/search a signer's documents with workspace credentials; `#list`/`#search` require the signer access code and send only it. | [`signer_document_resource.rb`](../lib/assinafy/resources/signer_document_resource.rb) |
354
393
  | `AuthResource#api_key` | Alias for `#get_api_key`. | [`auth_resource.rb`](../lib/assinafy/resources/auth_resource.rb) |
355
394
  | `WebhookResource#update` | Alias for `#register` (acts on the oldest endpoint). | [`webhook_resource.rb`](../lib/assinafy/resources/webhook_resource.rb) |
356
395
  | `AssignmentResource.build_payload` | Validate and normalize virtual or collect assignment bodies locally. | [`assignment_resource.rb`](../lib/assinafy/resources/assignment_resource.rb) |
@@ -359,12 +398,12 @@ return-value, and usage details.
359
398
  | `WebhookVerifier::SECRET_PREFIX`, `::DEFAULT_TOLERANCE` | The `whsec_` secret prefix and the default 300-second timestamp tolerance. | [`webhook_verifier.rb`](../lib/assinafy/support/webhook_verifier.rb) |
360
399
  | `WebhookVerifier#extract_event`, `#event_type`, `#event_payload`, `#event_object`, `#event_subject`, `#event_data` | Parse and access webhook envelope fields; `event_data` is retained as a compatibility helper. | [`webhook_verifier.rb`](../lib/assinafy/support/webhook_verifier.rb) |
361
400
  | `Assinafy::Error.new`, `#context` | Construct/read the SDK base error and its structured context. | [`errors.rb`](../lib/assinafy/errors.rb) |
362
- | `Assinafy::ApiError.new`, `.from_response`, `#status_code`, `#response_data`, `#error_name`, `#error_code` | Construct/read an API response error. | [`errors.rb`](../lib/assinafy/errors.rb) |
401
+ | `Assinafy::ApiError.new`, `.from_response`, `#status_code`, `#response_data`, `#error_name`, `#error_code`, `#rate_limit`, `#retry_after` | Construct/read an API response error, including its rate-limit headers. | [`errors.rb`](../lib/assinafy/errors.rb) |
363
402
  | `Assinafy::ValidationError.new`, `#errors`; `Assinafy::NetworkError` | Represent caller-side validation and transport failures. | [`errors.rb`](../lib/assinafy/errors.rb) |
364
403
  | `Assinafy::OAuthError.new`, `.from_response`, `#error`, `#error_description` | Represent an OAuth endpoint failure; subclasses `ApiError`, so existing rescue clauses keep working. | [`errors.rb`](../lib/assinafy/errors.rb) |
365
404
  | `Assinafy::OAuth.generate_code_verifier`, `.code_challenge`, `.generate_state`, `.normalize_scope`, `.validate_code_verifier!`, `.authorization_url` | Build the PKCE pair, CSRF state, and authorization URL for the browser half of the OAuth flow. No network request. | [`oauth.rb`](../lib/assinafy/oauth.rb) |
366
405
  | `Assinafy::OAuth::AUTHORIZATION_SERVER`, `::AUTHORIZATION_SERVER_METADATA_URL`, `::AUTHORIZATION_ENDPOINT`, `::CODE_CHALLENGE_METHOD`, `::SCOPES` | Published authorization-server endpoints and scopes. | [`oauth.rb`](../lib/assinafy/oauth.rb) |
367
- | `AssignmentResource::VERIFICATION_METHODS`, `::NOTIFICATION_METHODS` | The signer verification (`Email`, `Whatsapp`, `DigitalCertificate`) and notification (`Email`, `Whatsapp`) enums, validated locally by `build_payload`. | [`assignment_resource.rb`](../lib/assinafy/resources/assignment_resource.rb) |
406
+ | `AssignmentResource::VERIFICATION_METHODS`, `::NOTIFICATION_METHODS` | The signer verification (`Email`, `Whatsapp`, `DigitalCertificate`, `IdDocumentFaceMatchBasic`) and notification (`Email`, `Whatsapp`) enums, validated locally by `build_payload`. | [`assignment_resource.rb`](../lib/assinafy/resources/assignment_resource.rb) |
368
407
  | `Assinafy::Utils.handle_assinafy_response`, `.clean_params`, `.query_params`, `.body_params`, `.require_email`, `.require_expiration` | Internal public helpers used by resources for envelope and parameter normalization; applications should prefer resource methods. | [`utils.rb`](../lib/assinafy/utils.rb) |
369
408
  | `Assinafy::NullLogger#debug`, `#info`, `#warn`, `#error`, `#fatal`, `#unknown` | Internal no-op logger methods used when no logger is configured. | [`null_logger.rb`](../lib/assinafy/null_logger.rb) |
370
409
  | `Assinafy::VERSION` | Published SDK version constant. | [`version.rb`](../lib/assinafy/version.rb) |
@@ -461,6 +500,8 @@ A signer within an assignment: the base Signer plus per-assignment verification/
461
500
  | `notified` | boolean | No | Yes | — |
462
501
  | `completed` | boolean | No | Yes | Only present in account-owner contexts. |
463
502
  | `notification_history` | Array<[`NotificationHistoryEntry`](#notificationhistoryentry)> | No | Yes | Per-channel delivery history for this signer (email + WhatsApp), most-recent send order. |
503
+ | `government_id` | string | No | Yes | Signer's normalized CPF (11 digits) or CNPJ (14 characters; may be alphanumeric). |
504
+ | `identity_verification_status` | string | No | Yes | Latest identity-document verification status; `InReview` means the account must decide it before the document can be certificated. enum: `["Pending", "InReview", "Approved", "Declined", "Expired", "Abandoned"]` |
464
505
 
465
506
  ### AssignmentSummary
466
507
 
@@ -790,6 +831,47 @@ A signing party belonging to a workspace account.
790
831
  | `email` | string (email) | No | Yes | — |
791
832
  | `whatsapp_phone_number` | string | No | Yes | E.164 format; normalized on save. |
792
833
  | `has_accepted_terms` | boolean | No | No | — |
834
+ | `government_id` | string | No | Yes | Signer's normalized CPF (11 digits) or CNPJ (14 characters; may be alphanumeric). |
835
+
836
+ ### SignerIdentityReview
837
+
838
+ Account review of a biometric identity verification. Images are available only during InReview.
839
+
840
+ | Property | Type | Required | Nullable | Constraints / description |
841
+ | --- | --- | --- | --- | --- |
842
+ | `id` | string | No | No | — |
843
+ | `status` | string | No | No | — enum: `["Pending", "InReview", "Approved", "Declined", "Expired", "Abandoned"]` |
844
+ | `provider` | string | No | No | — enum: `["AssinafyVerify"]` |
845
+ | `verification_method` | string | No | No | — enum: `["IdDocumentFaceMatchBasic"]` |
846
+ | `reason_codes` | Array<object> | No | No | Why the provider sent it to review, in its order. Unknown codes have the code as description. |
847
+ | `reason_codes[].code` | string | No | No | — |
848
+ | `reason_codes[].description` | string | No | No | — |
849
+ | `evidence_available` | boolean | No | No | — |
850
+ | `expected` | object | No | Yes | What the signer was registered with. |
851
+ | `expected.name` | string | No | Yes | — |
852
+ | `expected.cpf` | string | No | Yes | — |
853
+ | `document` | object | No | Yes | What was read from the document. |
854
+ | `document.type` | string | No | Yes | — |
855
+ | `document.name` | string | No | Yes | — |
856
+ | `document.cpf` | string | No | Yes | — |
857
+ | `matches` | object | No | Yes | — |
858
+ | `matches.cpf` | boolean | No | Yes | — |
859
+ | `matches.name` | number | No | Yes | Name similarity, 0-1. |
860
+ | `matches.birth_date` | boolean | No | Yes | — |
861
+ | `checks` | Array<object> | No | No | — |
862
+ | `checks[].name` | string | No | No | — |
863
+ | `checks[].result` | string | No | No | pass, review, skipped, or a failure result. |
864
+ | `checks[].score` | number | No | Yes | — |
865
+ | `checks[].reasons` | Array<string> | No | No | — |
866
+ | `images` | Array<string> | No | No | The evidence images the reviewer can open with **Signer identity review image**, in display order: `document_front`, `document_back`, `document_pdf`, `face`. Only while `status` is `InReview`; empty once decided. |
867
+ | `ruleset_version` | string | No | Yes | — |
868
+ | `evidence_sha256` | string | No | Yes | — |
869
+ | `reviewed_by` | object | No | Yes | — |
870
+ | `reviewed_by.id` | string | No | No | — |
871
+ | `reviewed_by.name` | string | No | No | — |
872
+ | `reviewed_at` | string (date-time) | No | Yes | — |
873
+ | `review_note` | string | No | Yes | — |
874
+ | `completed_at` | string (date-time) | No | Yes | — |
793
875
 
794
876
  ### SignerSelf
795
877
 
@@ -806,6 +888,8 @@ The current signer, as returned by `GET /v1/signers/self`. Extends Signer with t
806
888
  | `has_signature` | boolean | No | No | Whether the signer has a saved signature image stored. |
807
889
  | `has_initial` | boolean | No | No | Whether the signer has a saved initials image stored. |
808
890
  | `is_signature_reusable` | boolean | No | No | Whether the signer opted to reuse their saved signature/initials in future processes. When false, clients should not pre-render the saved image even if `has_signature`/`has_initial` is true. |
891
+ | `government_id` | string | No | Yes | Signer's normalized CPF (11 digits) or CNPJ (14 characters; may be alphanumeric). |
892
+ | `verification_method` | string | No | No | How this signature request verifies the signer. `Email`/`Whatsapp` ask for the 6-digit code; `IdDocumentFaceMatchBasic` runs the identity document check instead (see **Identity document verification status**). enum: `["Email", "Whatsapp", "DigitalCertificate", "IdDocumentFaceMatchBasic"]` |
809
893
 
810
894
  ### SigningUrl
811
895
 
@@ -52,6 +52,32 @@ module Assinafy
52
52
  @error_code = response_data['code']
53
53
  end
54
54
 
55
+ # Rate-limit window reported by the `X-Rate-Limit-Limit`, `X-Rate-Limit-Remaining` and
56
+ # `X-Rate-Limit-Reset` response headers. Only the headers present are included.
57
+ #
58
+ # @return [Hash{Symbol=>Integer}, nil] `{ limit:, remaining:, reset: }` (reset in seconds), or nil
59
+ # @example
60
+ # e.rate_limit # => { limit: 60, remaining: 0, reset: 42 }
61
+ def rate_limit
62
+ context[:rate_limit]
63
+ end
64
+
65
+ # Seconds to wait before retrying, from the `Retry-After` header of a `429 Too Many Requests`.
66
+ #
67
+ # @return [Integer, nil]
68
+ # @example Back off on a rate-limited request
69
+ # begin
70
+ # client.documents.list
71
+ # rescue Assinafy::ApiError => e
72
+ # raise unless e.status_code == 429
73
+ #
74
+ # sleep(e.retry_after || 1)
75
+ # retry
76
+ # end
77
+ def retry_after
78
+ context[:retry_after]
79
+ end
80
+
55
81
  # Build an {ApiError} from an HTTP status and parsed body. Reads the human
56
82
  # message from `message`, `error`, or `name` (in that order).
57
83
  #