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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.en.md +57 -9
- data/README.md +62 -3
- data/docs/API_REFERENCE.md +95 -10
- data/lib/assinafy/resources/assignment_resource.rb +115 -3
- data/lib/assinafy/resources/signer_resource.rb +55 -1
- data/lib/assinafy/resources/webhook_resource.rb +83 -55
- data/lib/assinafy/support/webhook_verifier.rb +1 -1
- data/lib/assinafy/version.rb +1 -1
- data/sig/assinafy.rbs +15 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83473f4fd075a7c665a16ee6ea6c38c64c548927930221d24ecd468cce1f4203
|
|
4
|
+
data.tar.gz: 57c81beb685fe09baf9de80eb36abc628d224952b2b08e582d19e24ecb402c16
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
397
|
-
|
|
398
|
-
|
|
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
|
|
436
|
-
[Assignments](#assignments)). Both require a non-empty `signers` array. Steps are
|
|
437
|
-
contiguous from 1; equal steps sign in parallel, and each step is notified when the
|
|
438
|
-
certificate signer is alone in its step. Deadlines are zoned ISO 8601 and at least one
|
|
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 =
|
|
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
|
|
534
|
-
passo**. Um
|
|
535
|
-
|
|
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_..."
|
data/docs/API_REFERENCE.md
CHANGED
|
@@ -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-
|
|
4
|
-
> `https://api.assinafy.com.br/v1/docs/openapi.json` (
|
|
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`),
|
|
306
|
-
signer is alone in its signing step.
|
|
307
|
-
|
|
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.
|
|
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
|
-
|
|
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? ||
|
|
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
|
|
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
|
|
53
|
-
# # is_active
|
|
54
|
-
# # url
|
|
55
|
-
# # email
|
|
56
|
-
# # updated_at
|
|
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
|
|
86
|
-
# # is_active
|
|
87
|
-
# # url
|
|
88
|
-
# # email
|
|
89
|
-
# # updated_at
|
|
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
|
|
115
|
-
# # is_active
|
|
116
|
-
# # url
|
|
117
|
-
# # email
|
|
118
|
-
# # updated_at
|
|
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: }
|
|
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
|
|
140
|
+
# # => unwrapped data payload returned:
|
|
141
141
|
# # [
|
|
142
|
-
# # { id
|
|
143
|
-
# # { id
|
|
144
|
-
# # { id
|
|
145
|
-
# # { id
|
|
146
|
-
# # { id
|
|
147
|
-
# # { id
|
|
148
|
-
# # { id
|
|
149
|
-
# # { id
|
|
150
|
-
# # # ... (
|
|
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
|
|
175
|
+
# # 'id' => "dispatch-id",
|
|
176
176
|
# # event: "signature_requested",
|
|
177
177
|
# # activity_id: 15431,
|
|
178
178
|
# # endpoint: "https://example.com/webhook",
|
|
179
|
-
# # payload: { id
|
|
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
|
|
185
|
-
# # updated_at
|
|
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
|
|
214
|
+
# # 'id' => "dispatch-id",
|
|
215
215
|
# # event: "signature_requested",
|
|
216
216
|
# # activity_id: 15431,
|
|
217
217
|
# # endpoint: "https://example.com/webhook",
|
|
218
|
-
# # payload: { id
|
|
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
|
|
224
|
-
# # updated_at
|
|
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
|
|
250
|
-
# # name
|
|
251
|
-
# # url
|
|
252
|
-
# # email
|
|
253
|
-
# # events
|
|
254
|
-
# # is_active
|
|
255
|
-
# # signing_enabled
|
|
256
|
-
# # created_at
|
|
257
|
-
# # updated_at
|
|
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
|
|
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
|
|
305
|
-
# # email
|
|
306
|
-
# # created_at
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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')
|
|
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)
|
data/lib/assinafy/version.rb
CHANGED
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
|