assinafy 1.11.1 → 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: 4c9d44ba9996ee18372b0c965c691f9a74695b98ffd85ebe5e43ae29a4f3b662
4
- data.tar.gz: a0313ec17a476c5439eab8fd004936c337b4b99e14323b8dfd864553b68f22d9
3
+ metadata.gz: d2a0b97db3a9ad302307e31bccf8f06751fe310dd253417bb129ad921b0a4b3f
4
+ data.tar.gz: 94eb893e22a78777a0b4102d572e274e0f22c5b50cd3768d348f2deac69541ab
5
5
  SHA512:
6
- metadata.gz: 4a3e1ef28628add0a0e5abfc6f59f09f5ea0fe09c067a6fbb8cb3b767771590479bb8493b525ce389d2a572f15eea8257ed9a1ba47e68e7be1297603777febbd
7
- data.tar.gz: 159e300170cd02a74d7668084362b5c80f5e99eea2653e2fd5f9faa0a838446b3987f9d5cd9df131cf0f2feba6a0dd50fdae0de4351ebba213c92e942ea3d529
6
+ metadata.gz: 43838b1c27e22fc5210a8e877a4e1897f50030a6e32801cf490f101de6d18805ed79bff013120801ce90ab53369531142bf678cbe0e04f543139aa72790bab11
7
+ data.tar.gz: f0e442b567d84643b2237ec21edf5b59de9155268451986dcdcc19f841ccf1514e1fd11e112fedc0ff1b8c04439feb411fec7ec37bc06128a86409996745798e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
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
+
5
18
  ## [1.11.1] - 2026-10-12
6
19
 
7
20
  ### 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`
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_..."
@@ -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-12 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` |
@@ -111,6 +112,9 @@ Required parameters and object properties are marked with `*`.
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
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) |
@@ -132,6 +136,8 @@ 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
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)> } |
137
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)> } |
@@ -294,7 +300,7 @@ look successful while the connection stays active.
294
300
 
295
301
  Verification and notification methods are coupled per signer: the omitted side is inferred from the other, and
296
302
  when both are omitted both are `Email`. Exactly one notification method is allowed. Allowed pairs: `Email` with
297
- `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`
298
304
  costs 0.45 credits (its WhatsApp notification, paid plans only); `DigitalCertificate` costs 0.5 credits per signer
299
305
  on top of its notification, shown in cost estimates under the breakdown code `SignatureDigitalCertificate`.
300
306
 
@@ -311,10 +317,38 @@ Certificate signers complete the signature in Assinafy's hosted signing flow, re
311
317
  `/signers/certificate/complete`), whose authentication and request/response schemas are not part of the OpenAPI
312
318
  contract. Once the flow completes, the `pades` artifact returns the qualified signature.
313
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
+
314
345
  ### Webhook endpoints and deliveries
315
346
 
316
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
317
- 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`
318
352
  and `#update_endpoint` accept only `url`, `email`, `events`, `name`, `is_active`, and `signing_enabled`.
319
353
  `#register`, `#get`, and `#inactivate` (`/webhooks/subscriptions`, `/webhooks/inactivate`) act on the account's
320
354
  oldest endpoint; `#register` accepts only `url`, `email`, `events`, and `is_active`.
@@ -369,7 +403,7 @@ return-value, and usage details.
369
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) |
370
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) |
371
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) |
372
- | `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) |
373
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) |
374
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) |
375
409
  | `Assinafy::VERSION` | Published SDK version constant. | [`version.rb`](../lib/assinafy/version.rb) |
@@ -466,6 +500,8 @@ A signer within an assignment: the base Signer plus per-assignment verification/
466
500
  | `notified` | boolean | No | Yes | — |
467
501
  | `completed` | boolean | No | Yes | Only present in account-owner contexts. |
468
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"]` |
469
505
 
470
506
  ### AssignmentSummary
471
507
 
@@ -795,6 +831,47 @@ A signing party belonging to a workspace account.
795
831
  | `email` | string (email) | No | Yes | — |
796
832
  | `whatsapp_phone_number` | string | No | Yes | E.164 format; normalized on save. |
797
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 | — |
798
875
 
799
876
  ### SignerSelf
800
877
 
@@ -811,6 +888,8 @@ The current signer, as returned by `GET /v1/signers/self`. Extends Signer with t
811
888
  | `has_signature` | boolean | No | No | Whether the signer has a saved signature image stored. |
812
889
  | `has_initial` | boolean | No | No | Whether the signer has a saved initials image stored. |
813
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"]` |
814
893
 
815
894
  ### SigningUrl
816
895
 
@@ -29,7 +29,10 @@ module Assinafy
29
29
  # certificate (an e-CPF, or an e-CNPJ naming them as legal
30
30
  # representative); a CNPJ requires an e-CNPJ for that company, from any
