assinafy 1.11.1 → 2.0.1

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: 83473f4fd075a7c665a16ee6ea6c38c64c548927930221d24ecd468cce1f4203
4
+ data.tar.gz: 57c81beb685fe09baf9de80eb36abc628d224952b2b08e582d19e24ecb402c16
5
5
  SHA512:
6
- metadata.gz: 4a3e1ef28628add0a0e5abfc6f59f09f5ea0fe09c067a6fbb8cb3b767771590479bb8493b525ce389d2a572f15eea8257ed9a1ba47e68e7be1297603777febbd
7
- data.tar.gz: 159e300170cd02a74d7668084362b5c80f5e99eea2653e2fd5f9faa0a838446b3987f9d5cd9df131cf0f2feba6a0dd50fdae0de4351ebba213c92e942ea3d529
6
+ metadata.gz: dda868b4808b8885693edc57d8825eaf25a31dd628395180f651bd479a2b59ced524205683900812aa61654b2e79a7c69a110ca8c99c54ea084f21ea3515640a
7
+ data.tar.gz: ba6c518504f42fddaa0f9cfe4f58b0bfe6e23a83255f2cee5ef1a76551fba72b628f8ea448ba93b24201b055fd41c2ee95882534c3eedfb334f49a5b23cb5439
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to the `assinafy` Ruby gem are documented here.
4
4
 
5
+ ## [2.0.1] - 2026-10-19
6
+
7
+ ### Fixed
8
+
9
+ - Assignment creation and cost estimation reject `DigitalCertificate` signers unless the assignment method is
10
+ `virtual`, before an unsignable collect assignment can invite the signer or consume credits. The local
11
+ `ValidationError` exposes the stable code `digital_certificate_requires_virtual` in `errors[:code]`.
12
+
13
+ ## [2.0.0] - 2026-10-16
14
+
15
+ ### Added
16
+
17
+ - `IdDocumentFaceMatchBasic` biometric verification in assignments, cost estimates and template generation,
18
+ paired with either Email or Whatsapp notifications.
19
+ - `SignerResource#identity_verification` and `#start_identity_verification` for hosted identity verification
20
+ with explicit biometric-processing consent and signer-only authentication.
21
+ - `AssignmentResource#identity_review`, `#identity_review_image` and `#decide_identity_review` for account
22
+ reviewers to inspect evidence and approve or reject a biometric identity review.
23
+ - `WebhookResource#list_endpoints(url:)` filters by exact URL, and `#delete_endpoint_by_url` removes the endpoint
24
+ registered with that URL.
25
+
5
26
  ## [1.11.1] - 2026-10-12
6
27
 
7
28
  ### Added
data/README.en.md CHANGED
@@ -388,14 +388,17 @@ 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
394
395
  notification charges the notification again.
395
396
  - `DigitalCertificate` needs the Digital Certificate account feature (Standard and Pro plans), a CPF or CNPJ in the
396
- signer's `government_id`, and the signer alone in its signing step. A CPF requires that person's certificate (an
397
- e-CPF, or an e-CNPJ naming them as legal representative); a CNPJ requires an e-CNPJ for that company, from any of
398
- its representatives.
397
+ signer's `government_id`, a `method: 'virtual'` assignment, and the signer alone in its signing step. A collect
398
+ assignment, including one inferred from a template with positioned signer fields, cannot use certificate
399
+ signing. Direct assignment creation and estimation reject it with `ValidationError` code
400
+ `digital_certificate_requires_virtual`. A CPF requires that person's certificate (an e-CPF, or an e-CNPJ naming
401
+ them as legal representative); a CNPJ requires an e-CNPJ for that company, from any of its representatives.
399
402
  - `AssignmentResource::VERIFICATION_METHODS` and `::NOTIFICATION_METHODS` publish the enums. `build_payload`
400
403
  validates values, pairings, and `step` locally, so a typo fails before the request is sent.
401
404
 
