assinafy 2.0.1 → 2.1.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 +23 -0
- data/README.en.md +27 -4
- data/README.md +100 -1047
- data/docs/API_REFERENCE.md +154 -18
- data/lib/assinafy/client.rb +4 -0
- data/lib/assinafy/resources/account_resource.rb +2 -1
- data/lib/assinafy/resources/assignment_resource.rb +10 -11
- data/lib/assinafy/resources/base_resource.rb +3 -0
- data/lib/assinafy/resources/contract_resource.rb +450 -0
- data/lib/assinafy/resources/document_resource.rb +3 -0
- data/lib/assinafy/resources/field_resource.rb +2 -0
- data/lib/assinafy/resources/signer_resource.rb +3 -0
- data/lib/assinafy/resources/tag_resource.rb +5 -1
- data/lib/assinafy/resources/user_resource.rb +19 -7
- data/lib/assinafy/resources/webhook_resource.rb +35 -5
- data/lib/assinafy/version.rb +1 -1
- data/lib/assinafy.rb +1 -0
- data/sig/assinafy.rbs +18 -0
- metadata +4 -3
data/README.md
CHANGED
|
@@ -5,1123 +5,176 @@
|
|
|
5
5
|
[](https://github.com/assinafy/ruby-sdk/actions/workflows/ci.yml)
|
|
6
6
|
[](https://rubygems.org/gems/assinafy)
|
|
7
7
|
|
|
8
|
-
SDK Ruby para
|
|
9
|
-
|
|
8
|
+
SDK Ruby 3.2+ para todas as operações publicadas da [API Assinafy v1](https://api.assinafy.com.br/v1/docs).
|
|
9
|
+
O SDK é para servidores: nunca coloque chave de API, segredo OAuth ou código de signatário em aplicativo móvel,
|
|
10
|
+
navegador, log ou repositório. A referência completa, com payloads de cada operação, está em
|
|
11
|
+
[docs/API_REFERENCE.md](docs/API_REFERENCE.md); a lista detalhada por recurso está em
|
|
12
|
+
[README.en.md](README.en.md).
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
fatores, endpoints de webhook com assinatura nativa, e o ciclo de vida completo de templates. O
|
|
13
|
-
[`spec/api_coverage_spec.rb`](spec/api_coverage_spec.rb), versionado no repositório, valida que cada
|
|
14
|
-
rota mapeia de forma única para um método público do SDK — se uma operação deixar de ter cobertura,
|
|
15
|
-
a suíte falha.
|
|
16
|
-
|
|
17
|
-
Este documento acompanha uma integração do início ao fim, na ordem em que ela acontece. Para
|
|
18
|
-
consulta rápida por recurso, veja **[README.en.md](README.en.md)**; para consulta por operação,
|
|
19
|
-
[docs/API_REFERENCE.md](docs/API_REFERENCE.md).
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Sumário
|
|
24
|
-
|
|
25
|
-
1. [Instalação, configuração e ambientes](#1-instalação-configuração-e-ambientes)
|
|
26
|
-
2. [Autenticação](#2-autenticação)
|
|
27
|
-
3. [Conexão OAuth 2.1 (apps de marketplace)](#3-conexão-oauth-21-apps-de-marketplace)
|
|
28
|
-
4. [Enviar o documento e esperar os metadados](#4-enviar-o-documento-e-esperar-os-metadados)
|
|
29
|
-
5. [Signatários](#5-signatários)
|
|
30
|
-
6. [Métodos de verificação e notificação](#6-métodos-de-verificação-e-notificação)
|
|
31
|
-
7. [Estimar o custo](#7-estimar-o-custo)
|
|
32
|
-
8. [Abrir o assignment](#8-abrir-o-assignment)
|
|
33
|
-
9. [A experiência do signatário](#9-a-experiência-do-signatário)
|
|
34
|
-
10. [Webhooks: endpoints e verificação de assinatura](#10-webhooks-endpoints-e-verificação-de-assinatura)
|
|
35
|
-
11. [Andamento, download e verificação](#11-andamento-download-e-verificação)
|
|
36
|
-
12. [Organizar e limpar](#12-organizar-e-limpar)
|
|
37
|
-
13. [Erros](#13-erros)
|
|
38
|
-
14. [Paginação](#14-paginação)
|
|
39
|
-
15. [Recursos do cliente](#15-recursos-do-cliente)
|
|
40
|
-
16. [Assinaturas RBS](#16-assinaturas-rbs)
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## 1. Instalação, configuração e ambientes
|
|
45
|
-
|
|
46
|
-
- Ruby 3.2+ (suporte mantido: 3.3+; 3.2 é compatibilidade legada/EOL)
|
|
47
|
-
- Bundler
|
|
48
|
-
- TLS 1.2 ou superior (o SDK recusa TLS 1.0 e 1.1)
|
|
49
|
-
|
|
50
|
-
Do RubyGems.org:
|
|
14
|
+
## Instalação e cliente
|
|
51
15
|
|
|
52
16
|
```ruby
|
|
53
17
|
# Gemfile
|
|
54
18
|
gem 'assinafy'
|
|
55
19
|
```
|
|
56
20
|
|
|
57
|
-
```bash
|
|
58
|
-
bundle install
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Do GitHub Packages (mirror), com um personal access token de escopo `read:packages`:
|
|
62
|
-
|
|
63
|
-
```ruby
|
|
64
|
-
source 'https://rubygems.pkg.github.com/assinafy' do
|
|
65
|
-
gem 'assinafy'
|
|
66
|
-
end
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
bundle config https://rubygems.pkg.github.com/assinafy USUARIO:TOKEN
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Ambientes
|
|
74
|
-
|
|
75
|
-
| Ambiente | `base_url` | Servidor de autorização OAuth |
|
|
76
|
-
| --- | --- | --- |
|
|
77
|
-
| Produção | `https://api.assinafy.com.br/v1` (padrão) | `https://auth.assinafy.com.br` |
|
|
78
|
-
| Sandbox | `https://sandbox.assinafy.com.br/v1` | `https://auth-sandbox.assinafy.com.br` |
|
|
79
|
-
|
|
80
|
-
O sandbox é gratuito: use-o para testar a integração antes de ir para produção, trocando a
|
|
81
|
-
`base_url` e, no OAuth, o servidor de autorização. Nunca envie uma chave de API do sandbox para a
|
|
82
|
-
produção, nem o contrário. Os endpoints de webhook e a autenticação em dois fatores estão
|
|
83
|
-
disponíveis em produção (`api.assinafy.com.br`).
|
|
84
|
-
|
|
85
|
-
### Configuração
|
|
86
|
-
|
|
87
21
|
```ruby
|
|
88
22
|
require 'assinafy'
|
|
89
|
-
require 'logger'
|
|
90
23
|
|
|
91
24
|
client = Assinafy::Client.new(
|
|
92
|
-
api_key:
|
|
93
|
-
account_id:
|
|
94
|
-
base_url:
|
|
95
|
-
|
|
96
|
-
timeout: 30,
|
|
97
|
-
logger: Logger.new($stdout)
|
|
98
|
-
)
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
- `base_url:` precisa ser uma URL `http`/`https` absoluta. Qualquer outra coisa — um host sem
|
|
102
|
-
esquema, um caminho relativo, outro esquema — levanta `Assinafy::ValidationError` em vez de
|
|
103
|
-
anexar suas credenciais a ela. A barra final é removida. `Configuration#base_url=` e
|
|
104
|
-
`#timeout=` validam da mesma forma que o construtor.
|
|
105
|
-
- Métodos com escopo de conta aceitam uma sobrescrita de `account_id` por chamada, para tenants com
|
|
106
|
-
múltiplas workspaces.
|
|
107
|
-
- O `logger:` recebe mensagens de ciclo de vida do SDK, nunca corpos de requisição ou credenciais.
|
|
108
|
-
- Toda requisição envia `User-Agent: Assinafy-Ruby-SDK/v<Assinafy::VERSION>`.
|
|
109
|
-
- `Client.from_config(hash)` aceita hashes com chaves string ou símbolo (por exemplo, YAML já
|
|
110
|
-
interpretado).
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## 2. Autenticação
|
|
115
|
-
|
|
116
|
-
A Assinafy aceita três credenciais. Escolha pela pergunta "**quem** está agindo?".
|
|
117
|
-
|
|
118
|
-
| Credencial | Quem age | Quando usar |
|
|
119
|
-
| --- | --- | --- |
|
|
120
|
-
| **Chave de API** (`api_key:`) | a própria workspace | integrações de back-end. Permanente. Enviada como `X-Api-Key`. |
|
|
121
|
-
| **Token de sessão** (`token:`) | o usuário que fez login | depois de `client.auth.login`. JWT, expira em ~1 hora. |
|
|
122
|
-
| **OAuth 2.1** (`token:`) | um aplicativo, **em nome de** um usuário | apps de marketplace, integrações de terceiros, assistentes de IA. Veja a [seção 3](#3-conexão-oauth-21-apps-de-marketplace). |
|
|
123
|
-
|
|
124
|
-
Configure **exatamente uma** credencial por cliente; se `api_key:` e `token:` forem informados, o
|
|
125
|
-
SDK envia apenas `X-Api-Key`. Um cliente **sem credenciais** serve para login, OAuth e endpoints
|
|
126
|
-
públicos ou de signatário — o SDK remove `X-Api-Key`/`Authorization` dessas chamadas de qualquer
|
|
127
|
-
forma.
|
|
128
|
-
|
|
129
|
-
### Login com e-mail e senha
|
|
130
|
-
|
|
131
|
-
```ruby
|
|
132
|
-
publico = Assinafy::Client.new
|
|
133
|
-
sessao = publico.auth.login(email: 'usuario@example.com', password: ENV.fetch('ASSINAFY_PASSWORD'))
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Quando o usuário tem **autenticação em dois fatores** ativa, o login não devolve `access_token`:
|
|
137
|
-
devolve um desafio com `mfa_token`. Troque-o pelo código do aplicativo autenticador, ou por um
|
|
138
|
-
código de recuperação:
|
|
139
|
-
|
|
140
|
-
```ruby
|
|
141
|
-
if sessao['mfa_token']
|
|
142
|
-
sessao = publico.auth.verify_mfa(
|
|
143
|
-
mfa_token: sessao['mfa_token'],
|
|
144
|
-
code: '123456' # ou um código de recuperação, como "ABCD-EFGH-JKMN"
|
|
145
|
-
)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
usuario = Assinafy::Client.new(
|
|
149
|
-
token: sessao.fetch('access_token'),
|
|
150
|
-
account_id: sessao.fetch('accounts').first.fetch('id')
|
|
25
|
+
api_key: ENV.fetch('ASSINAFY_API_KEY'),
|
|
26
|
+
account_id: ENV.fetch('ASSINAFY_ACCOUNT_ID'),
|
|
27
|
+
base_url: ENV.fetch('ASSINAFY_BASE_URL', 'https://api.assinafy.com.br/v1'),
|
|
28
|
+
timeout: 30
|
|
151
29
|
)
|
|
152
30
|
```
|
|
153
31
|
|
|
154
|
-
`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
(`client.auth.social_login`) devolve o mesmo formato.
|
|
32
|
+
Use `https://sandbox.assinafy.com.br/v1` no sandbox. O SDK exige URL absoluta, usa TLS 1.2+ e aceita exatamente
|
|
33
|
+
uma credencial por cliente: `api_key:` para a workspace ou `token:` para sessão/OAuth. Endpoints públicos e de
|
|
34
|
+
signatário removem essas credenciais e enviam apenas o código próprio da operação.
|
|
158
35
|
|
|
159
|
-
|
|
36
|
+
## OAuth 2.1 para marketplace
|
|
160
37
|
|
|
161
|
-
|
|
38
|
+
O fluxo é authorization code com PKCE `S256`. Registre um callback HTTPS e, para cada tentativa, guarde o
|
|
39
|
+
`code_verifier`, `state` e emissor esperado no servidor.
|
|
162
40
|
|
|
163
41
|
```ruby
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
inscricao['secret'] # exibido só nesta resposta
|
|
167
|
-
|
|
168
|
-
codigos = usuario.auth.confirm_totp_enrollment(method_id: inscricao.fetch('id'), code: '123456')
|
|
169
|
-
codigos['recovery_codes'] # => ["ABCD-EFGH-JKMN", ...] — exibidos uma única vez
|
|
42
|
+
verifier = Assinafy::OAuth.generate_code_verifier
|
|
43
|
+
state = Assinafy::OAuth.generate_state
|
|
170
44
|
|
|
171
|
-
|
|
172
|
-
# => { 'methods' => [{ 'id' => 'mfa-method-id', 'type' => 'Totp', 'label' => 'Meu celular', ... }],
|
|
173
|
-
# 'recovery_codes_remaining' => 10 }
|
|
174
|
-
|
|
175
|
-
usuario.auth.regenerate_recovery_codes(password: ENV.fetch('ASSINAFY_PASSWORD'))
|
|
176
|
-
usuario.auth.delete_mfa_method('mfa-method-id', code: '123456') # => { 'is_mfa_enabled' => false }
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
- A autenticação em dois fatores só fica ativa depois de `confirm_totp_enrollment`.
|
|
180
|
-
- Confirmar um novo autenticador quando já existe um **substitui** o anterior e exige
|
|
181
|
-
reautenticação: `password:` ou `reauth_code:` (código do dispositivo atual ou de recuperação).
|
|
182
|
-
- `regenerate_recovery_codes` e `delete_mfa_method` exigem `password:` ou `code:`; sem nenhum dos
|
|
183
|
-
dois, o SDK levanta `ValidationError` antes de enviar. Um código de recuperação usado como prova é
|
|
184
|
-
consumido. Remover o último método também descarta os códigos de recuperação.
|
|
185
|
-
|
|
186
|
-
### Gerenciar a chave de API
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
usuario.auth.create_api_key(password: ENV.fetch('ASSINAFY_PASSWORD')) # => { 'api_key' => '...' } — exibida uma única vez
|
|
190
|
-
usuario.auth.get_api_key # => { 'api_key' => '****...' } — mascarada
|
|
191
|
-
usuario.auth.delete_api_key # => nil
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Gerar uma chave nova **invalida a anterior**. Nunca exponha a chave em um front-end.
|
|
195
|
-
|
|
196
|
-
---
|
|
197
|
-
|
|
198
|
-
## 3. Conexão OAuth 2.1 (apps de marketplace)
|
|
199
|
-
|
|
200
|
-
Use OAuth quando um aplicativo age **em nome de um usuário**, com a permissão dele. Diferente da
|
|
201
|
-
chave de API, o token vale para **uma** workspace e carrega apenas os escopos que o usuário aprovou.
|
|
202
|
-
O fluxo é *authorization code* com **PKCE obrigatório** (o servidor aceita apenas `S256`).
|
|
203
|
-
|
|
204
|
-
### Escopos
|
|
205
|
-
|
|
206
|
-
| Escopo | Concede |
|
|
207
|
-
| --- | --- |
|
|
208
|
-
| `documents:read` | ler documentos, páginas, tags, signatários, assignments, atividades e entregas de webhook |
|
|
209
|
-
| `documents:write` | criar, alterar e excluir documentos e gerenciar seus signatários e assignments |
|
|
210
|
-
| `templates:read` | ler templates, páginas, papéis, campos e tags |
|
|
211
|
-
| `templates:write` | criar, alterar e excluir templates |
|
|
212
|
-
| `account:read` | ler perfil, tema, logo e endpoints de webhook da workspace |
|
|
213
|
-
| `webhooks:write` | criar, alterar, desativar e excluir endpoints de webhook da workspace |
|
|
214
|
-
| `openid` | identificar o usuário (claim `sub`) e habilitar `/oauth/userinfo` |
|
|
215
|
-
| `profile` | incluir o nome do usuário nas claims |
|
|
216
|
-
| `email` | incluir o e-mail e seu status de verificação nas claims |
|
|
217
|
-
| `offline_access` | receber um *refresh token* (nunca aparece no `scope` devolvido) |
|
|
218
|
-
|
|
219
|
-
Um token OAuth **nunca** alcança faturamento, ciclo de vida da conta, gerenciamento de credenciais,
|
|
220
|
-
segredos de assinatura de webhook ou superfícies administrativas — independentemente do escopo.
|
|
221
|
-
|
|
222
|
-
### Passo 1 — registrar o callback e redirecionar o usuário
|
|
223
|
-
|
|
224
|
-
Registre no aplicativo Assinafy uma URL de callback HTTPS. Para cada tentativa de autorização, gere
|
|
225
|
-
e guarde o verificador PKCE, o `state` e o emissor esperado:
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
verificador = Assinafy::OAuth.generate_code_verifier
|
|
229
|
-
state = Assinafy::OAuth.generate_state
|
|
230
|
-
|
|
231
|
-
session[:assinafy_code_verifier] = verificador
|
|
232
|
-
session[:assinafy_state] = state
|
|
233
|
-
session[:assinafy_issuer] = Assinafy::OAuth::AUTHORIZATION_SERVER # emissor desta tentativa
|
|
234
|
-
|
|
235
|
-
redirect_to Assinafy::OAuth.authorization_url(
|
|
45
|
+
url = Assinafy::OAuth.authorization_url(
|
|
236
46
|
client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
|
|
237
47
|
redirect_uri: 'https://app.example.com/oauth/callback',
|
|
238
|
-
code_verifier:
|
|
48
|
+
code_verifier: verifier,
|
|
239
49
|
state: state,
|
|
240
50
|
scope: %w[documents:read documents:write offline_access]
|
|
241
51
|
)
|
|
242
52
|
```
|
|
243
53
|
|
|
244
|
-
|
|
245
|
-
`
|
|
246
|
-
`authorization_endpoint: 'https://auth-sandbox.assinafy.com.br/oauth/authorize'` e guarde
|
|
247
|
-
`https://auth-sandbox.assinafy.com.br` como emissor.
|
|
248
|
-
|
|
249
|
-
### Passo 2 — validar o callback e trocar o código
|
|
54
|
+
No callback, valide `state` e `iss` antes de ler `code` ou `error`; o emissor de produção é
|
|
55
|
+
`https://auth.assinafy.com.br`. Troque o código uma única vez, em até 60 segundos:
|
|
250
56
|
|
|
251
57
|
```ruby
|
|
252
|
-
unless params[:state] == session.delete(:assinafy_state) &&
|
|
253
|
-
params[:iss] == session.delete(:assinafy_issuer)
|
|
254
|
-
raise 'resposta de autorização inválida'
|
|
255
|
-
end
|
|
256
|
-
raise "autorização não concedida: #{params[:error]}" if params[:error] # access_denied, invalid_scope, ...
|
|
257
|
-
|
|
258
58
|
tokens = Assinafy::Client.new.oauth.exchange_code(
|
|
259
59
|
code: params.fetch(:code),
|
|
260
60
|
client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
|
|
261
|
-
code_verifier:
|
|
61
|
+
code_verifier: verifier,
|
|
262
62
|
redirect_uri: 'https://app.example.com/oauth/callback'
|
|
263
63
|
)
|
|
264
64
|
|
|
265
|
-
tokens
|
|
266
|
-
tokens['expires_in'] # => 3600
|
|
267
|
-
tokens['refresh_token'] # => presente apenas com offline_access
|
|
268
|
-
tokens['scope'] # => "documents:read documents:write"
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
Confira `state` e `iss` contra os valores guardados nesta tentativa **antes de qualquer outra
|
|
272
|
-
coisa**, inclusive num retorno com `error=` — é a proteção contra CSRF e contra respostas que não
|
|
273
|
-
são suas. O código vale uma vez e expira 60 segundos depois da aprovação: troque-o na hora, sem
|
|
274
|
-
repetir. O SDK confere localmente o formato do `code_verifier`, porque o servidor reporta um
|
|
275
|
-
verificador malformado como `invalid_grant`, indistinguível de um código expirado.
|
|
276
|
-
|
|
277
|
-
### Passo 3 — persistir a conexão e agir como o usuário
|
|
278
|
-
|
|
279
|
-
```ruby
|
|
280
|
-
access_token = tokens.fetch('access_token')
|
|
281
|
-
|
|
282
|
-
# O token só vale na workspace que o usuário escolheu.
|
|
283
|
-
workspace_id = Assinafy::Client.new(token: access_token).accounts.list[:data].first.fetch('id')
|
|
284
|
-
|
|
285
|
-
conexao = Conexao.create!(
|
|
286
|
-
workspace_id: workspace_id,
|
|
287
|
-
scope: tokens.fetch('scope'),
|
|
288
|
-
access_token: access_token, # guarde criptografado
|
|
289
|
-
refresh_token: tokens['refresh_token'], # guarde criptografado
|
|
290
|
-
expires_at: Time.now + tokens.fetch('expires_in')
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
usuario = Assinafy::Client.new(token: conexao.access_token, account_id: conexao.workspace_id)
|
|
294
|
-
usuario.documents.list
|
|
295
|
-
usuario.oauth.userinfo # => { 'sub' => ..., 'name' => ..., 'email' => ... } (escopo openid)
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### Renovar com rotação, uma vez por conexão
|
|
299
|
-
|
|
300
|
-
Cada renovação devolve um refresh token **novo**, válido por mais 30 dias, e aposenta o anterior: a
|
|
301
|
-
conexão só expira se passar 30 dias sem renovar. Reutilizar um refresh token aposentado encerra a
|
|
302
|
-
conexão inteira. Por isso, renove sob um lock por conexão e guarde os tokens novos atomicamente,
|
|
303
|
-
antes de usá-los:
|
|
304
|
-
|
|
305
|
-
```ruby
|
|
306
|
-
conexao.with_lock do # lock de linha: uma renovação por vez por conexão
|
|
307
|
-
next if conexao.expires_at > Time.now + 60 # outro processo já renovou
|
|
308
|
-
|
|
309
|
-
novos = Assinafy::Client.new.oauth.refresh(
|
|
310
|
-
refresh_token: conexao.refresh_token,
|
|
311
|
-
client_id: ENV.fetch('ASSINAFY_CLIENT_ID')
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
conexao.update!(
|
|
315
|
-
refresh_token: novos.fetch('refresh_token'),
|
|
316
|
-
access_token: novos.fetch('access_token'),
|
|
317
|
-
expires_at: Time.now + novos.fetch('expires_in')
|
|
318
|
-
)
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
usuario = Assinafy::Client.new(token: conexao.access_token, account_id: conexao.workspace_id)
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
> O SDK **não renova o token automaticamente** e envia cada pedido de token uma única vez — nunca
|
|
325
|
-
> adicione middleware de retry à conexão. `refresh` levanta `Assinafy::Error` em vez de devolver um
|
|
326
|
-
> sucesso sem refresh token novo; trate como `invalid_grant`.
|
|
327
|
-
>
|
|
328
|
-
> Se a renovação falhar sem resposta clara — timeout, conexão interrompida, `5xx` —, o servidor
|
|
329
|
-
> pode ter trocado o token sem que a resposta chegasse. Releia o refresh token guardado: se ainda
|
|
330
|
-
> for o que você enviou, **nunca o reenvie**; peça ao usuário para conectar de novo. Siga em frente
|
|
331
|
-
> só se outro processo já tiver guardado um token diferente. Só é seguro repetir uma falha que
|
|
332
|
-
> comprovadamente aconteceu antes do envio: DNS, conexão recusada, handshake TLS. Num `401` da API,
|
|
333
|
-
> renove uma vez; se falhar, ou em `invalid_grant`, peça ao usuário para conectar de novo.
|
|
334
|
-
|
|
335
|
-
Sem `offline_access` não há refresh token: quando o access token expirar, mande o usuário pelo
|
|
336
|
-
fluxo de autorização de novo.
|
|
337
|
-
|
|
338
|
-
### Revogar ao desconectar
|
|
339
|
-
|
|
340
|
-
Revogue o refresh token guardado **agora** — o mais recente — e só depois apague os tokens:
|
|
341
|
-
|
|
342
|
-
```ruby
|
|
343
|
-
Assinafy::Client.new.oauth.revoke(
|
|
344
|
-
token: conexao.reload.refresh_token,
|
|
345
|
-
client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
|
|
346
|
-
token_type_hint: 'refresh_token'
|
|
347
|
-
)
|
|
348
|
-
conexao.destroy!
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
Revogar um refresh token invalida também os access tokens emitidos a partir dele. A revogação
|
|
352
|
-
responde `200` inclusive para um token já aposentado, então revogar uma cópia antiga parece dar
|
|
353
|
-
certo enquanto a conexão continua ativa.
|
|
354
|
-
|
|
355
|
-
### Descoberta
|
|
356
|
-
|
|
357
|
-
```ruby
|
|
358
|
-
recurso = client.oauth.protected_resource_metadata
|
|
359
|
-
recurso['authorization_servers'] # => ["https://auth.assinafy.com.br"]
|
|
360
|
-
|
|
361
|
-
servidor = client.oauth.authorization_server_metadata
|
|
362
|
-
servidor['token_endpoint'] # => "https://api.assinafy.com.br/v1/oauth/token"
|
|
363
|
-
servidor['code_challenge_methods_supported'] # => ["S256"]
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
`authorization_server_metadata` acessa outro host e é enviado sem credenciais de workspace. Ele
|
|
367
|
-
aceita uma URL alternativa, desde que seja HTTPS absoluta; qualquer outra levanta
|
|
368
|
-
`ValidationError`.
|
|
369
|
-
|
|
370
|
-
### Clientes internos de serviço
|
|
371
|
-
|
|
372
|
-
O servidor também publica o grant RFC 8693 `urn:ietf:params:oauth:grant-type:token-exchange`,
|
|
373
|
-
reservado a clientes confidenciais internos provisionados pela Assinafy; apps de marketplace usam
|
|
374
|
-
autorização com PKCE e renovação. `client.oauth.token` aceita esse grant, com `client_secret`,
|
|
375
|
-
`subject_token`, `subject_token_type` e `resource`. Ele não emite refresh token.
|
|
376
|
-
|
|
377
|
-
### Erros OAuth
|
|
378
|
-
|
|
379
|
-
Os endpoints OAuth respondem com o objeto plano da RFC 6749, não com o envelope da API. O SDK
|
|
380
|
-
levanta `Assinafy::OAuthError` (subclasse de `Assinafy::ApiError`):
|
|
381
|
-
|
|
382
|
-
```ruby
|
|
383
|
-
begin
|
|
384
|
-
client.oauth.exchange_code(...)
|
|
385
|
-
rescue Assinafy::OAuthError => e
|
|
386
|
-
e.error # => "invalid_grant"
|
|
387
|
-
e.error_description # => "The authorization code is invalid or has expired."
|
|
388
|
-
e.status_code # => 400
|
|
389
|
-
end
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
Num `403` de qualquer recurso, `e.context[:www_authenticate]` traz o desafio que **nomeia o escopo
|
|
393
|
-
faltante**: peça ao usuário para conectar de novo incluindo esse escopo, sem repetir a chamada. Um
|
|
394
|
-
`403` sem esse desafio indica outra workspace, o papel do usuário ou uma área que tokens OAuth não
|
|
395
|
-
alcançam.
|
|
396
|
-
|
|
397
|
-
---
|
|
398
|
-
|
|
399
|
-
## 4. Enviar o documento e esperar os metadados
|
|
400
|
-
|
|
401
|
-
### Upload
|
|
402
|
-
|
|
403
|
-
```ruby
|
|
404
|
-
documento = client.documents.upload('./contrato-acme.pdf')
|
|
405
|
-
documento['id'] # => "document-id"
|
|
406
|
-
documento['name'] # => "contrato-acme.pdf"
|
|
407
|
-
documento['status'] # => "uploaded"
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
Aceita um caminho, um Hash com `:file_path`, ou `:buffer` + `:file_name` para bytes em memória:
|
|
411
|
-
|
|
412
|
-
```ruby
|
|
413
|
-
client.documents.upload(buffer: pdf_bytes, file_name: 'contrato-acme.pdf')
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
O documento recebe o nome do arquivo enviado; para mudar, use
|
|
417
|
-
`client.documents.rename(documento['id'], 'Contrato Acme')`. Somente PDF, no máximo 25 MB: o SDK
|
|
418
|
-
confere a extensão, o tamanho e o cabeçalho `%PDF-` antes de enviar.
|
|
419
|
-
|
|
420
|
-
### Esperar o processamento
|
|
421
|
-
|
|
422
|
-
A Assinafy extrai páginas e metadados de forma assíncrona. Só é possível abrir um assignment
|
|
423
|
-
depois disso:
|
|
424
|
-
|
|
425
|
-
```ruby
|
|
426
|
-
documento = client.documents.wait_until_ready(
|
|
427
|
-
documento['id'],
|
|
428
|
-
max_wait_seconds: 30,
|
|
429
|
-
poll_interval_seconds: 2
|
|
430
|
-
)
|
|
431
|
-
documento['status'] # => "metadata_ready"
|
|
432
|
-
documento['pages'] # => [{ 'id' => 'page-id', 'number' => 1, 'height' => 1651, 'width' => 1275 }]
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
Erros de rede durante a espera são tolerados; um status terminal (`failed`, `expired`,
|
|
436
|
-
`rejected_by_*`) ou o fim do prazo interrompe com `Assinafy::Error`.
|
|
437
|
-
|
|
438
|
-
### Alternativa: gerar a partir de um template
|
|
439
|
-
|
|
440
|
-
Um template é um PDF reutilizável com papéis e campos definidos. Configure os papéis e campos no
|
|
441
|
-
aplicativo Assinafy: o upload cria só um papel `Editor`, e a geração exige pelo menos um papel
|
|
442
|
-
`Signer`.
|
|
443
|
-
|
|
444
|
-
```ruby
|
|
445
|
-
template = client.templates.create('./contrato-modelo.pdf', name: 'Contrato padrão') # até 25 MB
|
|
446
|
-
client.templates.list
|
|
447
|
-
client.templates.get(template['id'])
|
|
448
|
-
client.templates.update(template['id'], name: 'Contrato padrão v2')
|
|
449
|
-
client.templates.download_page(template['id'], 'page-id') # bytes da imagem da página
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
Para gerar, informe uma entrada por papel, cada uma com `role_id` (de `template['roles']`) e o ID de
|
|
453
|
-
um signatário já existente e diferente para cada papel. A geração cria o documento e o assignment
|
|
454
|
-
juntos:
|
|
455
|
-
|
|
456
|
-
```ruby
|
|
457
|
-
papel = template.fetch('roles').find { |r| r['assignment_type'] == 'Signer' }
|
|
458
|
-
papeis = [{ role_id: papel.fetch('id'), id: signatario['id'],
|
|
459
|
-
verification_method: 'Email', notification_methods: ['Email'] }]
|
|
460
|
-
|
|
461
|
-
client.documents.estimate_cost_from_template(template['id'], papeis)
|
|
462
|
-
documento = client.documents.create_from_template(
|
|
463
|
-
template['id'], papeis,
|
|
464
|
-
name: 'contrato-acme.pdf', message: 'Por favor, assine.', expires_at: '2099-12-31T23:59:00-03:00'
|
|
465
|
-
)
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
### Campos
|
|
469
|
-
|
|
470
|
-
Definições de campo reutilizáveis, usadas em assignments `collect` e templates:
|
|
471
|
-
|
|
472
|
-
```ruby
|
|
473
|
-
campo = client.fields.create(name: 'CPF', type: 'text', regex: '/^\d{11}$/')
|
|
474
|
-
client.fields.types # tipos de campo disponíveis
|
|
475
|
-
client.fields.list
|
|
476
|
-
client.fields.validate(campo['id'], '12345678901')
|
|
477
|
-
client.fields.validate_multiple([{ field_id: campo['id'], value: '12345678901' }])
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
---
|
|
481
|
-
|
|
482
|
-
## 5. Signatários
|
|
483
|
-
|
|
484
|
-
Signatários pertencem à conta e podem ser reaproveitados entre documentos:
|
|
485
|
-
|
|
486
|
-
```ruby
|
|
487
|
-
signatario = client.signers.find_by_email('ana@example.com') || # paginação percorrida pelo SDK
|
|
488
|
-
client.signers.create(full_name: 'Ana Silva', email: 'ana@example.com')
|
|
489
|
-
signatario['id'] # => "signer-id"
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
Para notificar por WhatsApp, informe `whatsapp_phone_number` (ou o alias `phone:`) em E.164. Para
|
|
493
|
-
**certificado digital**, informe o CPF ou CNPJ em `government_id` já na criação:
|
|
494
|
-
|
|
495
|
-
```ruby
|
|
496
|
-
certificado = client.signers.create(
|
|
497
|
-
full_name: 'Ana Silva',
|
|
498
|
-
email: 'ana@example.com',
|
|
499
|
-
government_id: ENV.fetch('ASSINAFY_SIGNER_GOVERNMENT_ID') # CPF/CNPJ real autorizado
|
|
500
|
-
)
|
|
501
|
-
|
|
502
|
-
# Ou num signatário existente:
|
|
503
|
-
client.signers.update(signatario['id'], government_id: ENV.fetch('ASSINAFY_SIGNER_GOVERNMENT_ID'))
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
`client.signers.validate_create!(payload)` valida e normaliza o corpo sem fazer requisição.
|
|
507
|
-
|
|
508
|
-
---
|
|
509
|
-
|
|
510
|
-
## 6. Métodos de verificação e notificação
|
|
511
|
-
|
|
512
|
-
Cada signatário do assignment tem um **método de verificação** (como prova a identidade antes de
|
|
513
|
-
assinar) e um **método de notificação** (como recebe o convite). Verificação e notificação são
|
|
514
|
-
**acopladas**: envie um, os dois, ou nenhum — o lado que faltar é inferido do outro. Sem nenhum dos
|
|
515
|
-
dois, ambos assumem `Email`.
|
|
516
|
-
|
|
517
|
-
| Verificação | Como funciona | Notificação permitida | Custo por signatário |
|
|
518
|
-
| --- | --- | --- | --- |
|
|
519
|
-
| `Email` *(padrão)* | código de uso único (OTP) por e-mail, exigido antes de assinar | `Email` | 0 crédito |
|
|
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
|
-
| `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 |
|
|
523
|
-
|
|
524
|
-
- Informe **exatamente um** canal em `notification_methods` (um array de um elemento).
|
|
525
|
-
- O custo do certificado digital aparece na estimativa com o código `SignatureDigitalCertificate`.
|
|
526
|
-
Reenviar uma notificação cobra a notificação de novo.
|
|
527
|
-
- Os valores aceitos estão em `Assinafy::Resources::AssignmentResource::VERIFICATION_METHODS` e
|
|
528
|
-
`::NOTIFICATION_METHODS`. O SDK valida enum e combinação localmente: um valor fora do enum, ou um
|
|
529
|
-
par inválido, levanta `ValidationError` **antes** de a requisição sair.
|
|
530
|
-
|
|
531
|
-
### Certificado digital ICP-Brasil (A1/A3)
|
|
532
|
-
|
|
533
|
-
Exige o recurso **Certificado Digital** na conta (planos Standard e Pro), CPF ou CNPJ em
|
|
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.
|
|
540
|
-
|
|
541
|
-
---
|
|
542
|
-
|
|
543
|
-
## 7. Estimar o custo
|
|
544
|
-
|
|
545
|
-
Antes de enviar — principalmente com WhatsApp ou certificado digital:
|
|
546
|
-
|
|
547
|
-
```ruby
|
|
548
|
-
estimativa = client.assignments.estimate_cost(
|
|
549
|
-
documento['id'],
|
|
550
|
-
signers: [
|
|
551
|
-
{ verification_method: 'Email' },
|
|
552
|
-
{ verification_method: 'DigitalCertificate', notification_methods: ['Whatsapp'] }
|
|
553
|
-
]
|
|
554
|
-
)
|
|
555
|
-
estimativa['total_credits'] # => 0.95
|
|
556
|
-
estimativa['breakdown'] # itens com code, quantity, unit_cost, cost
|
|
557
|
-
estimativa['has_sufficient_resources'] # => true
|
|
558
|
-
estimativa['blocking_reason'] # => nil, ou "InsufficientCredits", ...
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
Na estimativa, os signatários podem ser descritos só pelo método, sem `id`. Para um template, use
|
|
562
|
-
`client.documents.estimate_cost_from_template`; para um reenvio,
|
|
563
|
-
`client.assignments.estimate_resend_cost`.
|
|
564
|
-
|
|
565
|
-
---
|
|
566
|
-
|
|
567
|
-
## 8. Abrir o assignment
|
|
568
|
-
|
|
569
|
-
Um assignment é o convite para assinar um documento. Todo assignment exige ao menos um signatário.
|
|
570
|
-
|
|
571
|
-
**`virtual`** — sem campos posicionados; o signatário aceita o documento inteiro.
|
|
572
|
-
|
|
573
|
-
```ruby
|
|
574
|
-
assignment = client.assignments.create(
|
|
575
|
-
documento['id'],
|
|
576
|
-
method: 'virtual',
|
|
577
|
-
signers: [
|
|
578
|
-
{ id: signatario['id'], verification_method: 'Email', notification_methods: ['Email'], step: 1 },
|
|
579
|
-
{ id: certificado['id'], verification_method: 'DigitalCertificate', notification_methods: ['Email'], step: 2 }
|
|
580
|
-
],
|
|
581
|
-
message: 'Por favor, assine o contrato em anexo.',
|
|
582
|
-
expires_at: '2099-12-31T23:59:00-03:00',
|
|
583
|
-
copy_receivers: [] # IDs de signatários que só recebem cópia
|
|
584
|
-
)
|
|
585
|
-
|
|
586
|
-
assignment['signing_urls']
|
|
587
|
-
# => [{ 'signer_id' => 'signer-id', 'url' => 'https://.../sign/...' }, ...]
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
**`collect`** — campos posicionados página a página, além da lista de signatários:
|
|
591
|
-
|
|
592
|
-
```ruby
|
|
593
|
-
client.assignments.create(
|
|
594
|
-
documento['id'],
|
|
595
|
-
method: 'collect',
|
|
596
|
-
signers: [{ id: signatario['id'] }],
|
|
597
|
-
entries: [{
|
|
598
|
-
page_id: documento['pages'].first['id'],
|
|
599
|
-
fields: [{
|
|
600
|
-
signer_id: signatario['id'],
|
|
601
|
-
field_id: campo['id'],
|
|
602
|
-
display_settings: { left: 100, top: 100, width: 240, height: 48, fontSize: 16 }
|
|
603
|
-
}]
|
|
604
|
-
}]
|
|
605
|
-
)
|
|
65
|
+
oauth_client = Assinafy::Client.new(token: tokens.fetch('access_token'), account_id: 'workspace-id')
|
|
606
66
|
```
|
|
607
67
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
68
|
+
Persista workspace, escopos, expiração e tokens criptografados. `offline_access` solicita o refresh token, mas não
|
|
69
|
+
aparece no `scope` devolvido. Renove sob um lock por conexão e salve atomicamente o novo access token e o novo
|
|
70
|
+
refresh token; cada refresh aposenta o anterior. Nunca repita token/revoke por middleware. Ao desconectar, revogue
|
|
71
|
+
o refresh token mais recente. `client.oauth.userinfo`, `protected_resource_metadata` e
|
|
72
|
+
`authorization_server_metadata` completam o fluxo.
|
|
612
73
|
|
|
613
|
-
|
|
614
|
-
paralelo. Se informado para um signatário, informe para todos, em uma sequência contínua iniciada
|
|
615
|
-
em 1. Omita para assinatura simultânea.
|
|
616
|
-
- Cada signatário é notificado quando seu passo é ativado: o passo 1 na criação, os seguintes
|
|
617
|
-
quando todos do passo anterior concluírem.
|
|
618
|
-
- Um signatário por certificado digital fica sozinho no seu passo.
|
|
619
|
-
- `expires_at` deve ser ISO 8601 com fuso horário e estar pelo menos uma hora no futuro.
|
|
74
|
+
## Fluxo completo de assinatura
|
|
620
75
|
|
|
621
|
-
|
|
76
|
+
1. Faça upload e espere os metadados do PDF.
|
|
77
|
+
2. Crie ou reutilize os signatários.
|
|
78
|
+
3. Estime o custo.
|
|
79
|
+
4. Crie um assignment `virtual` ou `collect`.
|
|
80
|
+
5. O signatário aceita os termos, confirma sua identidade e assina ou recusa.
|
|
81
|
+
6. Acompanhe atividades/webhooks, baixe o artefato e verifique seu hash.
|
|
82
|
+
7. Opcionalmente registre termos contratuais, aditivos, renovação ou encerramento.
|
|
83
|
+
8. Exclua somente os recursos que sua aplicação possui, depois dos dependentes.
|
|
622
84
|
|
|
623
85
|
```ruby
|
|
624
|
-
client.
|
|
625
|
-
client.
|
|
626
|
-
client.assignments.reset_expiration(documento['id'], assignment['id'], '2099-01-31T23:59:00-03:00')
|
|
627
|
-
client.assignments.whatsapp_notifications(documento['id'], assignment['id'])
|
|
628
|
-
client.assignments.list # assignments da conta
|
|
629
|
-
```
|
|
86
|
+
document = client.documents.upload(file_path: '/tmp/contract.pdf')
|
|
87
|
+
document = client.documents.wait_until_ready(document.fetch('id'))
|
|
630
88
|
|
|
631
|
-
|
|
89
|
+
signer = client.signers.find_by_email('signer@example.com') ||
|
|
90
|
+
client.signers.create(full_name: 'Example Signer', email: 'signer@example.com')
|
|
632
91
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
resultado[:signer_ids] # => ["signer-id"]
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
O helper faz upload, espera o processamento, cria os signatários (incluindo `government_id`, quando
|
|
646
|
-
informado) e abre um assignment `virtual`. Ele valida o payload inteiro **antes** de enviar
|
|
647
|
-
qualquer coisa, então um `expires_at` malformado não deixa um documento órfão.
|
|
648
|
-
|
|
649
|
-
> **Não é transacional.** Se uma chamada posterior falhar, o documento enviado e os signatários
|
|
650
|
-
> já criados continuam existindo. Em caso de erro, `e.context[:document]` e
|
|
651
|
-
> `e.context[:signer_ids]` trazem o que foi criado, para você limpar.
|
|
652
|
-
|
|
653
|
-
---
|
|
654
|
-
|
|
655
|
-
## 9. A experiência do signatário
|
|
656
|
-
|
|
657
|
-
O signatário recebe o link de acesso pelo canal de notificação. As chamadas abaixo usam o **código
|
|
658
|
-
de acesso do signatário** (enviado como parâmetro de query `signer-access-code`), não suas
|
|
659
|
-
credenciais de workspace — o SDK remove `X-Api-Key`/`Authorization` delas. Um cliente sem
|
|
660
|
-
credenciais basta:
|
|
661
|
-
|
|
662
|
-
```ruby
|
|
663
|
-
signatario_client = Assinafy::Client.new
|
|
664
|
-
codigo = 'codigo-de-acesso-do-link-de-assinatura'
|
|
92
|
+
request = {
|
|
93
|
+
method: 'virtual',
|
|
94
|
+
signers: [{
|
|
95
|
+
signer_id: signer.fetch('id'),
|
|
96
|
+
verification_method: 'Email',
|
|
97
|
+
notification_methods: ['Email']
|
|
98
|
+
}],
|
|
99
|
+
expires_at: (Time.now + 86_400).iso8601
|
|
100
|
+
}
|
|
665
101
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
signatario_client.signers.accept_terms(signer_access_code: codigo) unless dados['has_accepted_terms']
|
|
669
|
-
doc = signatario_client.assignments.signer_document(signer_access_code: codigo, has_accepted_terms: true)
|
|
670
|
-
|
|
671
|
-
# 2. Confirmar a identidade
|
|
672
|
-
signatario_client.signers.confirm_data(
|
|
673
|
-
doc['id'], { full_name: 'Ana Silva', government_id: '00000000000' },
|
|
674
|
-
signer_access_code: codigo
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
# 3. Verificar o código de uso único (OTP) recebido por e-mail ou WhatsApp
|
|
678
|
-
signatario_client.signers.verify_email(verification_code: '123456', signer_access_code: codigo)
|
|
679
|
-
|
|
680
|
-
# 4. Enviar a imagem da assinatura (PNG)
|
|
681
|
-
signatario_client.signers.upload_signature(
|
|
682
|
-
File.binread('assinatura.png'), signer_access_code: codigo, type: 'signature'
|
|
683
|
-
)
|
|
102
|
+
cost = client.assignments.estimate_cost(document.fetch('id'), request)
|
|
103
|
+
assignment = client.assignments.create(document.fetch('id'), request)
|
|
684
104
|
```
|
|
685
105
|
|
|
686
|
-
`
|
|
106
|
+
`Client#upload_and_request_signatures` executa upload, signatários e assignment numa chamada e valida todo o
|
|
107
|
+
pedido antes da rede. Templates configurados usam `client.documents.create_from_template`.
|
|
687
108
|
|
|
688
|
-
|
|
109
|
+
### Verificação e notificação
|
|
689
110
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
```ruby
|
|
697
|
-
itens = doc.fetch('assignment').fetch('items').map do |item|
|
|
698
|
-
{
|
|
699
|
-
item_id: item.fetch('id'),
|
|
700
|
-
field_id: item.dig('field', 'id'),
|
|
701
|
-
page_id: item.dig('page', 'id'),
|
|
702
|
-
value: 'Aceito'
|
|
703
|
-
}
|
|
704
|
-
end
|
|
705
|
-
|
|
706
|
-
signatario_client.assignments.sign(doc['id'], doc.dig('assignment', 'id'), itens, signer_access_code: codigo)
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
O SDK converte as chaves `item_id`/`field_id`/`page_id` para o `itemId`/`fieldId`/`pageId` que a API
|
|
710
|
-
espera.
|
|
711
|
-
|
|
712
|
-
Recusar:
|
|
713
|
-
|
|
714
|
-
```ruby
|
|
715
|
-
signatario_client.assignments.decline(
|
|
716
|
-
doc['id'], doc.dig('assignment', 'id'),
|
|
717
|
-
decline_reason: 'Valores divergentes da proposta',
|
|
718
|
-
signer_access_code: codigo
|
|
719
|
-
)
|
|
720
|
-
# Vários documentos de uma vez:
|
|
721
|
-
signatario_client.signer_documents.decline_multiple([doc['id']], decline_reason: 'Não', signer_access_code: codigo)
|
|
722
|
-
```
|
|
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
|
-
|
|
773
|
-
### Signatários com certificado A1/A3
|
|
111
|
+
| `verification_method` | Notificação | Regras |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| `Email` | `Email` | código por e-mail |
|
|
114
|
+
| `Whatsapp` | `Whatsapp` | código por WhatsApp |
|
|
115
|
+
| `IdDocumentFaceMatchBasic` | `Email` ou `Whatsapp` | documento + selfie; requer CPF válido e recurso na conta |
|
|
116
|
+
| `DigitalCertificate` | `Email` ou `Whatsapp` | A1/A3 no Web PKI hospedado; somente assignment `virtual` |
|
|
774
117
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
signatários por certificado, e o SDK não envolve o handshake Web PKI
|
|
779
|
-
(`/signers/certificate/start`, `/signers/certificate/complete`), cujos esquemas não fazem parte do
|
|
780
|
-
contrato OpenAPI. Concluído o fluxo, o artefato `pades` traz a assinatura PAdES qualificada.
|
|
118
|
+
Use exatamente uma notificação por signatário. Etapas são opcionais, mas, quando usadas, devem ser positivas,
|
|
119
|
+
contíguas a partir de 1 e atribuídas a todos; valores iguais são paralelos. Um signatário de certificado fica
|
|
120
|
+
sozinho na etapa. `expires_at` deve ter fuso e ficar pelo menos uma hora no futuro.
|
|
781
121
|
|
|
782
|
-
|
|
122
|
+
Na biometria, `signers.start_identity_verification(consent: true, signer_access_code:)` devolve uma URL bearer:
|
|
123
|
+
preserve o fragmento e não a armazene em logs ou browser storage. O revisor usa `assignments.identity_review`,
|
|
124
|
+
`identity_review_image` e `decide_identity_review`. Evidências existem apenas durante `InReview` e não devem ser
|
|
125
|
+
cacheadas. Uma rejeição cancela o documento. Certificados A1/A3 são concluídos no fluxo hospedado; o SDK não
|
|
126
|
+
inventa os endpoints de handshake ainda não publicados.
|
|
783
127
|
|
|
784
|
-
|
|
128
|
+
Operações do signatário usam `signer_access_code:` sem chave/token da workspace. O fluxo inclui termos,
|
|
129
|
+
confirmação de dados, OTP, identidade, imagem de assinatura, `assignments.sign`/`decline` e operações em lote em
|
|
130
|
+
`signer_documents`.
|
|
785
131
|
|
|
786
|
-
|
|
787
|
-
até 3 em planos pagos; cada um tem sua própria URL (distinta das demais), lista de eventos e
|
|
788
|
-
configuração de assinatura. Todo endpoint ativo inscrito num evento o recebe.
|
|
132
|
+
## Webhooks e artefatos
|
|
789
133
|
|
|
790
|
-
|
|
134
|
+
Crie o endpoint com `signing_enabled: true`, guarde o segredo `whsec_...` retornado uma vez e verifique o corpo
|
|
135
|
+
bruto antes de interpretar JSON:
|
|
791
136
|
|
|
792
137
|
```ruby
|
|
793
|
-
client.webhooks.list_event_types # => [{ 'id' => 'document_ready', 'description' => '...' }, ...]
|
|
794
|
-
|
|
795
138
|
endpoint = client.webhooks.create_endpoint(
|
|
796
|
-
url:
|
|
797
|
-
email:
|
|
798
|
-
events:
|
|
799
|
-
name: 'ERP',
|
|
139
|
+
url: 'https://example.com/webhooks/assinafy',
|
|
140
|
+
email: 'ops@example.com',
|
|
141
|
+
events: ['document_certificated'],
|
|
800
142
|
signing_enabled: true
|
|
801
143
|
)
|
|
802
|
-
endpoint['id'] # => "webhook-endpoint-id"
|
|
803
|
-
|
|
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
|
|
806
|
-
client.webhooks.get_endpoint('webhook-endpoint-id')
|
|
807
|
-
client.webhooks.update_endpoint('webhook-endpoint-id', is_active: false) # só os campos enviados
|
|
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
|
|
810
|
-
```
|
|
811
|
-
|
|
812
|
-
- `create_endpoint` exige `url`, `email` e `events`; `name`, `is_active` (padrão `true`) e
|
|
813
|
-
`signing_enabled` (padrão `false`) são opcionais. Chaves desconhecidas levantam
|
|
814
|
-
`ValidationError` localmente.
|
|
815
|
-
- Criar além do limite do plano responde `403`; uma URL já usada por outro endpoint responde `400`.
|
|
816
|
-
- Em `update_endpoint`, `signing_enabled: true` gera um segredo se o endpoint não tiver um (e mantém
|
|
817
|
-
o atual se tiver); `signing_enabled: false` descarta o segredo.
|
|
818
|
-
|
|
819
|
-
### Segredo de assinatura
|
|
820
144
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
de um aplicativo OAuth for perdido, exclua e recrie o endpoint.
|
|
145
|
+
verifier = Assinafy::Support::WebhookVerifier.new(endpoint.fetch('secret'))
|
|
146
|
+
raise 'invalid webhook' unless verifier.verify_delivery(raw_body, request.headers)
|
|
824
147
|
|
|
825
|
-
|
|
826
|
-
segredo = client.webhooks.endpoint_secret('webhook-endpoint-id')
|
|
827
|
-
segredo['secret'] # => "whsec_..."
|
|
828
|
-
|
|
829
|
-
novo = client.webhooks.rotate_endpoint_secret('webhook-endpoint-id')
|
|
148
|
+
event = verifier.extract_event(raw_body)
|
|
830
149
|
```
|
|
831
150
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
desativada, ambos respondem `400`.
|
|
836
|
-
|
|
837
|
-
### O contrato de entrega
|
|
838
|
-
|
|
839
|
-
| Propriedade | Valor |
|
|
840
|
-
| --- | --- |
|
|
841
|
-
| Requisição | `POST`, `Content-Type: application/json` |
|
|
842
|
-
| `webhook-id` | ID da mensagem, igual em todas as tentativas do mesmo evento para o mesmo endpoint — use para deduplicar |
|
|
843
|
-
| `webhook-timestamp` | horário Unix (segundos) da tentativa |
|
|
844
|
-
| `webhook-signature` | só com assinatura ativa: entradas `v1,<base64>` separadas por espaço |
|
|
845
|
-
| Sucesso | qualquer resposta `2xx` |
|
|
846
|
-
| Tentativas | até 2 por evento, com 3 segundos de intervalo |
|
|
847
|
-
| Circuit breaker | depois de 10 eventos consecutivos com falha, a entrega pausa e só uma amostra de eventos é testada até um dar certo |
|
|
848
|
-
|
|
849
|
-
Responda `2xx` rapidamente e processe em segundo plano. Para forçar uma nova entrega, use
|
|
850
|
-
`retry_dispatch`:
|
|
151
|
+
Deduplicate por `webhook-id`. O timestamp tem tolerância padrão de 300 segundos. Aplicações OAuth precisam guardar
|
|
152
|
+
o segredo da criação: leitura e rotação exigem chave de API ou sessão de usuário. Depois da certificação, use
|
|
153
|
+
`documents.download`, `thumbnail`, `download_page`, `activities` e `verify` conforme o artefato.
|
|
851
154
|
|
|
852
|
-
|
|
853
|
-
client.webhooks.list_dispatches(endpoint_id: 'webhook-endpoint-id', delivered: false, per_page: 20)
|
|
854
|
-
client.webhooks.retry_dispatch('dispatch-id')
|
|
855
|
-
```
|
|
155
|
+
## Contratos, erros e recursos
|
|
856
156
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
segundos.
|
|
157
|
+
Planos com contract tracking usam `client.contracts` para termos, família, aditivo, renovação e encerramento.
|
|
158
|
+
Envie o `updated_at` lido como precondição de escrita; conflito concorrente responde `409`. Uma renovação repetível
|
|
159
|
+
usa o `term` atual ou `end_date` posterior.
|
|
861
160
|
|
|
862
|
-
|
|
161
|
+
Falhas da API levantam `Assinafy::ApiError`; OAuth usa `OAuthError < ApiError`; entrada inválida usa
|
|
162
|
+
`ValidationError`; transporte/TLS usa `NetworkError`. `ApiError#rate_limit` expõe limite/restante/reset e
|
|
163
|
+
`#retry_after` o atraso de `429`; o SDK nunca repete automaticamente. Listagens paginadas devolvem
|
|
164
|
+
`{ data:, meta: }`, listas simples devolvem `Array`, binários devolvem `String` e respostas vazias devolvem `nil`.
|
|
863
165
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
assinatura em tempo constante e rejeita horários a mais de 5 minutos do relógio local:
|
|
166
|
+
Recursos: `accounts`, `auth`, `oauth`, `users`, `documents`, `contracts`, `signers`, `signer_documents`,
|
|
167
|
+
`assignments`, `templates`, `tags`, `fields`, `webhooks` e `webhook_verifier`. As assinaturas RBS publicadas ficam em
|
|
168
|
+
[`sig/assinafy.rbs`](sig/assinafy.rbs).
|
|
868
169
|
|
|
869
|
-
|
|
870
|
-
verificador = Assinafy::Support::WebhookVerifier.new(ENV.fetch('ASSINAFY_WEBHOOK_SECRET'))
|
|
871
|
-
# ou client.webhook_verifier, que usa o webhook_secret do cliente
|
|
872
|
-
|
|
873
|
-
verificador.verify_delivery(corpo_bruto, headers) # => true / false
|
|
874
|
-
verificador.verify_delivery(corpo_bruto, headers, tolerance: 120) # janela em segundos
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
`headers` pode ser o `request.headers` do Rails, o `env` do Rack (`HTTP_WEBHOOK_ID`) ou um Hash
|
|
878
|
-
simples. O método devolve `false` — nunca levanta — para segredo ausente ou malformado, cabeçalho
|
|
879
|
-
faltando, assinatura errada ou horário fora da janela. Sempre passe o corpo **bruto**, exatamente
|
|
880
|
-
como recebido, nunca o JSON reserializado. Com mais de um endpoint, cada um tem seu segredo.
|
|
881
|
-
|
|
882
|
-
Receptor Rails:
|
|
883
|
-
|
|
884
|
-
```ruby
|
|
885
|
-
class AssinafyWebhooksController < ActionController::API
|
|
886
|
-
VERIFICADOR = Assinafy::Support::WebhookVerifier.new(ENV.fetch('ASSINAFY_WEBHOOK_SECRET'))
|
|
887
|
-
|
|
888
|
-
def create
|
|
889
|
-
corpo = request.raw_post
|
|
890
|
-
return head(:unauthorized) unless VERIFICADOR.verify_delivery(corpo, request.headers)
|
|
891
|
-
|
|
892
|
-
webhook_id = request.headers['webhook-id']
|
|
893
|
-
return head(:ok) if EventoAssinafy.exists?(webhook_id: webhook_id) # já processado
|
|
894
|
-
|
|
895
|
-
evento = VERIFICADOR.extract_event(corpo)
|
|
896
|
-
EventoAssinafy.create!(webhook_id: webhook_id, tipo: VERIFICADOR.event_type(evento), corpo: corpo)
|
|
897
|
-
ProcessarEventoAssinafyJob.perform_later(webhook_id)
|
|
898
|
-
head :ok
|
|
899
|
-
end
|
|
900
|
-
end
|
|
901
|
-
```
|
|
902
|
-
|
|
903
|
-
Receptor Rack (Sinatra):
|
|
904
|
-
|
|
905
|
-
```ruby
|
|
906
|
-
post '/webhooks/assinafy' do
|
|
907
|
-
corpo = request.body.read
|
|
908
|
-
halt 401 unless VERIFICADOR.verify_delivery(corpo, request.env)
|
|
909
|
-
|
|
910
|
-
evento = VERIFICADOR.extract_event(corpo)
|
|
911
|
-
case VERIFICADOR.event_type(evento)
|
|
912
|
-
when 'signer_signed_document' then processar(VERIFICADOR.event_object(evento)) # o Document
|
|
913
|
-
when 'document_ready' then baixar_assinado(VERIFICADOR.event_object(evento)['id'])
|
|
914
|
-
end
|
|
915
|
-
|
|
916
|
-
200
|
|
917
|
-
end
|
|
918
|
-
```
|
|
919
|
-
|
|
920
|
-
`extract_event` devolve `nil` para JSON inválido; `event_type`, `event_payload`, `event_object` e
|
|
921
|
-
`event_subject` leem os campos do envelope.
|
|
922
|
-
|
|
923
|
-
O método `verify(corpo, assinatura_hex)` continua disponível apenas para receptores cujo **próprio
|
|
924
|
-
gateway** assina o corpo com um HMAC-SHA256 em hexadecimal e um segredo compartilhado.
|
|
925
|
-
|
|
926
|
-
### Assinatura legada
|
|
927
|
-
|
|
928
|
-
`client.webhooks.register`, `#get` e `#inactivate` (`/webhooks/subscriptions`) continuam funcionando e
|
|
929
|
-
agem sobre o endpoint **mais antigo** da conta. `register` aceita só `url`, `email`, `events` e
|
|
930
|
-
`is_active`; outras chaves levantam `ValidationError`. Prefira as operações de endpoint.
|
|
931
|
-
|
|
932
|
-
---
|
|
933
|
-
|
|
934
|
-
## 11. Andamento, download e verificação
|
|
935
|
-
|
|
936
|
-
```ruby
|
|
937
|
-
client.documents.signing_progress(documento['id'])
|
|
938
|
-
# => { signed: 1, total: 3, pending: 2, percentage: 33.33 }
|
|
939
|
-
|
|
940
|
-
client.documents.fully_signed?(documento['id']) # => false
|
|
941
|
-
client.documents.activities(documento['id']) # trilha completa de eventos, com origin (ip, user-agent)
|
|
942
|
-
```
|
|
943
|
-
|
|
944
|
-
| Artefato | Conteúdo |
|
|
945
|
-
| --- | --- |
|
|
946
|
-
| `original` | O PDF enviado, como recebido |
|
|
947
|
-
| `certificated` | O documento assinado, com a certificação da plataforma |
|
|
948
|
-
| `certificate-page` | Apenas a página de certificação |
|
|
949
|
-
| `pades` | Assinaturas ICP-Brasil dos signatários + caixa de certificação — só em documentos com signatários por certificado digital |
|
|
950
|
-
| `bundle` | Zip com `original`, `certificated` e `certificate-page`, mais o `pades` quando houver |
|
|
951
|
-
|
|
952
|
-
```ruby
|
|
953
|
-
File.binwrite('assinado.pdf', client.documents.download(documento['id'], 'certificated'))
|
|
954
|
-
File.binwrite('pades.pdf', client.documents.download(documento['id'], 'pades'))
|
|
955
|
-
File.binwrite('pacote.zip', client.documents.download(documento['id'], 'bundle'))
|
|
956
|
-
```
|
|
957
|
-
|
|
958
|
-
A verificação pública confere um documento assinado pelo hash da assinatura, sem autenticação:
|
|
959
|
-
|
|
960
|
-
```ruby
|
|
961
|
-
verificacao = Assinafy::Client.new.documents.verify('hash-da-assinatura')
|
|
962
|
-
verificacao['is_valid'] # => true
|
|
963
|
-
verificacao['agreement_code'] # código impresso no certificado do documento
|
|
964
|
-
```
|
|
965
|
-
|
|
966
|
-
Ela devolve o resultado da Assinafy; não valida independentemente a assinatura do PDF nem a
|
|
967
|
-
cadeia de certificação.
|
|
968
|
-
|
|
969
|
-
---
|
|
970
|
-
|
|
971
|
-
## 12. Organizar e limpar
|
|
972
|
-
|
|
973
|
-
```ruby
|
|
974
|
-
tag = client.tags.create(name: 'Contratos 2026', color: '2072b9')
|
|
975
|
-
client.documents.append_tags(documento['id'], [tag['id']])
|
|
976
|
-
client.documents.replace_tags(documento['id'], [tag['id']])
|
|
977
|
-
client.documents.detach_tag(documento['id'], tag['id'])
|
|
978
|
-
|
|
979
|
-
# Documentos que carregam TODAS as tags informadas (o SDK junta o Array com vírgulas):
|
|
980
|
-
client.documents.list(tags: [tag['id'], 'outra-tag-id'])
|
|
981
|
-
|
|
982
|
-
client.documents.rename(documento['id'], 'Contrato Acme — assinado')
|
|
983
|
-
```
|
|
984
|
-
|
|
985
|
-
Apague só o que a sua aplicação criou, e só depois de concluir o que depende do recurso. Alguns
|
|
986
|
-
respondem `409` enquanto ainda estão em processamento ou referenciados. Mantenha signatários e
|
|
987
|
-
templates compartilhados.
|
|
988
|
-
|
|
989
|
-
```ruby
|
|
990
|
-
client.documents.delete(documento['id'])
|
|
991
|
-
client.signers.delete(signatario['id'])
|
|
992
|
-
client.webhooks.delete_endpoint('webhook-endpoint-id')
|
|
993
|
-
client.tags.delete(tag['id'], force: true) # force desvincula de documentos e templates
|
|
994
|
-
```
|
|
995
|
-
|
|
996
|
-
---
|
|
997
|
-
|
|
998
|
-
## 13. Erros
|
|
999
|
-
|
|
1000
|
-
```ruby
|
|
1001
|
-
begin
|
|
1002
|
-
client.documents.details(documento_id)
|
|
1003
|
-
rescue Assinafy::ValidationError => e
|
|
1004
|
-
# entrada inválida — detectada ANTES de qualquer requisição
|
|
1005
|
-
warn e.errors.inspect
|
|
1006
|
-
rescue Assinafy::OAuthError => e
|
|
1007
|
-
# falha em endpoint OAuth (subclasse de ApiError)
|
|
1008
|
-
warn "#{e.error}: #{e.error_description}"
|
|
1009
|
-
rescue Assinafy::ApiError => e
|
|
1010
|
-
# a API respondeu com erro
|
|
1011
|
-
warn "Assinafy respondeu #{e.status_code}: #{e.message}"
|
|
1012
|
-
warn e.response_data.inspect
|
|
1013
|
-
rescue Assinafy::NetworkError => e
|
|
1014
|
-
# conexão, timeout ou TLS
|
|
1015
|
-
warn "Falha de rede: #{e.message}"
|
|
1016
|
-
rescue Assinafy::Error => e
|
|
1017
|
-
# qualquer outra falha do SDK
|
|
1018
|
-
warn e.context.inspect
|
|
1019
|
-
end
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
Toda exceção do SDK deriva de `Assinafy::Error` e carrega um `#context` com detalhes para
|
|
1023
|
-
depuração. `ApiError` trata as duas formas de corpo de erro da API — o erro de framework
|
|
1024
|
-
(`{"name":..., "code":..., "status":...}`) e o envelope de aplicação
|
|
1025
|
-
(`{"status":..., "data":null, "message":...}`) — inclusive quando um `200` traz um `status`
|
|
1026
|
-
interno de falha. Num `403`, `e.context[:www_authenticate]` traz o desafio OAuth, quando houver. Num `429`,
|
|
1027
|
-
`e.retry_after` traz os segundos de `Retry-After` e `e.rate_limit` traz `{ limit:, remaining:, reset: }` dos
|
|
1028
|
-
cabeçalhos `X-Rate-Limit-*`.
|
|
1029
|
-
|
|
1030
|
-
---
|
|
1031
|
-
|
|
1032
|
-
## 14. Paginação
|
|
1033
|
-
|
|
1034
|
-
Os métodos `*.list*` devolvem `{ data: [...], meta: { ... } }` quando a API envia cabeçalhos de
|
|
1035
|
-
paginação:
|
|
1036
|
-
|
|
1037
|
-
```ruby
|
|
1038
|
-
pagina = client.documents.list(page: 1, per_page: 50)
|
|
1039
|
-
pagina[:data] # => [Documento, ...]
|
|
1040
|
-
pagina[:meta] # => { current_page: 1, per_page: 50, total: 128, last_page: 3 }
|
|
1041
|
-
```
|
|
1042
|
-
|
|
1043
|
-
O `per_page:` em estilo Ruby é convertido para o parâmetro `per-page` documentado. Valores acima
|
|
1044
|
-
do máximo são limitados pelo servidor. Endpoints que não paginam (por exemplo `accounts.list`)
|
|
1045
|
-
devolvem só `{ data: [...] }`, sem `meta`.
|
|
1046
|
-
|
|
1047
|
-
```ruby
|
|
1048
|
-
def cada_documento(client)
|
|
1049
|
-
return to_enum(:cada_documento, client) unless block_given?
|
|
1050
|
-
|
|
1051
|
-
pagina = 1
|
|
1052
|
-
loop do
|
|
1053
|
-
resultado = client.documents.list(page: pagina, per_page: 50)
|
|
1054
|
-
resultado[:data].each { |documento| yield documento }
|
|
1055
|
-
|
|
1056
|
-
meta = resultado[:meta]
|
|
1057
|
-
break unless meta && meta[:last_page] && pagina < meta[:last_page]
|
|
1058
|
-
|
|
1059
|
-
pagina += 1
|
|
1060
|
-
end
|
|
1061
|
-
end
|
|
1062
|
-
```
|
|
1063
|
-
|
|
1064
|
-
---
|
|
1065
|
-
|
|
1066
|
-
## 15. Recursos do cliente
|
|
1067
|
-
|
|
1068
|
-
| Acessor | Cobre |
|
|
1069
|
-
| --- | --- |
|
|
1070
|
-
| `client.auth` | login, autenticação em dois fatores, login social, senha, chaves de API |
|
|
1071
|
-
| `client.oauth` | OAuth 2.1: token, refresh, revoke, userinfo, descoberta |
|
|
1072
|
-
| `client.accounts` | workspaces, tema, KPIs, logo |
|
|
1073
|
-
| `client.users` | perfil próprio, KPIs, preferências de notificação |
|
|
1074
|
-
| `client.documents` | upload, listagem, download, ciclo de vida, tags, verificação |
|
|
1075
|
-
| `client.signers` | CRUD de signatários e todo o autoatendimento do signatário |
|
|
1076
|
-
| `client.signer_documents` | documentos do signatário, assinar/recusar em lote |
|
|
1077
|
-
| `client.assignments` | pedidos de assinatura, custos, reenvios, assinatura, recusa |
|
|
1078
|
-
| `client.templates` | ciclo de vida de templates |
|
|
1079
|
-
| `client.tags` | CRUD de tags |
|
|
1080
|
-
| `client.fields` | definições de campo e validação |
|
|
1081
|
-
| `client.webhooks` | endpoints, segredos de assinatura, entregas, reenvio |
|
|
1082
|
-
| `client.webhook_verifier` | verificação de assinatura das entregas recebidas |
|
|
1083
|
-
|
|
1084
|
-
KPIs: `client.users.stats` e `client.accounts.stats` aceitam `granularity:` (`'monthly'` ou
|
|
1085
|
-
`'daily'`) e `month:` (`'AAAA-MM'`), validados localmente:
|
|
1086
|
-
|
|
1087
|
-
```ruby
|
|
1088
|
-
client.accounts.stats(granularity: 'daily', month: '2026-06')
|
|
1089
|
-
```
|
|
1090
|
-
|
|
1091
|
-
`client.faraday_connection` expõe a conexão Faraday para inspeção em testes. Nunca adicione
|
|
1092
|
-
middleware que repita requisições: pedidos de token não podem ser reenviados.
|
|
1093
|
-
|
|
1094
|
-
---
|
|
1095
|
-
|
|
1096
|
-
## 16. Assinaturas RBS
|
|
1097
|
-
|
|
1098
|
-
`sig/assinafy.rbs` acompanha a gem, então consumidores que usam RBS têm as assinaturas
|
|
1099
|
-
publicadas. [`spec/rbs_signature_spec.rb`](spec/rbs_signature_spec.rb) garante que todo método
|
|
1100
|
-
público de recurso, do `Client` e de `Assinafy::OAuth` tenha uma assinatura declarada.
|
|
1101
|
-
|
|
1102
|
-
---
|
|
1103
|
-
|
|
1104
|
-
## Documentação
|
|
1105
|
-
|
|
1106
|
-
- **[README.en.md](README.en.md)** — referência completa por recurso, em inglês
|
|
1107
|
-
- [docs/API_REFERENCE.md](docs/API_REFERENCE.md) — referência por operação
|
|
1108
|
-
- [CHANGELOG.md](CHANGELOG.md) — histórico de versões
|
|
1109
|
-
- [Documentação da API](https://api.assinafy.com.br/v1/docs)
|
|
1110
|
-
|
|
1111
|
-
## Desenvolvimento e testes
|
|
170
|
+
## Desenvolvimento
|
|
1112
171
|
|
|
1113
172
|
```bash
|
|
1114
|
-
bundle exec
|
|
1115
|
-
bundle exec rubocop
|
|
173
|
+
bundle exec rspec
|
|
174
|
+
bundle exec rubocop --cache false
|
|
1116
175
|
bundle exec bundler-audit check --update
|
|
1117
176
|
rbs -I sig validate
|
|
1118
177
|
ruby scripts/check_api_contract.rb
|
|
1119
178
|
```
|
|
1120
179
|
|
|
1121
|
-
|
|
1122
|
-
Veja os [pré-requisitos e variáveis](README.en.md#live-integration-tests), incluindo um template
|
|
1123
|
-
com papel `Signer` para testar a geração. Os testes enviam e-mails e removem os recursos criados.
|
|
1124
|
-
|
|
1125
|
-
## Licença
|
|
1126
|
-
|
|
1127
|
-
Distribuído sob a licença [MIT](LICENSE).
|
|
180
|
+
Licença: [MIT](LICENSE).
|