31
31
  # of its representatives.
32
- VERIFICATION_METHODS = %w[Email Whatsapp DigitalCertificate].freeze
32
+ # - `IdDocumentFaceMatchBasic` — government ID photos, face match and liveness.
33
+ # Requires its account feature and a valid CPF in `government_id`; priced per signer.
34
+ VERIFICATION_METHODS = %w[Email Whatsapp DigitalCertificate IdDocumentFaceMatchBasic].freeze
35
+ IDENTITY_REVIEW_IMAGES = %w[document_front document_back document_pdf face].freeze
33
36
 
34
37
  # Channels used to notify a signer of the request. Exactly one per signer;
35
38
  # WhatsApp incurs an additional cost and is available only on paid
@@ -244,7 +247,8 @@ module Assinafy
244
247
  def validate_method_pair!(signer)
245
248
  verification = signer[:verification_method]
246
249
  notification = signer[:notification_methods]&.first
247
- return if verification.nil? || notification.nil? || verification == 'DigitalCertificate'
250
+ return if verification.nil? || notification.nil? ||
251
+ %w[DigitalCertificate IdDocumentFaceMatchBasic].include?(verification)
248
252
  return if verification == notification
249
253
 
250
254
  raise ValidationError.new('notification_methods must match the verification_method')
@@ -488,6 +492,95 @@ module Assinafy
488
492
  end
489
493
  end
490
494
 
495
+ # Read the account review of a signer's biometric identity verification.
496
+ # Evidence contains summaries; images are available only while InReview.
497
+ # The signer can sign during review, but certification awaits approval.
498
+ #
499
+ # @param document_id [String]
500
+ # @param assignment_id [String]
501
+ # @param signer_id [String]
502
+ # @return [Hash] SignerIdentityReview; see docs/API_REFERENCE.md#signeridentityreview for all fields
503
+ # @raise [Assinafy::ApiError] on failure, including 404 when no verification exists
504
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
505
+ # @raise [Assinafy::ValidationError] on invalid IDs
506
+ # @see GET /documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/identity-verification
507
+ # @example Read an open review (no request body; workspace credentials)
508
+ # client.assignments.identity_review('document-id', 'assignment-id', 'signer-id')
509
+ # # => { 'id' => 'verification-id', 'status' => 'InReview', 'provider' => 'AssinafyVerify',
510
+ # # 'verification_method' => 'IdDocumentFaceMatchBasic',
511
+ # # 'reason_codes' => [{ 'code' => 'NAME_MISMATCH', 'description' => 'Name differs' }],
512
+ # # 'evidence_available' => true, 'expected' => { 'name' => 'Example Signer', 'cpf' => nil },
513
+ # # 'document' => { 'type' => 'CNH', 'name' => 'EXAMPLE SIGNER', 'cpf' => nil },
514
+ # # 'matches' => { 'cpf' => true, 'name' => 0.52, 'birth_date' => nil },
515
+ # # 'checks' => [{ 'name' => 'name', 'result' => 'review', 'score' => 0.52,
516
+ # # 'reasons' => ['NAME_MISMATCH'] }], 'images' => ['document_front', 'face'],
517
+ # # 'ruleset_version' => '2026-10-12.2', 'evidence_sha256' => 'sha256-placeholder',
518
+ # # 'reviewed_by' => nil, 'reviewed_at' => nil, 'review_note' => nil, 'completed_at' => nil }
519
+ def identity_review(document_id, assignment_id, signer_id)
520
+ path = identity_review_path(document_id, assignment_id, signer_id)
521
+
522
+ call('Failed to fetch signer identity review') { http_get(path) }
523
+ end
524
+
525
+ # Download one evidence image or document PDF while a review is InReview.
526
+ # Fetched from the provider on each call; keep the sensitive bytes out of logs and caches.
527
+ #
528
+ # @param document_id [String]
529
+ # @param assignment_id [String]
530
+ # @param signer_id [String]
531
+ # @param name [String] document_front, document_back, document_pdf or face, as listed in the review
532
+ # @return [String] binary JPEG, PNG or PDF bytes
533
+ # @raise [Assinafy::ApiError] on failure; 404 after a decision or when evidence is unavailable
534
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
535
+ # @raise [Assinafy::ValidationError] on invalid IDs or image name
536
+ # @see GET /documents/{document}/assignments/{assignment}/signers/{signer}/identity-verification/images/{name}
537
+ # @example Read the face image (no request body; workspace credentials)
538
+ # client.assignments.identity_review_image('document-id', 'assignment-id', 'signer-id', 'face')
539
+ # # => "\x89PNG\r\n\x1A\n..." # binary String
540
+ def identity_review_image(document_id, assignment_id, signer_id, name)
541
+ path = identity_review_path(document_id, assignment_id, signer_id)
542
+ unless IDENTITY_REVIEW_IMAGES.include?(name)
543
+ raise ValidationError.new("Image name must be one of: #{IDENTITY_REVIEW_IMAGES.join(', ')}")
544
+ end
545
+
546
+ call_binary('Failed to download signer identity review image') { http_get("#{path}/images/#{name}") }
547
+ end
548
+
549
+ # Approve or reject an InReview identity verification. Rejection cancels the
550
+ # document and notifies every signer and copy receiver. The note is recorded
551
+ # in activities and sent to the provider; it is never shown to the signer.
552
+ #
553
+ # @param document_id [String]
554
+ # @param assignment_id [String]
555
+ # @param signer_id [String]
556
+ # @param decision [String] Approved or Rejected
557
+ # @param note [String, nil] optional review note, at most 2000 characters
558
+ # @return [Hash] SignerIdentityReview, the same fields as {#identity_review}, with the decision recorded
559
+ # @raise [Assinafy::ApiError] on failure; 400 unless InReview and awaiting signatures or identity review
560
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
561
+ # @raise [Assinafy::ValidationError] on invalid IDs, decision or note
562
+ # @see POST /documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/identity-verification/review
563
+ # @example Approve a review with workspace credentials
564
+ # client.assignments.decide_identity_review('document-id', 'assignment-id', 'signer-id',
565
+ # decision: 'Approved', note: 'Identity checked')
566
+ # # Body: { 'decision' => 'Approved', 'note' => 'Identity checked' }
567
+ # # => the identity_review object with 'status' => 'Approved', 'images' => [],
568
+ # # 'reviewed_by' => { 'id' => 'user-id', 'name' => 'Example Reviewer' },
569
+ # # 'reviewed_at' => '2026-10-16T12:00:00Z', 'review_note' => 'Identity checked'
570
+ def decide_identity_review(document_id, assignment_id, signer_id, decision:, note: nil)
571
+ path = identity_review_path(document_id, assignment_id, signer_id)
572
+ unless %w[Approved Rejected].include?(decision)
573
+ raise ValidationError.new('Decision must be Approved or Rejected')
574
+ end
575
+ unless note.nil? || (note.is_a?(String) && note.length <= 2000)
576
+ raise ValidationError.new('Review note must be a String of at most 2000 characters')
577
+ end
578
+
579
+ call('Failed to decide signer identity review') do
580
+ http_post("#{path}/review", body_params(decision: decision, note: note))
581
+ end
582
+ end
583
+
491
584
  # Fetch the document a signer is being asked to sign (signer-access-code auth).