@@ -432,10 +435,11 @@ assignment = client.assignments.create(
432
435
  assignment['signing_urls'] # => [{ 'signer_id' => 'signer-id', 'url' => 'https://.../sign/...' }, ...]
433
436
  ```
434
437
 
435
- `virtual` has no positioned fields; `collect` adds `entries` of positioned fields (see
436
- [Assignments](#assignments)). Both require a non-empty `signers` array. Steps are all-or-none, positive, and
437
- contiguous from 1; equal steps sign in parallel, and each step is notified when the previous one finishes. A
438
- certificate signer is alone in its step. Deadlines are zoned ISO 8601 and at least one hour ahead.
438
+ `virtual` has no positioned fields and is required for `DigitalCertificate`; `collect` adds `entries` of
439
+ positioned fields (see [Assignments](#assignments)). Both require a non-empty `signers` array. Steps are
440
+ all-or-none, positive, and contiguous from 1; equal steps sign in parallel, and each step is notified when the
441
+ previous one finishes. A certificate signer is alone in its step. Deadlines are zoned ISO 8601 and at least one
442
+ hour ahead.
439
443
 
440
444
  For a template, bind one distinct existing signer to each role and create the document and assignment together:
441
445
 
@@ -517,6 +521,48 @@ signing.assignments.decline(
517
521
  )
518
522
  ```
519
523
 
524
+ **Biometric signers** use `verification_method: 'IdDocumentFaceMatchBasic'` with exactly one Email or Whatsapp
525
+ notification in virtual/collect assignments or template generation. Store a valid CPF in the signer's
526
+ `government_id` first and call `estimate_cost` or `estimate_cost_from_template` for the current price.
527
+ The OTP example above applies to Email/Whatsapp. For biometrics, read the verification state and collect explicit
528
+ biometric-processing consent before starting:
529
+
530
+ ```ruby
531
+ state = signing.signers.identity_verification(signer_access_code: access_code)
532
+ # => { 'can_start' => true, 'status' => nil, 'url' => nil, 'expires_at' => nil, 'completed_at' => nil }
533
+
534
+ # After the signer explicitly consents to biometric processing:
535
+ session = signing.signers.start_identity_verification(consent: true, signer_access_code: access_code)
536
+ # => { 'id' => 'verification-id', 'url' => 'https://verify.assinafy.com.br/c/session#t=token-placeholder',
537
+ # 'expires_at' => '2026-10-16T13:00:00Z' }
538
+ ```
539
+
540
+ Open the capture URL unchanged, including its `#t=` fragment. It is a bearer link: keep it out of logs and browser
541
+ storage. For an existing Pending session, use the URL from `identity_verification`. `can_start` is true only
542
+ before any session or after Expired; restarting an Expired session costs nothing extra. Declined or Abandoned
543
+ requires a new document and assignment.
544
+
545
+ InReview lets the signer view, confirm data and sign while account review holds certification. After all signers
546
+ sign, the document can be `pending_identity_review`. The account reviewer uses workspace credentials:
547
+
548
+ ```ruby
549
+ review = client.assignments.identity_review(document.fetch('id'), assignment.fetch('id'), signer.fetch('id'))
550
+ # => SignerIdentityReview: status, reason_codes, evidence_available, expected, document, matches,
551
+ # checks, images, ruleset_version, evidence_sha256 and decision metadata
552
+ image = client.assignments.identity_review_image(document.fetch('id'), assignment.fetch('id'), signer.fetch('id'), 'face')
553
+ # => JPEG, PNG or PDF bytes, depending on the name listed in review['images']; do not cache
554
+
555
+ # After the reviewer checks the evidence:
556
+ client.assignments.decide_identity_review(
557
+ document.fetch('id'), assignment.fetch('id'), signer.fetch('id'), decision: 'Approved', note: 'Identity checked'
558
+ )
559
+ ```
560
+
561
+ `decision: 'Rejected'` cancels the document and notifies signers and copy receivers. The optional note accepts at
562
+ most 2000 characters, goes to activities and the provider, and is never shown to the signer. Images exist only
563
+ during InReview. Observe signer_identity_review_required, signer_identity_review_approved,
564
+ signer_identity_review_rejected, signer_identity_verified and signer_identity_verification_failed events.
565
+
520
566
  **A1/A3 certificate signers** complete their signature in Assinafy's hosted signing flow, opened from
521
567
  `assignment['signing_urls']`: they accept the terms, confirm their data, and sign with their A1 or A3 certificate
522
568
  through the Web PKI browser extension. The ordinary signing endpoint rejects certificate signers, and the SDK does
@@ -538,7 +584,7 @@ endpoint = client.webhooks.create_endpoint(
538
584
  name: 'ERP',
539
585
  signing_enabled: true
540
586
  )
541
- secret = client.webhooks.endpoint_secret(endpoint.fetch('id'))['secret'] # => "whsec_..."
587
+ secret = endpoint.fetch('secret') # returned once on creation; store it immediately, including for OAuth apps
542
588
  ```
543
589
 
544
590
  - `create_endpoint` requires `url`, `email`, and `events`; `name`, `is_active` (default `true`), and
@@ -548,7 +594,9 @@ secret = client.webhooks.endpoint_secret(endpoint.fetch('id'))['secret'] # => "
548
594
  endpoint has none and keeps the current one otherwise; `false` discards it.
549
595
  - `rotate_endpoint_secret(id)` returns a new secret; the old one stops working immediately.
550
596
  - 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.
597
+ Both answer `400` when signing is disabled. An OAuth app that loses its secret must delete and recreate the endpoint.
598
+ - `list_endpoints(url: registered_url)` filters by exact URL; `delete_endpoint_by_url(registered_url)` removes
599
+ that endpoint and returns `nil` (`404` when no endpoint matches).
552
600
 
553
601
  **Delivery contract.** Each delivery is a `POST` with an `application/json` body and the headers `webhook-id`
554
602
  (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`.
@@ -530,9 +531,12 @@ dois, ambos assumem `Email`.
530
531
  ### Certificado digital ICP-Brasil (A1/A3)
531
532
 
532
533
  Exige o recurso **Certificado Digital** na conta (planos Standard e Pro), CPF ou CNPJ em
533
- `government_id` do signatário, e que cada signatário por certificado esteja **sozinho no seu
534
- passo**. Um CPF exige o certificado daquela pessoa (e-CPF, ou e-CNPJ que a nomeie como
535
- representante legal); um CNPJ exige um e-CNPJ da empresa, de qualquer um de seus representantes.
534
+ `government_id` do signatário, assignment com `method: 'virtual'` e que cada signatário por certificado esteja
535
+ **sozinho no seu passo**. Um assignment `collect`, inclusive quando inferido de um template com campos de
536
+ signatário posicionados, não aceita certificado digital. A criação e a estimativa diretas levantam
537
+ `ValidationError` com o código estável `digital_certificate_requires_virtual`. Um CPF exige o certificado daquela
538
+ pessoa (e-CPF, ou e-CNPJ que a nomeie como representante legal); um CNPJ exige um e-CNPJ da empresa, de qualquer
539
+ um de seus representantes.
536
540
 
537
541
  ---
538
542
 
@@ -717,6 +721,55 @@ signatario_client.assignments.decline(
717
721
  signatario_client.signer_documents.decline_multiple([doc['id']], decline_reason: 'Não', signer_access_code: codigo)
718
722
  ```
719
723
 
724
+ ### Signatários com verificação biométrica
725
+
726
+ Na criação do assignment (virtual ou collect), ou em `create_from_template`, envie
727
+ `verification_method: 'IdDocumentFaceMatchBasic'` e exatamente um canal de notificação.
728
+ Cadastre o CPF válido do signatário em `government_id` antes de criar o assignment. Use
729
+ `estimate_cost` ou `estimate_cost_from_template` para obter o custo antes de enviar.
730
+
731
+ O fluxo de OTP acima se aplica a `Email` e `Whatsapp`. Para biometria, consulte o estado com o
732
+ código de acesso do signatário e abra uma sessão somente após obter o consentimento explícito:
733
+
734
+ ```ruby
735
+ estado = signatario_client.signers.identity_verification(signer_access_code: codigo)
736
+ # => { 'can_start' => true, 'status' => nil, 'url' => nil, 'expires_at' => nil, 'completed_at' => nil }
737
+
738
+ # Após o consentimento explícito do signatário para o processamento biométrico:
739
+ sessao = signatario_client.signers.start_identity_verification(consent: true, signer_access_code: codigo)
740
+ # => { 'id' => 'verification-id', 'url' => 'https://verify.assinafy.com.br/c/session#t=token-placeholder',
741
+ # 'expires_at' => '2026-10-16T13:00:00Z' }
742
+ ```
743
+
744
+ Abra `sessao['url']` sem alterar a URL nem o fragmento `#t=`; ela concede acesso à captura e não
745
+ deve aparecer em logs ou armazenamento do navegador. Se já houver uma sessão `Pending`, use a
746
+ URL retornada por `identity_verification`. `can_start` só é verdadeiro antes da primeira sessão
747
+ ou após `Expired`; reiniciar uma sessão expirada não cobra novamente. `Declined` e `Abandoned`
748
+ exigem um novo documento e assignment.
749
+
750
+ Em `InReview`, o signatário continua visualizando, confirmando dados e assinando, mas a certificação
751
+ aguarda a revisão da conta. Depois de todas as assinaturas, o documento pode ficar em
752
+ `pending_identity_review`. Com as credenciais do workspace, o revisor consulta e decide:
753
+
754
+ ```ruby
755
+ revisao = client.assignments.identity_review(doc['id'], assignment['id'], signatario['id'])
756
+ # => SignerIdentityReview: status, reason_codes, evidence_available, expected, document,
757
+ # matches, checks, images, ruleset_version, evidence_sha256 e dados da decisão
758
+ imagem = client.assignments.identity_review_image(doc['id'], assignment['id'], signatario['id'], 'face')
759
+ # => bytes JPEG, PNG ou PDF, conforme o nome retornado em revisao['images']; não armazenar em cache
760
+
761
+ # Após o revisor conferir as evidências:
762
+ client.assignments.decide_identity_review(
763
+ doc['id'], assignment['id'], signatario['id'], decision: 'Approved', note: 'Identidade conferida'
764
+ )
765
+ ```
766
+
767
+ `decision: 'Rejected'` cancela o documento e notifica signatários e destinatários de cópia.
768
+ A nota opcional aceita até 2000 caracteres, fica na atividade e vai ao provedor; não é mostrada
769
+ ao signatário. As imagens só ficam disponíveis durante `InReview`. Acompanhe também os eventos
770
+ `signer_identity_review_required`, `signer_identity_review_approved`, `signer_identity_review_rejected`,
771
+ `signer_identity_verified` e `signer_identity_verification_failed`.
772
+
720
773
  ### Signatários com certificado A1/A3
721
774
 
722
775
  O signatário por certificado digital conclui a assinatura no fluxo hospedado da Assinafy, aberto
@@ -749,9 +802,11 @@ endpoint = client.webhooks.create_endpoint(
749
802
  endpoint['id'] # => "webhook-endpoint-id"
750
803
 
751
804
  client.webhooks.list_endpoints # do mais antigo ao mais novo
805
+ client.webhooks.list_endpoints(url: 'https://app.example.com/webhooks/assinafy') # URL exata
752
806
  client.webhooks.get_endpoint('webhook-endpoint-id')
753
807
  client.webhooks.update_endpoint('webhook-endpoint-id', is_active: false) # só os campos enviados
754
808
  client.webhooks.delete_endpoint('webhook-endpoint-id') # => nil, libera a vaga
809
+ client.webhooks.delete_endpoint_by_url('https://app.example.com/webhooks/assinafy') # => nil
755
810
  ```
756
811
 
757
812
  - `create_endpoint` exige `url`, `email` e `events`; `name`, `is_active` (padrão `true`) e
@@ -763,6 +818,10 @@ client.webhooks.delete_endpoint('webhook-endpoint-id') # =>
763
818
 
764
819
  ### Segredo de assinatura
765
820
 
821
+ Com `signing_enabled: true`, `create_endpoint` devolve o segredo uma única vez em
822
+ `endpoint['secret']`. Guarde esse valor na criação, inclusive em aplicativos OAuth. Se o segredo
823
+ de um aplicativo OAuth for perdido, exclua e recrie o endpoint.
824
+
766
825
  ```ruby
767
826
  segredo = client.webhooks.endpoint_secret('webhook-endpoint-id')
768
827
  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
 
@@ -302,19 +308,53 @@ on top of its notification, shown in cost estimates under the breakdown code `Si
302
308
 
303
309
  `DigitalCertificate` has the signer sign with their own ICP-Brasil certificate (A1 or A3) through the Web PKI
304
310
  browser extension, producing a qualified PAdES signature. It requires the Digital Certificate account feature, a
305
- CPF or CNPJ in the signer's `government_id` (accepted by both `SignerResource#create` and `#update`), and that the
306
- signer is alone in its signing step. A CPF requires that person's certificate (an e-CPF, or an e-CNPJ naming them
307
- as legal representative); a CNPJ requires an e-CNPJ for that company, from any of its representatives.
311
+ CPF or CNPJ in the signer's `government_id` (accepted by both `SignerResource#create` and `#update`), a
312
+ `method: 'virtual'` assignment, and that the signer is alone in its signing step. `AssignmentResource#create` and
313
+ `#estimate_cost` reject any other assignment method with `ValidationError`; its stable
314
+ `errors[:code]` is `digital_certificate_requires_virtual`. A CPF requires that person's certificate (an e-CPF, or
315
+ an e-CNPJ naming them as legal representative); a CNPJ requires an e-CNPJ for that company, from any of its
316
+ representatives.
317
+
318
+ A template with positioned signer fields creates a collect assignment, so it cannot be used with a
319
+ `DigitalCertificate` signer. Use a template without positioned signer fields for certificate signing.
308
320
 
309
321
  Certificate signers complete the signature in Assinafy's hosted signing flow, reached through the assignment's
310
322
  `signing_urls`. The SDK does not wrap the two-step Web PKI handshake (`/signers/certificate/start`,
311
323
  `/signers/certificate/complete`), whose authentication and request/response schemas are not part of the OpenAPI
312
324
  contract. Once the flow completes, the `pades` artifact returns the qualified signature.
313
325
 
326
+ ### Biometric identity verification
327
+
328
+ `IdDocumentFaceMatchBasic` requires its account feature and a valid CPF in the signer's `government_id`.
329
+ It uses government ID photos, a photo holding the ID and liveness, and adds a per-signer fee to the chosen
330
+ Email or Whatsapp notification. Call `estimate_cost` or `estimate_cost_from_template` for the current total.
331
+ Both assignment methods and template generation accept this verification method.
332
+
333
+ With the signer access code, read `signers.identity_verification`; after the signer explicitly consents to
334
+ biometric processing, call `signers.start_identity_verification(consent: true, signer_access_code: code)`.
335
+ Open the returned capture URL unchanged, including its fragment; it is a bearer link, so keep it out of logs
336
+ and browser storage. The status response backfills pending provider decisions. `can_start` is true only before
337
+ any session or after Expired; Pending, InReview, Approved, Declined and Abandoned cannot restart. A restart
338
+ replaces an Expired session without another charge. Declined or Abandoned requires a new document and assignment.
339
+
340
+ During InReview the signer may view, confirm data and sign. Certification waits for account approval; once all
341
+ signers sign, the document can be `pending_identity_review`. Read `assignments.identity_review`, download only
342
+ names from its `images` with `identity_review_image`, and submit `decide_identity_review(decision: 'Approved')`
343
+ or `decision: 'Rejected'` while the document is pending_signature or pending_identity_review. Rejection cancels
344
+ the document and notifies signers and copy receivers. Optional `note:` (at most 2000 characters) is recorded in
345
+ activities and sent to the provider; it is never shown to the signer. Images are fetched from the provider,
346
+ available only during InReview, and must not be cached. The review retains summaries, with `evidence_available`
347
+ false when none is retained or fetched. Direct approvals expose evidence through the signer_identity_verified
348
+ activity instead. Observe signer_identity_review_required, signer_identity_review_approved,
349
+ signer_identity_review_rejected, signer_identity_verified and signer_identity_verification_failed events.
350
+
314
351
  ### Webhook endpoints and deliveries
315
352
 
316
353
  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`
354
+ own signing setting, and every active endpoint subscribed to an event receives it. Creation with
355
+ `signing_enabled: true` returns a `secret` once: store it immediately. OAuth apps cannot read or rotate it later;
356
+ if lost, delete and recreate the endpoint. `#list_endpoints(url: registered_url)` finds an exact URL match and
357
+ `#delete_endpoint_by_url(registered_url)` deletes that endpoint, returning `nil`. `WebhookResource#create_endpoint`
318
358
  and `#update_endpoint` accept only `url`, `email`, `events`, `name`, `is_active`, and `signing_enabled`.
319
359
  `#register`, `#get`, and `#inactivate` (`/webhooks/subscriptions`, `/webhooks/inactivate`) act on the account's
320
360
  oldest endpoint; `#register` accepts only `url`, `email`, `events`, and `is_active`.
@@ -369,7 +409,7 @@ return-value, and usage details.
369
409
  | `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
410
  | `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
411
  | `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) |
412
+ | `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
413
  | `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
414
  | `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
415
  | `Assinafy::VERSION` | Published SDK version constant. | [`version.rb`](../lib/assinafy/version.rb) |
@@ -466,6 +506,8 @@ A signer within an assignment: the base Signer plus per-assignment verification/
466
506
  | `notified` | boolean | No | Yes | — |
467
507
  | `completed` | boolean | No | Yes | Only present in account-owner contexts. |
468
508
  | `notification_history` | Array<[`NotificationHistoryEntry`](#notificationhistoryentry)> | No | Yes | Per-channel delivery history for this signer (email + WhatsApp), most-recent send order. |
509
+ | `government_id` | string | No | Yes | Signer's normalized CPF (11 digits) or CNPJ (14 characters; may be alphanumeric). |
510
+ | `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
511
 
470
512
  ### AssignmentSummary
471
513
 
@@ -795,6 +837,47 @@ A signing party belonging to a workspace account.
795
837
  | `email` | string (email) | No | Yes | — |
796
838
  | `whatsapp_phone_number` | string | No | Yes | E.164 format; normalized on save. |
797
839
  | `has_accepted_terms` | boolean | No | No | — |
840
+ | `government_id` | string | No | Yes | Signer's normalized CPF (11 digits) or CNPJ (14 characters; may be alphanumeric). |
841
+
842
+ ### SignerIdentityReview
843
+
844
+ Account review of a biometric identity verification. Images are available only during InReview.
845
+
846
+ | Property | Type | Required | Nullable | Constraints / description |
847
+ | --- | --- | --- | --- | --- |
848
+ | `id` | string | No | No | — |
849
+ | `status` | string | No | No | — enum: `["Pending", "InReview", "Approved", "Declined", "Expired", "Abandoned"]` |
850
+ | `provider` | string | No | No | — enum: `["AssinafyVerify"]` |
851
+ | `verification_method` | string | No | No | — enum: `["IdDocumentFaceMatchBasic"]` |
852
+ | `reason_codes` | Array<object> | No | No | Why the provider sent it to review, in its order. Unknown codes have the code as description. |
853
+ | `reason_codes[].code` | string | No | No | — |
854
+ | `reason_codes[].description` | string | No | No | — |
855
+ | `evidence_available` | boolean | No | No | — |
856
+ | `expected` | object | No | Yes | What the signer was registered with. |
857
+ | `expected.name` | string | No | Yes | — |
858
+ | `expected.cpf` | string | No | Yes | — |
859
+ | `document` | object | No | Yes | What was read from the document. |
860
+ | `document.type` | string | No | Yes | — |
861
+ | `document.name` | string | No | Yes | — |
862
+ | `document.cpf` | string | No | Yes | — |
863
+ | `matches` | object | No | Yes | — |
864
+ | `matches.cpf` | boolean | No | Yes | — |
865
+ | `matches.name` | number | No | Yes | Name similarity, 0-1. |
866
+ | `matches.birth_date` | boolean | No | Yes | — |
867
+ | `checks` | Array<object> | No | No | — |
868
+ | `checks[].name` | string | No | No | — |
869
+ | `checks[].result` | string | No | No | pass, review, skipped, or a failure result. |
870
+ | `checks[].score` | number | No | Yes | — |
871
+ | `checks[].reasons` | Array<string> | No | No | — |
872
+ | `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. |
873
+ | `ruleset_version` | string | No | Yes | — |
874
+ | `evidence_sha256` | string | No | Yes | — |
875
+ | `reviewed_by` | object | No | Yes | — |
876
+ | `reviewed_by.id` | string | No | No | — |
877
+ | `reviewed_by.name` | string | No | No | — |
878
+ | `reviewed_at` | string (date-time) | No | Yes | — |
879
+ | `review_note` | string | No | Yes | — |
880
+ | `completed_at` | string (date-time) | No | Yes | — |
798
881
 
799
882
  ### SignerSelf
800
883
 
@@ -811,6 +894,8 @@ The current signer, as returned by `GET /v1/signers/self`. Extends Signer with t
811
894
  | `has_signature` | boolean | No | No | Whether the signer has a saved signature image stored. |
812
895
  | `has_initial` | boolean | No | No | Whether the signer has a saved initials image stored. |
813
896
  | `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. |
897
+ | `government_id` | string | No | Yes | Signer's normalized CPF (11 digits) or CNPJ (14 characters; may be alphanumeric). |
898
+ | `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
899
 
815
900
  ### SigningUrl
816
901
 
@@ -11,6 +11,7 @@ module Assinafy
11
11
  class AssignmentResource < BaseResource
12
12
  OPTIONAL_FIELDS = %i[message expires_at copy_receivers].freeze
13
13
  METHODS = %w[virtual collect].freeze
14
+ DIGITAL_CERTIFICATE_REQUIRES_VIRTUAL = 'digital_certificate_requires_virtual'
14
15
 
15
16
  # How a signer's identity is verified before they may sign, set per signer
16
17
  # on the assignment. When omitted it is inferred from
@@ -24,12 +25,15 @@ module Assinafy
24
25
  # certificate (A1 or A3), producing a qualified PAdES signature.
25
26
  # Requires the Digital Certificate feature, a CPF or CNPJ in the
26
27
  # signer's `government_id`, and that the signer is alone in its signing
27
- # step. Charged 0.5 credits per signer on top of its notification
28
+ # step on a virtual assignment. Charged 0.5 credits per signer on top of its notification
28
29
  # (breakdown code `SignatureDigitalCertificate`). A CPF requires that person's
29
30
  # certificate (an e-CPF, or an e-CNPJ naming them as legal
30
31
  # representative); a CNPJ requires an e-CNPJ for that company, from any
31
32
  # of its representatives.
32
- VERIFICATION_METHODS = %w[Email Whatsapp DigitalCertificate].freeze
33
+ # - `IdDocumentFaceMatchBasic` — government ID photos, face match and liveness.
34
+ # Requires its account feature and a valid CPF in `government_id`; priced per signer.
35
+ VERIFICATION_METHODS = %w[Email Whatsapp DigitalCertificate IdDocumentFaceMatchBasic].freeze
36
+ IDENTITY_REVIEW_IMAGES = %w[document_front document_back document_pdf face].freeze
33
37
 
34
38
  # Channels used to notify a signer of the request. Exactly one per signer;
35
39
  # WhatsApp incurs an additional cost and is available only on paid
@@ -121,6 +125,7 @@ module Assinafy
121
125
 
122
126
  result = { method: method }
123
127
  normalised = signers.map { |ref| normalise_signer_ref(ref, options) }
128
+ validate_certificate_method!(method, normalised)
124
129
  validate_signing_steps!(normalised)
125
130
  result[:signers] = normalised unless normalised.empty?
126
131
  OPTIONAL_FIELDS.each { |key| result[key] = p[key] if p[key] }
@@ -244,12 +249,23 @@ module Assinafy
244
249
  def validate_method_pair!(signer)
245
250
  verification = signer[:verification_method]
246
251
  notification = signer[:notification_methods]&.first
247
- return if verification.nil? || notification.nil? || verification == 'DigitalCertificate'
252
+ return if verification.nil? || notification.nil? ||
253
+ %w[DigitalCertificate IdDocumentFaceMatchBasic].include?(verification)
248
254
  return if verification == notification
249
255
 
250
256
  raise ValidationError.new('notification_methods must match the verification_method')
251
257
  end
252
258
 
259
+ def validate_certificate_method!(method, signers)
260
+ return if method == 'virtual'
261
+ return unless signers.any? { |signer| signer[:verification_method] == 'DigitalCertificate' }
262
+
263
+ raise ValidationError.new(
264
+ 'DigitalCertificate signers require method: virtual',
265
+ { code: DIGITAL_CERTIFICATE_REQUIRES_VIRTUAL, method: method }
266
+ )
267
+ end
268
+
253
269
  def validate_signing_steps!(signers)
254
270
  steps = signers.map { |signer| signer[:step] }
255
271
  contiguous = steps.compact.uniq.sort.each_with_index.all? { |step, index| step == index + 1 }
@@ -488,6 +504,95 @@ module Assinafy
488
504
  end
489
505
  end
490
506
 
507
+ # Read the account review of a signer's biometric identity verification.
508
+ # Evidence contains summaries; images are available only while InReview.
509
+ # The signer can sign during review, but certification awaits approval.
510
+ #
511
+ # @param document_id [String]
512
+ # @param assignment_id [String]
513
+ # @param signer_id [String]
514
+ # @return [Hash] SignerIdentityReview; see docs/API_REFERENCE.md#signeridentityreview for all fields
515
+ # @raise [Assinafy::ApiError] on failure, including 404 when no verification exists
516
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
517
+ # @raise [Assinafy::ValidationError] on invalid IDs
518
+ # @see GET /documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/identity-verification
519
+ # @example Read an open review (no request body; workspace credentials)
520
+ # client.assignments.identity_review('document-id', 'assignment-id', 'signer-id')
521
+ # # => { 'id' => 'verification-id', 'status' => 'InReview', 'provider' => 'AssinafyVerify',
522
+ # # 'verification_method' => 'IdDocumentFaceMatchBasic',
523
+ # # 'reason_codes' => [{ 'code' => 'NAME_MISMATCH', 'description' => 'Name differs' }],
524
+ # # 'evidence_available' => true, 'expected' => { 'name' => 'Example Signer', 'cpf' => nil },
525
+ # # 'document' => { 'type' => 'CNH', 'name' => 'EXAMPLE SIGNER', 'cpf' => nil },
526
+ # # 'matches' => { 'cpf' => true, 'name' => 0.52, 'birth_date' => nil },
527
+ # # 'checks' => [{ 'name' => 'name', 'result' => 'review', 'score' => 0.52,
528
+ # # 'reasons' => ['NAME_MISMATCH'] }], 'images' => ['document_front', 'face'],
529
+ # # 'ruleset_version' => '2026-10-12.2', 'evidence_sha256' => 'sha256-placeholder',
530
+ # # 'reviewed_by' => nil, 'reviewed_at' => nil, 'review_note' => nil, 'completed_at' => nil }
531
+ def identity_review(document_id, assignment_id, signer_id)
532
+ path = identity_review_path(document_id, assignment_id, signer_id)
533
+
534
+ call('Failed to fetch signer identity review') { http_get(path) }
535
+ end
536
+
537
+ # Download one evidence image or document PDF while a review is InReview.
538
+ # Fetched from the provider on each call; keep the sensitive bytes out of logs and caches.
539
+ #
540
+ # @param document_id [String]
541
+ # @param assignment_id [String]
542
+ # @param signer_id [String]
543
+ # @param name [String] document_front, document_back, document_pdf or face, as listed in the review
544
+ # @return [String] binary JPEG, PNG or PDF bytes
545
+ # @raise [Assinafy::ApiError] on failure; 404 after a decision or when evidence is unavailable
546
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
547
+ # @raise [Assinafy::ValidationError] on invalid IDs or image name
548
+ # @see GET /documents/{document}/assignments/{assignment}/signers/{signer}/identity-verification/images/{name}
549
+ # @example Read the face image (no request body; workspace credentials)
550
+ # client.assignments.identity_review_image('document-id', 'assignment-id', 'signer-id', 'face')
551
+ # # => "\x89PNG\r\n\x1A\n..." # binary String
552
+ def identity_review_image(document_id, assignment_id, signer_id, name)
553
+ path = identity_review_path(document_id, assignment_id, signer_id)
554
+ unless IDENTITY_REVIEW_IMAGES.include?(name)
555
+ raise ValidationError.new("Image name must be one of: #{IDENTITY_REVIEW_IMAGES.join(', ')}")
556
+ end
557
+
558
+ call_binary('Failed to download signer identity review image') { http_get("#{path}/images/#{name}") }
559
+ end
560
+
561
+ # Approve or reject an InReview identity verification. Rejection cancels the
562
+ # document and notifies every signer and copy receiver. The note is recorded
563
+ # in activities and sent to the provider; it is never shown to the signer.
564
+ #
565
+ # @param document_id [String]
566
+ # @param assignment_id [String]
567
+ # @param signer_id [String]
568
+ # @param decision [String] Approved or Rejected
569
+ # @param note [String, nil] optional review note, at most 2000 characters
570
+ # @return [Hash] SignerIdentityReview, the same fields as {#identity_review}, with the decision recorded
571
+ # @raise [Assinafy::ApiError] on failure; 400 unless InReview and awaiting signatures or identity review
572
+ # @raise [Assinafy::NetworkError] on transport or TLS failure
573
+ # @raise [Assinafy::ValidationError] on invalid IDs, decision or note
574
+ # @see POST /documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/identity-verification/review
575
+ # @example Approve a review with workspace credentials
576
+ # client.assignments.decide_identity_review('document-id', 'assignment-id', 'signer-id',
577
+ # decision: 'Approved', note: 'Identity checked')
578
+ # # Body: { 'decision' => 'Approved', 'note' => 'Identity checked' }
579
+ # # => the identity_review object with 'status' => 'Approved', 'images' => [],
580
+ # # 'reviewed_by' => { 'id' => 'user-id', 'name' => 'Example Reviewer' },
581
+ # # 'reviewed_at' => '2026-10-16T12:00:00Z', 'review_note' => 'Identity checked'
582
+ def decide_identity_review(document_id, assignment_id, signer_id, decision:, note: nil)
583
+ path = identity_review_path(document_id, assignment_id, signer_id)
584
+ unless %w[Approved Rejected].include?(decision)
585
+ raise ValidationError.new('Decision must be Approved or Rejected')
586
+ end
587
+ unless note.nil? || (note.is_a?(String) && note.length <= 2000)
588
+ raise ValidationError.new('Review note must be a String of at most 2000 characters')
589
+ end
590
+
591
+ call('Failed to decide signer identity review') do
592
+ http_post("#{path}/review", body_params(decision: decision, note: note))
593
+ end
594
+ end
595
+
491
596
  # Fetch the document a signer is being asked to sign (signer-access-code auth).
492
597
  #
493
598
  # @param signer_access_code [String]
@@ -626,6 +731,13 @@ module Assinafy
626
731
 
627
732
  private
628
733
 
734
+ def identity_review_path(document_id, assignment_id, signer_id)
735
+ doc_id = require_id(document_id, 'Document ID')
736
+ asg_id = require_id(assignment_id, 'Assignment ID')
737
+ sid = require_id(signer_id, 'Signer ID')
738
+ "documents/#{doc_id}/assignments/#{asg_id}/signers/#{sid}/identity-verification"
739
+ end
740
+
629
741
  def normalise_sign_item(item)
630
742
  normalised = require_payload(item, 'Assignment item').each_with_object({}) do |(key, value), result|
631
743
  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.1'
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
@@ -334,7 +336,9 @@ module Assinafy
334
336
  METHODS: Array[String]
335
337
  VERIFICATION_METHODS: Array[String]
336
338
  NOTIFICATION_METHODS: Array[String]
339
+ DIGITAL_CERTIFICATE_REQUIRES_VIRTUAL: String
337
340
  SIGN_ITEM_KEY_MAP: Hash[String, String]
341
+ IDENTITY_REVIEW_IMAGES: Array[String]
338
342
 
339
343
  def self.build_payload: (params payload, ?params options) -> Hash[String, untyped]
340
344
  def list: (?params params, ?String? account_id_override) -> list_result
@@ -343,6 +347,15 @@ module Assinafy
343
347
  def reset_expiration: (String document_id, String assignment_id, String? expires_at) -> api_object
344
348
  def resend_notification: (String document_id, String assignment_id, String signer_id) -> api_object
345
349
  def estimate_resend_cost: (String document_id, String assignment_id, String signer_id) -> api_object
350
+ def identity_review: (String document_id, String assignment_id, String signer_id) -> api_object
351
+ def identity_review_image: (String document_id, String assignment_id, String signer_id, String name) -> String
352
+ def decide_identity_review: (
353
+ String document_id,
354
+ String assignment_id,
355
+ String signer_id,
356
+ decision: String,
357
+ ?note: String?
358
+ ) -> api_object
346
359
  def signer_document: (signer_access_code: String, ?has_accepted_terms: bool?) -> api_object
347
360
  def sign: (String document_id, String assignment_id, Array[params] items, signer_access_code: String) -> api_object
348
361
  def decline: (
@@ -362,11 +375,12 @@ module Assinafy
362
375
  def list_event_types: () -> Array[api_object]
363
376
  def list_dispatches: (?params params, ?String? account_id_override) -> list_result
364
377
  def retry_dispatch: (String dispatch_id, ?String? account_id_override) -> api_object
365
- def list_endpoints: (?String? account_id_override) -> Array[api_object]
378
+ def list_endpoints: (?String? account_id_override, ?url: String?) -> Array[api_object]
366
379
  def create_endpoint: (params payload, ?String? account_id_override) -> api_object
367
380
  def get_endpoint: (String endpoint_id, ?String? account_id_override) -> api_object
368
381
  def update_endpoint: (String endpoint_id, params payload, ?String? account_id_override) -> api_object
369
382
  def delete_endpoint: (String endpoint_id, ?String? account_id_override) -> nil
383
+ def delete_endpoint_by_url: (String url, ?String? account_id_override) -> nil
370
384
  def endpoint_secret: (String endpoint_id, ?String? account_id_override) -> api_object
371
385
  def rotate_endpoint_secret: (String endpoint_id, ?String? account_id_override) -> api_object
372
386
  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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Assinafy SDK Contributors