492
585
  #
493
586
  # @param signer_access_code [String]
@@ -626,6 +719,13 @@ module Assinafy
626
719
 
627
720
  private
628
721
 
722
+ def identity_review_path(document_id, assignment_id, signer_id)
723
+ doc_id = require_id(document_id, 'Document ID')
724
+ asg_id = require_id(assignment_id, 'Assignment ID')
725
+ sid = require_id(signer_id, 'Signer ID')
726
+ "documents/#{doc_id}/assignments/#{asg_id}/signers/#{sid}/identity-verification"
727
+ end
728
+
629
729
  def normalise_sign_item(item)
630
730
  normalised = require_payload(item, 'Assignment item').each_with_object({}) do |(key, value), result|
631
731
  raw = key.to_s
@@ -149,7 +149,7 @@ module Assinafy
149
149
  # @option payload [String] :email
150
150
  # @option payload [String] :whatsapp_phone_number E.164; normalized on save
151
151
  # @option payload [String] :phone alias for `:whatsapp_phone_number`
152
- # @option payload [String] :government_id CPF/CNPJ; digits only on save
152
+ # @option payload [String] :government_id CPF (digits) or CNPJ (may be alphanumeric); normalized on save
153
153
  # @param account_id_override [String, nil]
154
154
  # @return [Hash] updated signer object (envelope `data` unwrapped)
155
155
  # @raise [Assinafy::ApiError] on an unsuccessful API response
@@ -273,6 +273,60 @@ module Assinafy
273
273
  end
274
274
  end
275
275
 
276
+ # Read the signer's biometric verification state. Only an absent or Expired
277
+ # session can start again. InReview permits signing while account review
278
+ # holds certification; Declined or Abandoned needs a new document and assignment.
279
+ #
280
+ # @param signer_access_code [String]
281
+ # @return [Hash] can_start, status, url, expires_at and completed_at (nullable except can_start)
282
+ # @raise [Assinafy::ApiError] on an unsuccessful API response
283
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
284
+ # @raise [Assinafy::ValidationError] on a missing or invalid access code
285
+ # @see GET /signers/identity-verification
286
+ # @example Read state with signer-only authentication
287
+ # client.signers.identity_verification(signer_access_code: 'signer-access-code')
288
+ # # GET /signers/identity-verification?signer-access-code=signer-access-code (no body)
289
+ # # => { 'can_start' => false, 'status' => 'Pending',
290
+ # # 'url' => 'https://verify.assinafy.com.br/c/session-id#t=token-placeholder',
291
+ # # 'expires_at' => '2026-10-16T13:00:00Z', 'completed_at' => nil }
292
+ # # The URL is a bearer link: open it intact, including its fragment; never log or cache it.
293
+ def identity_verification(signer_access_code:)
294
+ code = require_signer_access_code(signer_access_code)
295
+
296
+ call('Failed to fetch signer identity verification') do
297
+ http_get('signers/identity-verification', { signer_access_code: code }, workspace_auth: false)
298
+ end
299
+ end
300
+
301
+ # Record explicit biometric-processing consent and open the hosted verification
302
+ # session. Restart is allowed only after Expired; it incurs no additional fee.
303
+ #
304
+ # @param consent [Boolean] must be true, supplied explicitly by the signer
305
+ # @param signer_access_code [String]
306
+ # @return [Hash] id, hosted capture url and nullable expires_at
307
+ # @raise [Assinafy::ApiError] on failure; 400 when a session already exists and is not Expired
308
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
309
+ # @raise [Assinafy::ValidationError] on missing consent or an invalid access code
310
+ # @see POST /signers/identity-verification
311
+ # @example Start with signer-only authentication after collecting consent
312
+ # client.signers.start_identity_verification(consent: true, signer_access_code: 'signer-access-code')
313
+ # # POST /signers/identity-verification?signer-access-code=signer-access-code
314
+ # # Body: { 'consent' => true }
315
+ # # => { 'id' => 'verification-id',
316
+ # # 'url' => 'https://verify.assinafy.com.br/c/session-id#t=token-placeholder',
317
+ # # 'expires_at' => '2026-10-16T13:00:00Z' }
318
+ # # Redirect to url unchanged, including its fragment; never log or cache the bearer link.
319
+ def start_identity_verification(consent:, signer_access_code:)
320
+ raise ValidationError.new('Biometric processing requires explicit consent: true') unless consent == true
321
+
322
+ code = require_signer_access_code(signer_access_code)
323
+
324
+ call('Failed to start signer identity verification') do
325
+ http_post('signers/identity-verification', body_params(consent: consent),
326
+ { signer_access_code: code }, workspace_auth: false)
327
+ end
328
+ end
329
+
276
330
  # Accept the platform's terms of use as the signer.
277
331
  #
278
332
  # The `signer-access-code` is sent as the documented query parameter (the
@@ -49,11 +49,11 @@ module Assinafy
49
49
  # # }
50
50
  # # => unwrapped data payload returned:
51
51
  # # {
52
- # # events: ["document_ready", "document_prepared"],
53
- # # is_active: true,
54
- # # url: "https://example.com/webhook",
55
- # # email: "ops@example.com",
56
- # # updated_at: "2026-06-05T21:13:24Z"
52
+ # # 'events' => ["document_ready", "document_prepared"],
53
+ # # 'is_active' => true,
54
+ # # 'url' => "https://example.com/webhook",
55
+ # # 'email' => "ops@example.com",
56
+ # # 'updated_at' => "2026-06-05T21:13:24Z"
57
57
  # # }
58
58
  def register(payload, account_id_override = nil)
59
59
  body = { is_active: true }.merge(webhook_body(payload, SUBSCRIPTION_FIELDS, REQUIRED_FIELDS))
@@ -82,11 +82,11 @@ module Assinafy
82
82
  # # GET /accounts/{account_id}/webhooks/subscriptions
83
83
  # # => unwrapped data payload returned (nil if no subscription exists):
84
84
  # # {
85
- # # events: ["document_ready", "signer_signed_document"],
86
- # # is_active: false,
87
- # # url: "https://example.com/sdk-smoke-webhook",
88
- # # email: "webhook@example.com",
89
- # # updated_at: "2026-06-05T21:13:24Z"
85
+ # # 'events' => ["document_ready", "signer_signed_document"],
86
+ # # 'is_active' => false,
87
+ # # 'url' => "https://example.com/sdk-smoke-webhook",
88
+ # # 'email' => "webhook@example.com",
89
+ # # 'updated_at' => "2026-06-05T21:13:24Z"
90
90
  # # }
91
91
  def get(account_id_override = nil)
92
92
  acc_id = account_id(account_id_override)
@@ -111,11 +111,11 @@ module Assinafy
111
111
  # # PUT /accounts/{account_id}/webhooks/inactivate (no request body)
112
112
  # # => unwrapped data payload returned:
113
113
  # # {
114
- # # events: ["document_ready", "document_prepared"],
115
- # # is_active: false,
116
- # # url: "https://example.com/webhook",
117
- # # email: "ops@example.com",
118
- # # updated_at: "2026-06-05T21:13:24Z"
114
+ # # 'events' => ["document_ready", "document_prepared"],
115
+ # # 'is_active' => false,
116
+ # # 'url' => "https://example.com/webhook",
117
+ # # 'email' => "ops@example.com",
118
+ # # 'updated_at' => "2026-06-05T21:13:24Z"
119
119
  # # }
120
120
  def inactivate(account_id_override = nil)
121
121
  acc_id = account_id(account_id_override)
@@ -129,7 +129,7 @@ module Assinafy
129
129
 
130
130
  # Catalogue of supported event-type identifiers.
131
131
  #
132
- # @return [Array<Hash>] each entry is { id:, description: } (18 event types available)
132
+ # @return [Array<Hash>] each entry is { id:, description: }
133
133
  # @raise [Assinafy::ApiError] on an unsuccessful API response
134
134
  # @raise [Assinafy::NetworkError] on transport or TLS failure
135
135
  # @raise [Assinafy::ValidationError] on invalid required input
@@ -137,17 +137,17 @@ module Assinafy
137
137
  # @example List subscribable event types
138
138
  # client.webhooks.list_event_types
139
139
  # # GET /webhooks/event-types
140
- # # => unwrapped data payload returned (18 entries):
140
+ # # => unwrapped data payload returned:
141
141
  # # [
142
- # # { id: "document_uploaded", description: "Triggered when the User has uploaded a Document" },
143
- # # { id: "document_metadata_ready", description: "Triggered when the document is ready to be prepared..." },
144
- # # { id: "document_prepared", description: "Triggered when the User prepares a Document." },
145
- # # { id: "assignment_created", description: "Triggered when the User created an assignment..." },
146
- # # { id: "signature_requested", description: "Triggered when the User requested signature..." },
147
- # # { id: "document_ready", description: "Triggered when the last Signer signs the Document..." },
148
- # # { id: "signer_created", description: "Triggered when the User created a Signer" },
149
- # # { id: "signer_email_verified", description: "Triggered when Signer's email has been verified..." }
150
- # # # ... (see docs for the full 18-event catalogue)
142
+ # # { 'id' => "document_uploaded", 'description' => "Document uploaded" },
143
+ # # { 'id' => "document_metadata_ready", 'description' => "Document metadata ready" },
144
+ # # { 'id' => "document_prepared", 'description' => "Document prepared" },
145
+ # # { 'id' => "assignment_created", 'description' => "Assignment created" },
146
+ # # { 'id' => "signature_requested", 'description' => "Signature requested" },
147
+ # # { 'id' => "document_ready", 'description' => "All signers signed" },
148
+ # # { 'id' => "signer_created", 'description' => "Signer created" },
149
+ # # { 'id' => "signer_email_verified", 'description' => "Signer email verified" }
150
+ # # # ... (read list_event_types for the complete current catalogue)
151
151
  # # ]
152
152
  def list_event_types
153
153
  call_array('Failed to list webhook event types') do
@@ -172,17 +172,17 @@ module Assinafy
172
172
  # # {
173
173
  # # data: [
174
174
  # # {
175
- # # id: "dispatch-id",
175
+ # # 'id' => "dispatch-id",
176
176
  # # event: "signature_requested",
177
177
  # # activity_id: 15431,
178
178
  # # endpoint: "https://example.com/webhook",
179
- # # payload: { id: 15431, event: "signature_requested", object: {}, subject: {}, payload: {} },
179
+ # # payload: { 'id' => 15431, event: "signature_requested", object: {}, subject: {}, payload: {} },
180
180
  # # delivered: false,
181
181
  # # http_status: 404,
182
182
  # # response_body: "{\"success\":false,...}",
183
183
  # # error: "Client error: `POST https://example.com/webhook` resulted in a 404 ...",
184
- # # created_at: "2026-07-20T15:57:38Z",
185
- # # updated_at: "2026-07-20T15:57:38Z"
184
+ # # 'created_at' => "2026-07-20T15:57:38Z",
185
+ # # 'updated_at' => "2026-07-20T15:57:38Z"
186
186
  # # }
187
187
  # # # ... (see docs for full dispatch shape)
188
188
  # # ],
@@ -211,17 +211,17 @@ module Assinafy
211
211
  # # => unwrapped data payload returned:
212
212
  # # {
213
213
  # # resource: "activity_dispatching_history",
214
- # # id: "dispatch-id",
214
+ # # 'id' => "dispatch-id",
215
215
  # # event: "signature_requested",
216
216
  # # activity_id: 15431,
217
217
  # # endpoint: "https://example.com/webhook",
218
- # # payload: { id: 15431, event: "signature_requested", object: {}, subject: {} },
218
+ # # payload: { 'id' => 15431, event: "signature_requested", object: {}, subject: {} },
219
219
  # # delivered: true,
220
220
  # # http_status: 200,
221
221
  # # response_body: "OK",
222
222
  # # error: nil,
223
- # # created_at: "2026-07-20T15:57:38Z",
224
- # # updated_at: "2026-07-20T15:57:39Z"
223
+ # # 'created_at' => "2026-07-20T15:57:38Z",
224
+ # # 'updated_at' => "2026-07-20T15:57:39Z"
225
225
  # # }
226
226
  def retry_dispatch(dispatch_id, account_id_override = nil)
227
227
  acc_id = account_id(account_id_override)
@@ -235,6 +235,7 @@ module Assinafy
235
235
  # List the account's webhook endpoints, oldest first.
236
236
  #
237
237
  # @param account_id_override [String, nil]
238
+ # @param url [String, nil] filter by the exact registered URL
238
239
  # @return [Array<Hash>] webhook endpoint objects
239
240
  # @raise [Assinafy::ApiError] on an unsuccessful API response
240
241
  # @raise [Assinafy::NetworkError] on transport or TLS failure
@@ -246,29 +247,31 @@ module Assinafy
246
247
  # # => unwrapped data payload returned:
247
248
  # # [
248
249
  # # {
249
- # # id: "webhook-endpoint-id",
250
- # # name: "ERP",
251
- # # url: "https://example.com/webhooks/assinafy",
252
- # # email: "ops@example.com",
253
- # # events: ["document_ready", "signer_signed_document"],
254
- # # is_active: true,
255
- # # signing_enabled: true,
256
- # # created_at: "2026-10-01T12:00:00Z",
257
- # # updated_at: "2026-10-01T12:00:00Z"
250
+ # # 'id' => "webhook-endpoint-id",
251
+ # # 'name' => "ERP",
252
+ # # 'url' => "https://example.com/webhooks/assinafy",
253
+ # # 'email' => "ops@example.com",
254
+ # # 'events' => ["document_ready", "signer_signed_document"],
255
+ # # 'is_active' => true,
256
+ # # 'signing_enabled' => true,
257
+ # # 'created_at' => "2026-10-01T12:00:00Z",
258
+ # # 'updated_at' => "2026-10-01T12:00:00Z"
258
259
  # # }
259
260
  # # ]
260
- def list_endpoints(account_id_override = nil)
261
+ def list_endpoints(account_id_override = nil, url: nil)
261
262
  acc_id = account_id(account_id_override)
263
+ require_string(url, 'Webhook URL') unless url.nil?
262
264
 
263
265
  call_array('Failed to list webhook endpoints') do
264
- http_get("accounts/#{acc_id}/webhooks/endpoints")
266
+ http_get("accounts/#{acc_id}/webhooks/endpoints", { url: url })
265
267
  end
266
268
  end
267
269
 
268
270
  # Register a new webhook endpoint. Each endpoint of an account needs a
269
271
  # distinct `url`. Creating one past the plan's limit (1, or 3 on paid
270
272
  # plans) answers `403`. With `signing_enabled: true` a signing secret is
271
- # generated; read it with {#endpoint_secret}.
273
+ # generated and returned once under `secret`. Store it from this response,
274
+ # especially for OAuth apps, which cannot call {#endpoint_secret}.
272
275
  #
273
276
  # @param payload [Hash]
274
277
  # @option payload [String] :url http(s) URL that receives the events (required)
@@ -301,9 +304,10 @@ module Assinafy
301
304
  # # "signing_enabled": true
302
305
  # # }
303
306
  # # => unwrapped data payload returned (same shape as {#list_endpoints} entries):
304
- # # { id: "webhook-endpoint-id", name: "ERP", url: "https://example.com/webhooks/assinafy",
305
- # # email: "ops@example.com", events: [...], is_active: true, signing_enabled: true,
306
- # # created_at: "2026-10-01T12:00:00Z", updated_at: "2026-10-01T12:00:00Z" }
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,
309
+ # # 'created_at' => "2026-10-01T12:00:00Z", 'updated_at' => "2026-10-01T12:00:00Z",
310
+ # # 'secret' => 'whsec_secret-placeholder' } # secret appears only on creation with signing enabled
307
311
  def create_endpoint(payload, account_id_override = nil)
308
312
  body = webhook_body(payload, ENDPOINT_FIELDS, REQUIRED_FIELDS)
309
313
  acc_id = account_id(account_id_override)
@@ -327,7 +331,7 @@ module Assinafy
327
331
  # @example Fetch an endpoint
328
332
  # client.webhooks.get_endpoint('webhook-endpoint-id')
329
333
  # # GET /accounts/{account_id}/webhooks/endpoints/webhook-endpoint-id
330
- # # => { id: "webhook-endpoint-id", name: "ERP", url: "https://example.com/webhooks/assinafy", ... }
334
+ # # => { 'id' => "webhook-endpoint-id", 'name' => "ERP", 'url' => "https://example.com/webhooks/assinafy", ... }
331
335
  def get_endpoint(endpoint_id, account_id_override = nil)
332
336
  call('Failed to fetch webhook endpoint') do
333
337
  http_get(endpoint_path(endpoint_id, account_id_override))
@@ -351,7 +355,7 @@ module Assinafy
351
355
  # # PUT /accounts/{account_id}/webhooks/endpoints/webhook-endpoint-id
352
356
  # # request body sent by the SDK:
353
357
  # # { "is_active": false }
354
- # # => { id: "webhook-endpoint-id", is_active: false, ... }
358
+ # # => { 'id' => "webhook-endpoint-id", 'is_active' => false, ... }
355
359
  def update_endpoint(endpoint_id, payload, account_id_override = nil)
356
360
  body = webhook_body(payload, ENDPOINT_FIELDS, [])
357
361
  raise ValidationError.new('Webhook endpoint update must change at least one field') if body.empty?
@@ -386,13 +390,37 @@ module Assinafy
386
390
  end
387
391
  end
388
392
 
393
+ # Delete the endpoint registered with an exact URL and free its slot.
394
+ # Requires webhooks:write for OAuth applications.
395
+ #
396
+ # @param url [String] exact URL used when registering the endpoint
397
+ # @param account_id_override [String, nil]
398
+ # @return [nil] the SDK discards the empty data array on success
399
+ # @raise [Assinafy::ApiError] on failure; 404 when the workspace has no matching endpoint
400
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
401
+ # @raise [Assinafy::ValidationError] on invalid URL or account ID
402
+ # @see DELETE /accounts/{account_id}/webhooks/endpoints
403
+ # @example Remove an integration by its registered URL
404
+ # client.webhooks.delete_endpoint_by_url('https://app.example.com/webhooks/assinafy')
405
+ # # DELETE /accounts/{account_id}/webhooks/endpoints?url=https%3A%2F%2Fapp.example.com%2Fwebhooks%2Fassinafy
406
+ # # No request body. Response envelope data: []
407
+ # # => nil
408
+ def delete_endpoint_by_url(url, account_id_override = nil)
409
+ acc_id = account_id(account_id_override)
410
+ target = require_string(url, 'Webhook URL')
411
+
412
+ call_void('Failed to delete webhook endpoint by URL') do
413
+ http_delete("accounts/#{acc_id}/webhooks/endpoints", { url: target })
414
+ end
415
+ end
416
+
389
417
  # Read the Standard Webhooks secret that signs deliveries to an endpoint.
390
418
  # Pass it to {Assinafy::Support::WebhookVerifier}. Not available to OAuth
391
419
  # applications; use an API key or a user access token.
392
420
  #
393
421
  # @param endpoint_id [String]
394
422
  # @param account_id_override [String, nil]
395
- # @return [Hash] `{ secret: "whsec_..." }`
423
+ # @return [Hash] `{ "secret" => "whsec_..." }`
396
424
  # @raise [Assinafy::ApiError] `400` when signing is disabled; `404` on an unknown ID
397
425
  # @raise [Assinafy::NetworkError] on transport or TLS failure
398
426
  # @raise [Assinafy::ValidationError] on invalid required input
@@ -400,7 +428,7 @@ module Assinafy
400
428
  # @example Read the signing secret
401
429
  # client.webhooks.endpoint_secret('webhook-endpoint-id')
402
430
  # # GET /accounts/{account_id}/webhooks/endpoints/webhook-endpoint-id/secret
403
- # # => { secret: "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw" }
431
+ # # => { 'secret' => "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw" }
404
432
  def endpoint_secret(endpoint_id, account_id_override = nil)
405
433
  call('Failed to fetch webhook endpoint secret') do
406
434
  http_get("#{endpoint_path(endpoint_id, account_id_override)}/secret")
@@ -413,7 +441,7 @@ module Assinafy
413
441
  #
414
442
  # @param endpoint_id [String]
415
443
  # @param account_id_override [String, nil]
416
- # @return [Hash] `{ secret: "whsec_..." }` (the new secret)
444
+ # @return [Hash] `{ "secret" => "whsec_..." }` (the new secret)
417
445
  # @raise [Assinafy::ApiError] `400` when signing is disabled; `404` on an unknown ID
418
446
  # @raise [Assinafy::NetworkError] on transport or TLS failure
419
447
  # @raise [Assinafy::ValidationError] on invalid required input
@@ -421,7 +449,7 @@ module Assinafy
421
449
  # @example Rotate the signing secret
422
450
  # client.webhooks.rotate_endpoint_secret('webhook-endpoint-id')
423
451
  # # POST /accounts/{account_id}/webhooks/endpoints/webhook-endpoint-id/secret/rotate (no request body)
424
- # # => { secret: "whsec_new-secret-placeholder" }
452
+ # # => { 'secret' => "whsec_new-secret-placeholder" }
425
453
  def rotate_endpoint_secret(endpoint_id, account_id_override = nil)
426
454
  path = endpoint_path(endpoint_id, account_id_override)
427
455
 
@@ -20,7 +20,7 @@ module Assinafy
20
20
  # with a hex HMAC-SHA256.
21
21
  #
22
22
  # @example Verify a signed delivery (Rack / Rails)
23
- # secret = client.webhooks.endpoint_secret('webhook-endpoint-id')[:secret]
23
+ # secret = client.webhooks.endpoint_secret('webhook-endpoint-id').fetch('secret')
24
24
  # verifier = Assinafy::Support::WebhookVerifier.new(secret)
25
25
  # raw_body = request.body.read
26
26
  # return head(:unauthorized) unless verifier.verify_delivery(raw_body, request.headers)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Assinafy
4
- VERSION = '1.11.1'
4
+ VERSION = '2.0.0'
5
5
  USER_AGENT = "Assinafy-Ruby-SDK/v#{VERSION}".freeze
6
6
  end
data/sig/assinafy.rbs CHANGED
@@ -295,6 +295,8 @@ module Assinafy
295
295
  def delete: (String signer_id, ?String? account_id_override) -> nil
296
296
  def find_by_email: (String email, ?String? account_id_override) -> api_object?
297
297
  def self_data: (signer_access_code: String) -> api_object
298
+ def identity_verification: (signer_access_code: String) -> api_object
299
+ def start_identity_verification: (consent: bool, signer_access_code: String) -> api_object
298
300
  def accept_terms: (signer_access_code: String) -> nil
299
301
  def verify_email: (verification_code: String, signer_access_code: String) -> nil
300
302
  def confirm_data: (String document_id, params payload, signer_access_code: String) -> api_object
@@ -335,6 +337,7 @@ module Assinafy
335
337
  VERIFICATION_METHODS: Array[String]
336
338
  NOTIFICATION_METHODS: Array[String]
337
339
  SIGN_ITEM_KEY_MAP: Hash[String, String]
340
+ IDENTITY_REVIEW_IMAGES: Array[String]
338
341
 
339
342
  def self.build_payload: (params payload, ?params options) -> Hash[String, untyped]
340
343
  def list: (?params params, ?String? account_id_override) -> list_result
@@ -343,6 +346,15 @@ module Assinafy
343
346
  def reset_expiration: (String document_id, String assignment_id, String? expires_at) -> api_object
344
347
  def resend_notification: (String document_id, String assignment_id, String signer_id) -> api_object
345
348
  def estimate_resend_cost: (String document_id, String assignment_id, String signer_id) -> api_object
349
+ def identity_review: (String document_id, String assignment_id, String signer_id) -> api_object
350
+ def identity_review_image: (String document_id, String assignment_id, String signer_id, String name) -> String
351
+ def decide_identity_review: (
352
+ String document_id,
353
+ String assignment_id,
354
+ String signer_id,
355
+ decision: String,
356
+ ?note: String?
357
+ ) -> api_object
346
358
  def signer_document: (signer_access_code: String, ?has_accepted_terms: bool?) -> api_object
347
359
  def sign: (String document_id, String assignment_id, Array[params] items, signer_access_code: String) -> api_object
348
360
  def decline: (
@@ -362,11 +374,12 @@ module Assinafy
362
374
  def list_event_types: () -> Array[api_object]
363
375
  def list_dispatches: (?params params, ?String? account_id_override) -> list_result
364
376
  def retry_dispatch: (String dispatch_id, ?String? account_id_override) -> api_object
365
- def list_endpoints: (?String? account_id_override) -> Array[api_object]
377
+ def list_endpoints: (?String? account_id_override, ?url: String?) -> Array[api_object]
366
378
  def create_endpoint: (params payload, ?String? account_id_override) -> api_object
367
379
  def get_endpoint: (String endpoint_id, ?String? account_id_override) -> api_object
368
380
  def update_endpoint: (String endpoint_id, params payload, ?String? account_id_override) -> api_object
369
381
  def delete_endpoint: (String endpoint_id, ?String? account_id_override) -> nil
382
+ def delete_endpoint_by_url: (String url, ?String? account_id_override) -> nil
370
383
  def endpoint_secret: (String endpoint_id, ?String? account_id_override) -> api_object
371
384
  def rotate_endpoint_secret: (String endpoint_id, ?String? account_id_override) -> api_object
372
385
  end
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: 1.11.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Assinafy SDK Contributors