@assinafy/sdk 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -1
- package/README.en.md +1373 -0
- package/README.md +764 -564
- package/SECURITY.md +13 -0
- package/dist/index.d.mts +778 -7
- package/dist/index.d.ts +778 -7
- package/dist/index.js +844 -34
- package/dist/index.mjs +839 -32
- package/docs/API_COVERAGE.md +28 -4
- package/docs/COMPATIBILITY.md +63 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,72 +1,73 @@
|
|
|
1
1
|
# @assinafy/sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
**
|
|
23
|
-
[
|
|
24
|
-
[
|
|
25
|
-
([
|
|
26
|
-
[
|
|
27
|
-
|
|
28
|
-
**
|
|
29
|
-
[
|
|
30
|
-
[
|
|
31
|
-
[
|
|
32
|
-
[
|
|
33
|
-
|
|
34
|
-
**
|
|
35
|
-
[
|
|
36
|
-
[
|
|
37
|
-
[
|
|
38
|
-
[
|
|
39
|
-
[
|
|
40
|
-
[
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## Installation
|
|
3
|
+
*Português · [Read in English](README.en.md)*
|
|
4
|
+
|
|
5
|
+
SDK oficial em TypeScript para a [API Assinafy](https://api.assinafy.com.br/v1/docs) — plataforma
|
|
6
|
+
brasileira de assinatura eletrônica de documentos.
|
|
7
|
+
|
|
8
|
+
Cobre as 93 operações do documento OpenAPI oficial: contas, autenticação, aplicações OAuth 2.1,
|
|
9
|
+
usuários, documentos, assignments, signatários, fluxos do lado do signatário, templates, tags,
|
|
10
|
+
campos, webhooks, identidade visual, estatísticas e o fluxo de alto nível
|
|
11
|
+
`uploadAndRequestSignatures`. Cinco rotas adicionais de gestão de templates usadas por integrações
|
|
12
|
+
existentes e dois helpers de URL de navegador são mantidos por compatibilidade.
|
|
13
|
+
|
|
14
|
+
O mapa operação a operação está em [docs/API_COVERAGE.md](docs/API_COVERAGE.md); as variações por
|
|
15
|
+
deploy, em [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md).
|
|
16
|
+
|
|
17
|
+
## Sumário
|
|
18
|
+
|
|
19
|
+
Este documento vai da instalação ao fluxo completo de assinatura e, depois, ao detalhe de cada
|
|
20
|
+
recurso. Leia na ordem na primeira vez; use como referência depois.
|
|
21
|
+
|
|
22
|
+
**Preparação** — [Requisitos](#requisitos) · [Instalação](#instalação) ·
|
|
23
|
+
[Início rápido](#início-rápido) · [Autenticação](#autenticação) ·
|
|
24
|
+
[Aplicações OAuth](#aplicações-oauth) · [Configuração](#configuração)
|
|
25
|
+
([limite de requisições](#limite-de-requisições), [fábricas](#fábricas)) ·
|
|
26
|
+
[Cobertura de endpoints](#cobertura-de-endpoints)
|
|
27
|
+
|
|
28
|
+
**O fluxo ponta a ponta** — [Ciclo de vida do documento](#ciclo-de-vida-do-documento):
|
|
29
|
+
[envio do PDF](#1-envie-o-pdf) → [signatários](#2-crie-ou-reaproveite-os-signatários-por-e-mail) →
|
|
30
|
+
[orçamento e pedido de assinatura](#3-orce-depois-peça-as-assinaturas) →
|
|
31
|
+
[o lado do signatário](#4-conclua-o-fluxo-do-signatário-por-e-mail) →
|
|
32
|
+
[conclusão e artefatos](#5-acompanhe-a-conclusão-e-baixe-os-artefatos)
|
|
33
|
+
|
|
34
|
+
**Detalhe por recurso** — [Referência de recursos](#referência-de-recursos):
|
|
35
|
+
[documentos](#documentos) · [signatários](#signatários) · [assignments](#assignments) ·
|
|
36
|
+
[ramos pagos de assinatura](#ramos-pagos-de-assinatura) · [templates](#templates) · [tags](#tags) ·
|
|
37
|
+
[workspaces](#workspaces) · [definições de campo](#definições-de-campo) ·
|
|
38
|
+
[autenticação e chaves de API](#autenticação--gestão-de-chaves-de-api) ·
|
|
39
|
+
[usuário autenticado](#usuário-autenticado) · [webhooks](#webhooks)
|
|
40
|
+
([verificação](#verificação-de-webhooks)) ·
|
|
41
|
+
[endpoints do signatário](#endpoints-do-signatário)
|
|
42
|
+
|
|
43
|
+
**O restante** — [Helper de alto nível](#helper-de-alto-nível) · [Erros](#erros) ·
|
|
44
|
+
[Ambientes](#ambientes) · [Desenvolvimento](#desenvolvimento) · [Licença](#licença)
|
|
45
|
+
|
|
46
|
+
## Requisitos
|
|
47
|
+
|
|
48
|
+
- Node.js 22+ (usa as APIs nativas `FormData` / `Blob` para upload). Os imports CJS e ESM
|
|
49
|
+
empacotados são testados no 22 (LTS de manutenção), 24 (LTS ativo) e 26 (Current). O Node 20
|
|
50
|
+
chegou ao fim da vida em abril de 2026 e não é suportado.
|
|
51
|
+
- ou Bun 1.4.0 (versão fixada no desenvolvimento e na CI)
|
|
52
|
+
|
|
53
|
+
## Instalação
|
|
55
54
|
|
|
56
55
|
```bash
|
|
57
56
|
npm install @assinafy/sdk
|
|
58
|
-
#
|
|
57
|
+
# ou
|
|
59
58
|
bun add @assinafy/sdk
|
|
60
59
|
```
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
O pacote é publicado no [npmjs.com](https://www.npmjs.com/package/@assinafy/sdk) e no
|
|
62
|
+
[GitHub Packages](https://github.com/assinafy/typescript-sdk/packages). Para instalar do GitHub
|
|
63
|
+
Packages, adicione ao seu `.npmrc`:
|
|
63
64
|
|
|
64
65
|
```
|
|
65
66
|
@assinafy:registry=https://npm.pkg.github.com
|
|
66
67
|
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
|
|
67
68
|
```
|
|
68
69
|
|
|
69
|
-
##
|
|
70
|
+
## Início rápido
|
|
70
71
|
|
|
71
72
|
```ts
|
|
72
73
|
import { AssinafyClient } from '@assinafy/sdk';
|
|
@@ -78,140 +79,278 @@ const client = new AssinafyClient({
|
|
|
78
79
|
baseUrl,
|
|
79
80
|
});
|
|
80
81
|
|
|
81
|
-
const
|
|
82
|
-
source: { filePath: './
|
|
82
|
+
const resultado = await client.uploadAndRequestSignatures({
|
|
83
|
+
source: { filePath: './contrato.pdf' },
|
|
83
84
|
signers: [
|
|
84
|
-
{ name: '
|
|
85
|
-
{ name: '
|
|
85
|
+
{ name: 'João Silva', email: 'joao@exemplo.com.br' },
|
|
86
|
+
{ name: 'Maria Souza', email: 'maria@exemplo.com.br' },
|
|
86
87
|
],
|
|
87
|
-
message: '
|
|
88
|
+
message: 'Por favor, assine este contrato',
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
console.log('
|
|
91
|
-
console.log('
|
|
91
|
+
console.log('ID do documento:', resultado.document.id);
|
|
92
|
+
console.log('ID do assignment:', resultado.assignment.id);
|
|
92
93
|
```
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
[
|
|
95
|
+
Esse caminho usa verificação e notificação por e-mail para todos os signatários. WhatsApp e
|
|
96
|
+
certificado digital ICP-Brasil têm pré-requisitos e custos próprios — veja
|
|
97
|
+
[Ramos pagos de assinatura](#ramos-pagos-de-assinatura) antes de habilitar qualquer um dos dois.
|
|
98
|
+
|
|
99
|
+
## Autenticação
|
|
100
|
+
|
|
101
|
+
A API aceita três credenciais. A escolha depende de **em qual workspace** você está agindo.
|
|
97
102
|
|
|
98
|
-
|
|
103
|
+
| Credencial | Age sobre | Use quando |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `apiKey` (`X-Api-Key`) | **Seu próprio** workspace | Você automatiza a sua conta. Recomendada para serviços de back-end. |
|
|
106
|
+
| `token` (`Authorization: Bearer`) | O usuário autenticado | Você obteve uma sessão com `auth.login()`. |
|
|
107
|
+
| Token de acesso OAuth (`Authorization: Bearer`) | O workspace **de outra pessoa**, com autorização dela | Você constrói um app que outras pessoas conectam. Veja [Aplicações OAuth](#aplicações-oauth). |
|
|
99
108
|
|
|
100
|
-
|
|
109
|
+
Prefira `apiKey` para integração servidor a servidor — corresponde ao header `X-Api-Key`
|
|
110
|
+
recomendado pela Assinafy.
|
|
101
111
|
|
|
102
112
|
```ts
|
|
103
|
-
//
|
|
113
|
+
// Preferido: header X-Api-Key
|
|
104
114
|
new AssinafyClient({ apiKey: 'k_xxx', accountId: 'acc_xxx' });
|
|
105
115
|
|
|
106
|
-
//
|
|
116
|
+
// Token de acesso: Authorization: Bearer <token>
|
|
107
117
|
new AssinafyClient({ token: 'jwt_xxx', accountId: 'acc_xxx' });
|
|
108
118
|
```
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
As credenciais são opcionais na construção. Um cliente sem credenciais usa um transporte separado,
|
|
121
|
+
sem autenticação, para as operações públicas e as que usam código de acesso do signatário — assim
|
|
122
|
+
uma chave de API ou token Bearer nunca é anexada por acidente:
|
|
113
123
|
|
|
114
124
|
```ts
|
|
115
|
-
const
|
|
125
|
+
const clientePublico = new AssinafyClient({
|
|
116
126
|
baseUrl: 'https://sandbox.assinafy.com.br/v1',
|
|
117
127
|
});
|
|
118
128
|
|
|
119
|
-
await
|
|
120
|
-
await
|
|
121
|
-
await
|
|
129
|
+
await clientePublico.auth.login('eu@exemplo.com.br', 'senha');
|
|
130
|
+
await clientePublico.documents.getPublic(documentId);
|
|
131
|
+
await clientePublico.signerDocuments.self(signerAccessCode);
|
|
122
132
|
```
|
|
123
133
|
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
Métodos protegidos continuam exigindo `apiKey` ou `token`; sem credencial, a API devolve o `401`
|
|
135
|
+
normal.
|
|
126
136
|
|
|
127
|
-
|
|
128
|
-
`User-Agent: Assinafy-Typescript-SDK/v<
|
|
129
|
-
|
|
130
|
-
`SDK_USER_AGENT` for custom transport checks and observability rules.
|
|
137
|
+
Todos os transportes do SDK — inclusive os públicos e os por código de acesso — enviam
|
|
138
|
+
`User-Agent: Assinafy-Typescript-SDK/v<VERSÃO>`, onde `<VERSÃO>` é a versão instalada do pacote. O
|
|
139
|
+
valor exato também é exportado como `SDK_USER_AGENT`.
|
|
131
140
|
|
|
132
|
-
##
|
|
141
|
+
## Aplicações OAuth
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
| `token` | string | — | Access token (sent as `Authorization: Bearer`). |
|
|
138
|
-
| `accountId` | string | — | Default workspace/account ID. |
|
|
139
|
-
| `baseUrl` | string | `https://api.assinafy.com.br/v1` | Absolute API base without credentials, query, or fragment. Must be `https` unless the host is loopback. |
|
|
140
|
-
| `webhookSecret` | string | — | Opt-in HMAC secret used by `WebhookVerifier`; see its [contract caveat](docs/COMPATIBILITY.md#webhook-signature-verification-is-not-in-the-openapi-contract). |
|
|
141
|
-
| `timeout` | number | `30000` | Request timeout in milliseconds. |
|
|
142
|
-
| `maxRetries` | number | `2` | Auto-retries eligible HTTP 429 responses, honoring `Retry-After`. `0` disables. |
|
|
143
|
-
| `logger` | `Logger` | no-op | Optional `{debug,info,warn,error}` logger. |
|
|
143
|
+
Use OAuth quando o seu produto for conectado **pelos usuários dele** aos workspaces **deles** na
|
|
144
|
+
Assinafy, sem que você jamais tenha a senha ou a chave de API dessas pessoas. Para automatizar o seu
|
|
145
|
+
próprio workspace nada disso é necessário — continue com a chave de API.
|
|
144
146
|
|
|
145
|
-
|
|
147
|
+
Registre a aplicação no app da Assinafy em **Configurações → Aplicações OAuth → Nova aplicação**.
|
|
148
|
+
Você define as URIs de redirecionamento (`https://`, sem fragmento, comparadas caractere a
|
|
149
|
+
caractere), as permissões máximas que a aplicação poderá pedir, e se ela é **confidencial** (roda no
|
|
150
|
+
seu servidor e recebe um `client_secret`) ou **pública** (roda no dispositivo do usuário, só PKCE).
|
|
151
|
+
Aplicações não são criadas pela API.
|
|
146
152
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`OPTIONS`, and `DELETE` requests. `GET /sign` is excluded because it records
|
|
151
|
-
that the signer viewed the assignment. Writes are not replayed by default.
|
|
152
|
-
A non-empty `Idempotency-Key` opts a custom request into SDK replay, but it is
|
|
153
|
-
not part of the current Assinafy OpenAPI contract: confirm that the target
|
|
154
|
-
route deduplicates that key server-side first. No other HTTP status is retried.
|
|
153
|
+
O fluxo passa por dois hosts de propósito: a tela de consentimento fica no servidor de autorização
|
|
154
|
+
(`https://auth.assinafy.com.br`), enquanto os endpoints de token, revogação e userinfo ficam nesta
|
|
155
|
+
API. Leia os dois da descoberta em vez de fixá-los no código.
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
```ts
|
|
158
|
+
import { AssinafyClient, OAuthError } from '@assinafy/sdk';
|
|
159
|
+
|
|
160
|
+
const client = new AssinafyClient(); // nenhuma credencial necessária
|
|
161
|
+
|
|
162
|
+
// 1 — antes de redirecionar o usuário. Guarde a requisição inteira na sessão dele:
|
|
163
|
+
// `state` e `issuer` provam que o callback é seu, `codeVerifier` completa o
|
|
164
|
+
// PKCE e `nonce` valida o id_token.
|
|
165
|
+
const requisicao = await client.oauth.createAuthorizationUrl({
|
|
166
|
+
clientId: process.env.ASSINAFY_CLIENT_ID!,
|
|
167
|
+
redirectUri: 'https://meuapp.com.br/oauth/callback',
|
|
168
|
+
scopes: ['documents:read', 'documents:write', 'offline_access'],
|
|
169
|
+
});
|
|
170
|
+
sessao.oauth = requisicao;
|
|
171
|
+
resposta.redirect(requisicao.url); // navegação de página inteira
|
|
172
|
+
|
|
173
|
+
// 2 — em https://meuapp.com.br/oauth/callback
|
|
174
|
+
const { code } = client.oauth.readAuthorizationCallback(query, sessao.oauth);
|
|
175
|
+
const tokens = await client.oauth.exchangeCode({
|
|
176
|
+
code,
|
|
177
|
+
codeVerifier: sessao.oauth.codeVerifier,
|
|
178
|
+
redirectUri: 'https://meuapp.com.br/oauth/callback',
|
|
179
|
+
clientId: process.env.ASSINAFY_CLIENT_ID!,
|
|
180
|
+
clientSecret: process.env.ASSINAFY_CLIENT_SECRET, // só aplicações confidenciais
|
|
181
|
+
});
|
|
182
|
+
// → { access_token, token_type: 'Bearer', expires_in: 3600,
|
|
183
|
+
// scope: 'documents:read documents:write',
|
|
184
|
+
// refresh_token?, id_token? }
|
|
185
|
+
|
|
186
|
+
// 3 — um token vale para EXATAMENTE UM workspace: o que o usuário escolheu.
|
|
187
|
+
const conectado = new AssinafyClient({ token: tokens.access_token });
|
|
188
|
+
const { data } = await conectado.workspaces.list();
|
|
189
|
+
const accountId = data[0]?.id; // guarde junto com os tokens
|
|
190
|
+
|
|
191
|
+
// 4 — renove antes de completar uma hora (exige `offline_access`)
|
|
192
|
+
const renovado = await client.oauth.refreshToken({
|
|
193
|
+
refreshToken: conexao.refreshToken,
|
|
194
|
+
clientId: process.env.ASSINAFY_CLIENT_ID!,
|
|
195
|
+
clientSecret: process.env.ASSINAFY_CLIENT_SECRET,
|
|
196
|
+
});
|
|
197
|
+
await conexao.save({ refreshToken: renovado.refresh_token }); // ANTES de usar
|
|
198
|
+
|
|
199
|
+
// 5 — quando o usuário desconectar
|
|
200
|
+
await client.oauth.revokeToken({
|
|
201
|
+
token: conexao.refreshToken,
|
|
202
|
+
tokenTypeHint: 'refresh_token',
|
|
203
|
+
clientId: process.env.ASSINAFY_CLIENT_ID!,
|
|
204
|
+
clientSecret: process.env.ASSINAFY_CLIENT_SECRET,
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Descoberta e identidade, quando precisar:
|
|
157
209
|
|
|
158
210
|
```ts
|
|
159
|
-
//
|
|
160
|
-
|
|
211
|
+
await client.oauth.getProtectedResourceMetadata(); // RFC 9728, na raiz do host da API
|
|
212
|
+
await client.oauth.getAuthorizationServerMetadata(); // RFC 8414, em auth.assinafy.com.br
|
|
213
|
+
await client.oauth.getUserInfo(tokens.access_token); // claims OIDC; exige `openid`
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Permissões (escopos)
|
|
217
|
+
|
|
218
|
+
| Escopo | Permite ao seu app |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| `documents:read` | Ler documentos, seus signatários, assignments e atividades |
|
|
221
|
+
| `documents:write` | Criar documentos e enviá-los para assinatura |
|
|
222
|
+
| `templates:read` | Ler templates |
|
|
223
|
+
| `templates:write` | Criar e alterar templates |
|
|
224
|
+
| `account:read` | Ler perfil, tema e logotipo do workspace |
|
|
225
|
+
| `openid` | Receber um `id_token` identificando o usuário |
|
|
226
|
+
| `profile` | Ler o nome do usuário |
|
|
227
|
+
| `email` | Ler o e-mail do usuário e se está verificado |
|
|
228
|
+
| `offline_access` | Receber um refresh token |
|
|
229
|
+
|
|
230
|
+
Peça o mínimo: o usuário aprova todos ou nenhum. Leia o `scope` devolvido pelo endpoint de token em
|
|
231
|
+
vez de supor que o pedido foi atendido por inteiro — `offline_access` nunca aparece ali, porque é um
|
|
232
|
+
sinal de requisição, não uma permissão. Faturamento, membros do workspace, credenciais e
|
|
233
|
+
administração nunca são alcançáveis por um token OAuth, quaisquer que sejam seus escopos.
|
|
234
|
+
|
|
235
|
+
### O que o SDK garante para você
|
|
236
|
+
|
|
237
|
+
- Um verificador RFC 7636 (S256) e um `state` novos a cada tentativa.
|
|
238
|
+
- `state` comparado em tempo constante e `iss` conferido (RFC 9207) **antes** de qualquer confiança
|
|
239
|
+
na resposta.
|
|
240
|
+
- O `issuer` do documento RFC 8414 conferido contra a URL de onde ele veio.
|
|
241
|
+
- `redirect_uri` obrigatoriamente `https://` absoluta e sem fragmento.
|
|
242
|
+
- O indicador de recurso RFC 8707 preenchido com a origem da API configurada e mantido idêntico
|
|
243
|
+
entre a etapa de autorização e a de token.
|
|
244
|
+
|
|
245
|
+
### Tratamento de falhas
|
|
161
246
|
|
|
162
|
-
|
|
247
|
+
```ts
|
|
248
|
+
try {
|
|
249
|
+
await conectado.documents.upload({ filePath: './contrato.pdf' });
|
|
250
|
+
} catch (erro) {
|
|
251
|
+
if (erro instanceof ApiError && erro.challenge?.error === 'insufficient_scope') {
|
|
252
|
+
// Reconecte pedindo erro.challenge.scope — por exemplo 'documents:write'.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
| Situação | O que você vê | O que fazer |
|
|
258
|
+
| --- | --- | --- |
|
|
259
|
+
| O usuário recusou | `OAuthError` com `error: 'access_denied'` em `readAuthorizationCallback` | Nada; avise o usuário |
|
|
260
|
+
| Código usado, expirado (60 s) ou divergente | `OAuthError` `invalid_grant` | Recomece o fluxo de autorização |
|
|
261
|
+
| Refresh token reutilizado ou expirado | `OAuthError` `invalid_grant` | A conexão inteira acabou; peça para reconectar |
|
|
262
|
+
| `client_id`/segredo errado, app desativado | `OAuthError` `invalid_client` | Corrija a configuração; repetir não resolve |
|
|
263
|
+
| Token expirado ou revogado | `ApiError` `401` | Renove; se falhar, peça para reconectar |
|
|
264
|
+
| Falta de permissão | `ApiError` `403` com `challenge.error === 'insufficient_scope'` | Reconecte pedindo `challenge.scope` |
|
|
265
|
+
| `403` sem challenge | `ApiError` `403` | Outro workspace, ou área que OAuth não alcança |
|
|
266
|
+
|
|
267
|
+
Refresh tokens **rotacionam**: cada renovação devolve um novo e aposenta o anterior, e reutilizar um
|
|
268
|
+
token aposentado encerra a conexão inteira. Persista o novo valor antes de usar a resposta, trate um
|
|
269
|
+
timeout como "talvez tenha funcionado" — releia o token guardado em vez de repetir às cegas — e
|
|
270
|
+
nunca renove a mesma conexão duas vezes em paralelo. Uma conexão dura 30 dias a partir da aprovação,
|
|
271
|
+
por mais que seja renovada, e os endpoints de autorização e token aceitam 50 requisições por minuto
|
|
272
|
+
por IP.
|
|
273
|
+
|
|
274
|
+
Assistentes de IA como Claude, Claude Code e ChatGPT se conectam à Assinafy pelas próprias
|
|
275
|
+
configurações de conector; seus usuários não precisam que você registre nada para eles.
|
|
276
|
+
|
|
277
|
+
## Configuração
|
|
278
|
+
|
|
279
|
+
| Opção | Tipo | Padrão | Descrição |
|
|
280
|
+
| --------------- | -------- | -------------------------------- | --------- |
|
|
281
|
+
| `apiKey` | string | — | Credencial preferida (enviada como `X-Api-Key`). |
|
|
282
|
+
| `token` | string | — | Token de acesso (enviado como `Authorization: Bearer`). Serve também para tokens OAuth. |
|
|
283
|
+
| `accountId` | string | — | ID padrão da conta / workspace. |
|
|
284
|
+
| `baseUrl` | string | `https://api.assinafy.com.br/v1` | Base absoluta da API, sem credenciais, query ou fragmento. Precisa ser `https`, salvo em loopback. |
|
|
285
|
+
| `webhookSecret` | string | — | Segredo HMAC opcional usado pelo `WebhookVerifier`; veja a [ressalva de contrato](docs/COMPATIBILITY.md#webhook-signature-verification-is-not-in-the-openapi-contract). |
|
|
286
|
+
| `timeout` | number | `30000` | Timeout da requisição, em milissegundos. |
|
|
287
|
+
| `maxRetries` | number | `2` | Retenta automaticamente respostas `429` elegíveis, respeitando `Retry-After`. `0` desativa. |
|
|
288
|
+
| `logger` | `Logger` | no-op | Logger opcional `{debug,info,warn,error}`. |
|
|
289
|
+
|
|
290
|
+
### Limite de requisições
|
|
291
|
+
|
|
292
|
+
Em um HTTP `429` o cliente retenta até `maxRetries` vezes, aguardando o `Retry-After` (ou
|
|
293
|
+
`X-Rate-Limit-Reset`) devolvido pelo servidor antes de cada tentativa. A repetição automática vale
|
|
294
|
+
apenas para `GET`, `HEAD`, `OPTIONS` e `DELETE`, que são seguros de repetir. `GET /sign` fica de
|
|
295
|
+
fora porque registra que o signatário visualizou o assignment. Escritas não são repetidas por
|
|
296
|
+
padrão. Um `Idempotency-Key` não vazio inscreve uma requisição customizada na repetição do SDK, mas
|
|
297
|
+
ele **não** faz parte do contrato OpenAPI atual: confirme antes que a rota alvo de fato deduplica
|
|
298
|
+
essa chave no servidor. Nenhum outro status é retentado.
|
|
299
|
+
|
|
300
|
+
### Fábricas
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// Fábrica posicional
|
|
304
|
+
const client = AssinafyClient.create('chave-de-api', 'id-da-conta');
|
|
305
|
+
|
|
306
|
+
// A partir de um objeto simples (aceita chaves snake_case ou camelCase)
|
|
163
307
|
const client = AssinafyClient.fromConfig({
|
|
164
308
|
api_key: process.env.ASSINAFY_API_KEY!,
|
|
165
309
|
account_id: process.env.ASSINAFY_ACCOUNT_ID!,
|
|
166
310
|
});
|
|
167
311
|
```
|
|
168
312
|
|
|
169
|
-
##
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
|
176
|
-
|
|
|
177
|
-
| `client.documents`
|
|
178
|
-
| `client.signers`
|
|
179
|
-
| `client.assignments`
|
|
180
|
-
| `client.templates`
|
|
181
|
-
| `client.tags`
|
|
182
|
-
| `client.workspaces`
|
|
183
|
-
| `client.webhooks`
|
|
184
|
-
| `client.fields`
|
|
185
|
-
| `client.
|
|
186
|
-
| `client.
|
|
187
|
-
| `client.
|
|
188
|
-
| `client.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
`virtual` assignment, so no page coordinates or paid notification channel are
|
|
203
|
-
required.
|
|
204
|
-
|
|
205
|
-
### 1. Upload the PDF
|
|
313
|
+
## Cobertura de endpoints
|
|
314
|
+
|
|
315
|
+
As 93 operações documentadas em https://api.assinafy.com.br/v1/docs estão cobertas. A tabela abaixo
|
|
316
|
+
é o resumo por recurso; o mapa detalhado por operação está em
|
|
317
|
+
[docs/API_COVERAGE.md](docs/API_COVERAGE.md).
|
|
318
|
+
|
|
319
|
+
| Recurso | Endpoints |
|
|
320
|
+
| --- | --- |
|
|
321
|
+
| `client.documents` | list, search, upload, details, get, rename, activities, waitUntilReady, download, thumbnail, downloadPage, statuses, delete, verify, createFromTemplate, estimateCostFromTemplate, getPublic, sendToken, listTags, replaceTags, addTags, detachTag, isFullySigned, getSigningProgress |
|
|
322
|
+
| `client.signers` | create, get, list, update, delete, findByEmail |
|
|
323
|
+
| `client.assignments` | list, create, estimateCost, resetExpiration, resendNotification, estimateResendCost, listWhatsAppNotifications |
|
|
324
|
+
| `client.templates` | create, list, get, update, delete, downloadPage |
|
|
325
|
+
| `client.tags` | list, create, update, delete |
|
|
326
|
+
| `client.workspaces` | create, list, get, update, delete, getTheme, downloadLogo, uploadLogo, deleteLogo, getStats |
|
|
327
|
+
| `client.webhooks` | register, get, inactivate, listEventTypes, listDispatches, retryDispatch |
|
|
328
|
+
| `client.fields` | create, list, get, update, delete, validate, validateMultiple, listTypes |
|
|
329
|
+
| `client.oauth` | **getProtectedResourceMetadata**, **getAuthorizationServerMetadata**, **createAuthorizationUrl**, **readAuthorizationCallback**, **exchangeCode**, **refreshToken**, **revokeToken**, **getUserInfo** |
|
|
330
|
+
| `client.auth` | getSocialLoginUrl, getSocialLoginCallbackUrl, login, socialLogin, linkSocialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword |
|
|
331
|
+
| `client.users` | getCurrent, getStats, getNotificationPreferences, updateNotificationPreferences |
|
|
332
|
+
| `client.signerDocuments` | getCurrent, list, search, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline |
|
|
333
|
+
| `client.webhookVerifier` | verify, extractEvent, getEventType, getEventData |
|
|
334
|
+
|
|
335
|
+
Todo wrapper HTTP tem tipos de requisição e resposta verificados pelo TypeScript e JSDoc por método,
|
|
336
|
+
cobrindo o payload de rede, o formato de retorno, validação, erros relevantes da API e um exemplo
|
|
337
|
+
copiável. As declarações acompanham o pacote.
|
|
338
|
+
|
|
339
|
+
## Ciclo de vida do documento
|
|
340
|
+
|
|
341
|
+
A integração normal tem uma fase do dono da conta, uma fase do signatário e uma fase final de
|
|
342
|
+
artefatos. O exemplo abaixo mantém todos os signatários no e-mail e usa um assignment `virtual`,
|
|
343
|
+
então não exige coordenadas de página nem canal pago de notificação.
|
|
344
|
+
|
|
345
|
+
### 1. Envie o PDF
|
|
206
346
|
|
|
207
347
|
```ts
|
|
208
|
-
const
|
|
348
|
+
const enviado = await client.documents.upload({ filePath: './contrato.pdf' });
|
|
209
349
|
```
|
|
210
350
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
`IDocumentUploadResponse`:
|
|
351
|
+
O corpo multipart oficial contém a parte `file`. O SDK também aceita um nome de exibição (usado como
|
|
352
|
+
nome de arquivo dessa parte) e uma parte JSON `metadata` opcional, para deploys que a aceitem. A
|
|
353
|
+
resposta de sucesso é `IDocumentUploadResponse`:
|
|
215
354
|
|
|
216
355
|
```ts
|
|
217
356
|
{
|
|
@@ -241,37 +380,36 @@ display-name override (used as that file part's filename) and an optional JSON
|
|
|
241
380
|
}
|
|
242
381
|
```
|
|
243
382
|
|
|
244
|
-
`DocumentStatus`
|
|
245
|
-
`
|
|
246
|
-
`
|
|
383
|
+
`DocumentStatus` cobre `uploading`, `uploaded`, `metadata_processing`, `metadata_ready`,
|
|
384
|
+
`pending_signature`, `expired`, `certificating`, `certificated`, `rejected_by_signer`,
|
|
385
|
+
`rejected_by_user` e `failed`.
|
|
247
386
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
`virtual` assignment may be created immediately.
|
|
387
|
+
O envio precisa ser um PDF de no máximo 25 MB e 2.000 páginas. O SDK confere extensão, tamanho e o
|
|
388
|
+
cabeçalho `%PDF-` antes de enviar. Um upload novo pode ter `pages` vazio até o processamento de
|
|
389
|
+
metadados terminar. Espere antes de criar um assignment `collect`, porque seus campos referenciam
|
|
390
|
+
IDs de páginas renderizadas; um assignment `virtual` pode ser criado imediatamente.
|
|
253
391
|
|
|
254
392
|
```ts
|
|
255
|
-
const
|
|
393
|
+
const preparado = await client.documents.waitUntilReady(enviado.id, {
|
|
256
394
|
maxWaitMs: 30_000,
|
|
257
395
|
pollIntervalMs: 2_000,
|
|
258
396
|
});
|
|
259
397
|
```
|
|
260
398
|
|
|
261
|
-
### 2.
|
|
399
|
+
### 2. Crie ou reaproveite os signatários por e-mail
|
|
262
400
|
|
|
263
401
|
```ts
|
|
264
|
-
const
|
|
265
|
-
full_name: '
|
|
266
|
-
email: '
|
|
402
|
+
const signatarioA = await client.signers.create({
|
|
403
|
+
full_name: 'João Silva',
|
|
404
|
+
email: 'joao@exemplo.com.br',
|
|
267
405
|
});
|
|
268
|
-
const
|
|
269
|
-
full_name: '
|
|
270
|
-
email: '
|
|
406
|
+
const signatarioB = await client.signers.create({
|
|
407
|
+
full_name: 'Maria Souza',
|
|
408
|
+
email: 'maria@exemplo.com.br',
|
|
271
409
|
});
|
|
272
410
|
```
|
|
273
411
|
|
|
274
|
-
|
|
412
|
+
O corpo de rede é `{ full_name, email }`. Cada resposta é um `ISigner`:
|
|
275
413
|
|
|
276
414
|
```ts
|
|
277
415
|
{
|
|
@@ -280,35 +418,34 @@ The wire body is `{ full_name, email }`. Each response is an `ISigner`:
|
|
|
280
418
|
full_name: string;
|
|
281
419
|
email: string | null;
|
|
282
420
|
whatsapp_phone_number?: string | null;
|
|
283
|
-
cpf?: string | null; //
|
|
421
|
+
cpf?: string | null; // tipo de compatibilidade; a API não devolve
|
|
284
422
|
has_accepted_terms?: boolean;
|
|
285
|
-
has_signature?: boolean; //
|
|
286
|
-
has_initial?: boolean; //
|
|
287
|
-
is_signature_reusable?: boolean; //
|
|
423
|
+
has_signature?: boolean; // só na resposta de signers/self
|
|
424
|
+
has_initial?: boolean; // só na resposta de signers/self
|
|
425
|
+
is_signature_reusable?: boolean; // só na resposta de signers/self
|
|
288
426
|
metadata?: Record<string, unknown>;
|
|
289
427
|
}
|
|
290
428
|
```
|
|
291
429
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
always creates a new signer.
|
|
430
|
+
Quando há e-mail, `signers.create()` primeiro procura esse endereço no workspace e reaproveita o
|
|
431
|
+
signatário correspondente; uma requisição só com nome ou só com telefone sempre cria um novo.
|
|
295
432
|
|
|
296
|
-
### 3.
|
|
433
|
+
### 3. Orce, depois peça as assinaturas
|
|
297
434
|
|
|
298
|
-
|
|
435
|
+
A estimativa de custo recebe descritores de canal, não IDs de signatário:
|
|
299
436
|
|
|
300
437
|
```ts
|
|
301
|
-
const
|
|
438
|
+
const estimativa = await client.assignments.estimateCost(enviado.id, {
|
|
302
439
|
method: 'virtual',
|
|
303
|
-
signers: [{}, {}], // `{}`
|
|
440
|
+
signers: [{}, {}], // `{}` seleciona Email para cada signatário
|
|
304
441
|
});
|
|
305
442
|
|
|
306
|
-
if (!
|
|
307
|
-
throw new Error(
|
|
443
|
+
if (!estimativa.has_sufficient_resources) {
|
|
444
|
+
throw new Error(estimativa.blocking_reason ?? estimativa.message ?? 'Recursos insuficientes');
|
|
308
445
|
}
|
|
309
446
|
```
|
|
310
447
|
|
|
311
|
-
|
|
448
|
+
A resposta é `ICostEstimate`:
|
|
312
449
|
|
|
313
450
|
```ts
|
|
314
451
|
{
|
|
@@ -326,21 +463,21 @@ The response is `ICostEstimate`:
|
|
|
326
463
|
}
|
|
327
464
|
```
|
|
328
465
|
|
|
329
|
-
|
|
466
|
+
Crie o assignment por e-mail só depois de aceitar essa estimativa:
|
|
330
467
|
|
|
331
468
|
```ts
|
|
332
|
-
const assignment = await client.assignments.create(
|
|
469
|
+
const assignment = await client.assignments.create(enviado.id, {
|
|
333
470
|
method: 'virtual',
|
|
334
471
|
signers: [
|
|
335
|
-
{ id:
|
|
336
|
-
{ id:
|
|
472
|
+
{ id: signatarioA.id, verification_method: 'Email', notification_methods: ['Email'] },
|
|
473
|
+
{ id: signatarioB.id, verification_method: 'Email', notification_methods: ['Email'] },
|
|
337
474
|
],
|
|
338
|
-
message: '
|
|
475
|
+
message: 'Por favor, revise e assine',
|
|
339
476
|
expires_at: '2027-12-31T23:59:00Z',
|
|
340
477
|
});
|
|
341
478
|
```
|
|
342
479
|
|
|
343
|
-
|
|
480
|
+
A requisição devolve um `IAssignment`:
|
|
344
481
|
|
|
345
482
|
```ts
|
|
346
483
|
{
|
|
@@ -363,250 +500,232 @@ The request returns an `IAssignment`:
|
|
|
363
500
|
}
|
|
364
501
|
```
|
|
365
502
|
|
|
366
|
-
|
|
367
|
-
secrets.
|
|
503
|
+
As URLs e as mensagens entregues contêm credenciais do signatário; trate-as como segredo.
|
|
368
504
|
|
|
369
|
-
### 4.
|
|
505
|
+
### 4. Conclua o fluxo do signatário por e-mail
|
|
370
506
|
|
|
371
|
-
Assinafy
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
log them.
|
|
507
|
+
A Assinafy envia a cada signatário um link com o código de acesso dele e manda o código de
|
|
508
|
+
verificação de uso único pelo canal escolhido. Nenhum dos dois valores é devolvido como campo
|
|
509
|
+
autônomo do lado do dono da conta. Um portal de assinatura próprio precisa obter os dois pelo fluxo
|
|
510
|
+
de entrega ao signatário; não os fabrique nem os registre em log.
|
|
376
511
|
|
|
377
512
|
```ts
|
|
378
|
-
//
|
|
379
|
-
const
|
|
380
|
-
baseUrl,
|
|
381
|
-
});
|
|
513
|
+
// Cliente do signatário: nenhuma credencial da conta é necessária nem enviada.
|
|
514
|
+
const clienteSignatario = new AssinafyClient({ baseUrl });
|
|
382
515
|
|
|
383
|
-
const self = await
|
|
516
|
+
const self = await clienteSignatario.signerDocuments.self(accessCode); // ISignerSelf
|
|
384
517
|
|
|
385
518
|
// Query: signer-access-code=<accessCode>
|
|
386
|
-
//
|
|
387
|
-
await
|
|
519
|
+
// Corpo: { 'verification-code': '<código de seis dígitos>' }
|
|
520
|
+
await clienteSignatario.signerDocuments.verifyEmail({
|
|
388
521
|
signerAccessCode: accessCode,
|
|
389
522
|
verificationCode,
|
|
390
523
|
}); // Promise<void>
|
|
391
524
|
|
|
392
|
-
const
|
|
393
|
-
|
|
525
|
+
const confirmado = await clienteSignatario.signerDocuments.confirmData(
|
|
526
|
+
enviado.id,
|
|
394
527
|
accessCode,
|
|
395
528
|
{ full_name: self.full_name, email: self.email ?? undefined },
|
|
396
529
|
); // ISigner
|
|
397
530
|
|
|
398
|
-
const
|
|
399
|
-
// `getAssignment`
|
|
400
|
-
//
|
|
531
|
+
const assinavel = await clienteSignatario.signerDocuments.getAssignment(accessCode, true);
|
|
532
|
+
// `getAssignment` devolve IDocumentDetailsResponse e registra que o signatário
|
|
533
|
+
// visualizou o assignment. Não use como simples healthcheck.
|
|
401
534
|
|
|
402
|
-
await
|
|
403
|
-
//
|
|
535
|
+
await clienteSignatario.signerDocuments.signMultiple([assinavel.id], accessCode);
|
|
536
|
+
// Corpo de rede: { document_ids: [assinavel.id] }; o reconhecimento não traz dados.
|
|
404
537
|
```
|
|
405
538
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
can sign in parallel.
|
|
539
|
+
Repita esta fase separadamente para cada signatário, com o código de acesso e o código de uso único
|
|
540
|
+
dele. Os dois signatários deste exemplo compartilham o passo padrão e podem assinar em paralelo.
|
|
409
541
|
|
|
410
|
-
`signMultiple`
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
part of this SDK's current 89-operation surface.
|
|
542
|
+
`signMultiple` serve apenas a assignments `virtual`. Para `collect`, leia `assinavel.assignment.items`
|
|
543
|
+
e chame `sign(documentId, assignmentId, accessCode, entries)` com um array não vazio de
|
|
544
|
+
`{ itemId, fieldId, pageId, value }`. Um signatário virtual precisa confirmar os dados antes de
|
|
545
|
+
assinar. Um signatário `DigitalCertificate` não pode usar `sign`; esse ramo passa pelo fluxo de
|
|
546
|
+
certificado da Assinafy, descrito em
|
|
547
|
+
[Certificado digital ICP-Brasil](#certificado-digital-icp-brasil).
|
|
417
548
|
|
|
418
|
-
### 5.
|
|
549
|
+
### 5. Acompanhe a conclusão e baixe os artefatos
|
|
419
550
|
|
|
420
|
-
|
|
421
|
-
`documents.details(documentId)`
|
|
422
|
-
|
|
423
|
-
idempotency key. Once complete:
|
|
551
|
+
Assine o evento `document_ready` para saber da conclusão por evento, ou consulte
|
|
552
|
+
`documents.details(documentId)` até `status === 'certificated'`. As entregas de webhook podem se
|
|
553
|
+
repetir — use o `id` numérico como chave de idempotência. Concluído:
|
|
424
554
|
|
|
425
555
|
```ts
|
|
426
|
-
const
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
const
|
|
556
|
+
const documentoFinal = await client.documents.details(enviado.id);
|
|
557
|
+
const pdfAssinado = await client.documents.download(enviado.id, 'certificated');
|
|
558
|
+
const paginaCertificado = await client.documents.download(enviado.id, 'certificate-page');
|
|
559
|
+
const pacoteZip = await client.documents.download(enviado.id, 'bundle');
|
|
430
560
|
|
|
431
|
-
//
|
|
432
|
-
const
|
|
561
|
+
// Valide um hash de assinatura Assinafy que o seu fluxo tenha extraído.
|
|
562
|
+
const validacao = await client.documents.verify(documentSignatureHash);
|
|
433
563
|
```
|
|
434
564
|
|
|
435
|
-
`original`, `certificated
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
token belongs to the document creator.
|
|
565
|
+
`original`, `certificated` e `certificate-page` são PDFs. `bundle` é um ZIP com esses três artefatos
|
|
566
|
+
e também o `pades` quando o documento teve signatário por certificado ICP-Brasil. O PDF `pades`
|
|
567
|
+
existe apenas nesses documentos. Um artefato pode devolver `404` antes de terminar de ser gerado.
|
|
568
|
+
`decline_reason` só aparece nos detalhes do documento quando o token pertence a quem criou o
|
|
569
|
+
documento.
|
|
441
570
|
|
|
442
|
-
##
|
|
571
|
+
## Referência de recursos
|
|
443
572
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
573
|
+
A maioria dos métodos com escopo de conta aceita um `accountId` opcional que sobrepõe o padrão do
|
|
574
|
+
cliente. Os métodos de workspace `get`, `update`, `delete`, identidade visual e estatísticas sempre
|
|
575
|
+
exigem um ID de conta explícito.
|
|
447
576
|
|
|
448
|
-
###
|
|
577
|
+
### Documentos
|
|
449
578
|
|
|
450
579
|
```ts
|
|
451
|
-
//
|
|
580
|
+
// Envio a partir de um caminho de arquivo (recomendado)
|
|
452
581
|
const doc = await client.documents.upload(
|
|
453
|
-
{ filePath: './
|
|
454
|
-
{ name: '
|
|
582
|
+
{ filePath: './contrato.pdf' },
|
|
583
|
+
{ name: 'Contrato de prestação', metadata: { tipo: 'servico' } },
|
|
455
584
|
);
|
|
456
|
-
// `name`
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
585
|
+
// `name` e `metadata` são partes multipart de compatibilidade, fora do schema
|
|
586
|
+
// oficial (que só define o arquivo). `name` é opcional e assume o nome do
|
|
587
|
+
// arquivo. A API deriva o nome de exibição do arquivo enviado e acrescenta
|
|
588
|
+
// `.pdf` quando falta, então o documento acima é salvo como
|
|
589
|
+
// 'Contrato de prestação.pdf'. A API translitera acentos
|
|
590
|
+
// ('Contrato de Serviço' → 'Contrato de Servico.pdf').
|
|
462
591
|
// → {
|
|
463
592
|
// resource: 'document', id: '1031…', account_id: '102d…', template_id: null,
|
|
464
|
-
// name: '
|
|
593
|
+
// name: 'Contrato de prestacao.pdf', status: 'uploaded',
|
|
465
594
|
// artifacts: { original: 'https://…/download/original' },
|
|
466
595
|
// signing_url: 'https://app…/sign/1031…',
|
|
467
|
-
// pages: [], //
|
|
596
|
+
// pages: [], // preenchido quando o status chega a `metadata_ready`
|
|
468
597
|
// tags: [], is_closed: false, created_at: '2026-…', updated_at: '2026-…'
|
|
469
598
|
// }
|
|
470
599
|
|
|
471
|
-
// …
|
|
472
|
-
await client.documents.upload({ buffer, fileName: '
|
|
600
|
+
// …ou a partir de um Buffer já em memória
|
|
601
|
+
await client.documents.upload({ buffer, fileName: 'contrato.pdf' });
|
|
473
602
|
|
|
474
|
-
//
|
|
603
|
+
// Listagem → { data: IDocumentListItem[], meta?: { current_page, per_page, total, last_page } }
|
|
475
604
|
const { data, meta } = await client.documents.list({ page: 1, 'per-page': 20, sort: 'updated_at' });
|
|
476
605
|
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
const
|
|
606
|
+
// `search` é a alternativa leve a `list`: mesmo formato de item, mas a API não
|
|
607
|
+
// expande `assignment`/`pages`. Prefira-a para buscar por nome.
|
|
608
|
+
const achados = await client.documents.search({ search: 'contrato', status: 'pending_signature', 'per-page': 20 });
|
|
480
609
|
|
|
481
610
|
await client.documents.details(doc.id);
|
|
482
611
|
await client.documents.activities(doc.id);
|
|
483
612
|
await client.documents.waitUntilReady(doc.id, { maxWaitMs: 30_000 });
|
|
484
613
|
|
|
485
|
-
//
|
|
486
|
-
// `metadata_processing`,
|
|
487
|
-
// (
|
|
488
|
-
await client.documents.rename(doc.id, '
|
|
614
|
+
// Renomear. A API responde 400 enquanto o documento está em
|
|
615
|
+
// `metadata_processing`, então chame waitUntilReady() antes em um upload novo.
|
|
616
|
+
// (Passar `name` ao upload() evita tanto a ida extra quanto a corrida.)
|
|
617
|
+
await client.documents.rename(doc.id, 'Contrato assinado.pdf');
|
|
489
618
|
|
|
490
|
-
await client.documents.download(doc.id, 'certificated');
|
|
619
|
+
await client.documents.download(doc.id, 'certificated'); // PDF assinado
|
|
491
620
|
await client.documents.download(doc.id, 'certificate-page');
|
|
492
621
|
await client.documents.download(doc.id, 'bundle'); // ZIP
|
|
493
|
-
// `pades`
|
|
622
|
+
// `pades` existe apenas quando algum signatário usou DigitalCertificate.
|
|
494
623
|
await client.documents.download(doc.id, 'pades');
|
|
495
624
|
await client.documents.thumbnail(doc.id);
|
|
496
625
|
await client.documents.downloadPage(doc.id, pageId);
|
|
497
626
|
|
|
498
|
-
await client.documents.statuses(); //
|
|
627
|
+
await client.documents.statuses(); // todos os códigos de status + flag deletable
|
|
499
628
|
await client.documents.isFullySigned(doc.id);
|
|
500
629
|
await client.documents.getSigningProgress(doc.id);
|
|
501
630
|
await client.documents.delete(doc.id);
|
|
502
631
|
|
|
503
|
-
//
|
|
632
|
+
// Verifique um documento assinado pelo hash de assinatura Assinafy
|
|
504
633
|
await client.documents.verify('FE32EDDADE7CBDDCBB934E7402047450B0E59C02');
|
|
505
634
|
|
|
506
|
-
//
|
|
635
|
+
// Endpoints públicos (sem autenticação)
|
|
507
636
|
await client.documents.getPublic(doc.id);
|
|
508
|
-
//
|
|
509
|
-
await client.documents.sendToken(doc.id, '
|
|
637
|
+
// Corpo oficial: { email: 'maria@exemplo.com.br' }
|
|
638
|
+
await client.documents.sendToken(doc.id, 'maria@exemplo.com.br');
|
|
510
639
|
|
|
511
|
-
//
|
|
640
|
+
// Sobrecarga explícita de compatibilidade para deploys mais antigos:
|
|
512
641
|
// { recipient: '+5548999990000', channel: 'whatsapp' }
|
|
513
642
|
await client.documents.sendToken(doc.id, '+5548999990000', 'whatsapp');
|
|
514
643
|
|
|
515
|
-
//
|
|
516
|
-
const
|
|
517
|
-
const
|
|
518
|
-
const
|
|
644
|
+
// O contrato OpenAPI atual exige IDs de tags já existentes.
|
|
645
|
+
const tagContratos = await client.tags.create({ name: 'Contratos' });
|
|
646
|
+
const tagTrimestre = await client.tags.create({ name: '2026-T1' });
|
|
647
|
+
const tagUrgente = await client.tags.create({ name: 'Urgente' });
|
|
519
648
|
await client.documents.listTags(doc.id);
|
|
520
|
-
await client.documents.replaceTags(doc.id, [
|
|
521
|
-
await client.documents.addTags(doc.id, [
|
|
522
|
-
await client.documents.detachTag(doc.id,
|
|
649
|
+
await client.documents.replaceTags(doc.id, [tagContratos.id, tagTrimestre.id]); // [] desanexa tudo
|
|
650
|
+
await client.documents.addTags(doc.id, [tagUrgente.id]); // acrescenta
|
|
651
|
+
await client.documents.detachTag(doc.id, tagUrgente.id); // → { detached: true }
|
|
523
652
|
```
|
|
524
653
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
to 2,000 pages.
|
|
654
|
+
Os uploads são validados localmente: só arquivos `.pdf` de até 25 MB cujos bytes começam com o
|
|
655
|
+
cabeçalho mágico (`%PDF-`) são aceitos. A API também limita o documento a 2.000 páginas.
|
|
528
656
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
and `certificate-page`, plus `pades` when available.
|
|
657
|
+
As URLs de página e de artefato embutidas nas respostas JSON continuam exigindo a mesma autenticação
|
|
658
|
+
de conta das operações de download. Prefira `documents.downloadPage()` e `documents.download()`,
|
|
659
|
+
que aplicam a credencial e devolvem um `Buffer`. `bundle` contém `original`, `certificated` e
|
|
660
|
+
`certificate-page`, mais o `pades` quando existir.
|
|
534
661
|
|
|
535
|
-
|
|
536
|
-
`X-Pagination
|
|
537
|
-
both are silent rather than errors:
|
|
662
|
+
As listagens devolvem `{ data, meta }`, com `meta` preenchido a partir dos headers
|
|
663
|
+
`X-Pagination-*`. Dois comportamentos da API merecem atenção porque são silenciosos, não erros:
|
|
538
664
|
|
|
539
|
-
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
- `per-page`
|
|
543
|
-
|
|
544
|
-
`
|
|
545
|
-
the value you asked for.
|
|
665
|
+
- Só o `per-page` com hífen é lido. `per_page` é aceito e ignorado, caindo para 20 linhas — por isso
|
|
666
|
+
o SDK reescreve `per_page` como `per-page` em toda listagem, e um `per-page` explícito vence
|
|
667
|
+
quando os dois são enviados.
|
|
668
|
+
- `per-page` é limitado a **50**. Pedir 100 devolve 50 linhas com `200`. A constante exportada
|
|
669
|
+
`MAX_LIST_PAGE_SIZE` é esse teto; pagine com `page` em vez de pedir páginas maiores, e confie no
|
|
670
|
+
`meta.per_page` mais do que no valor pedido.
|
|
546
671
|
|
|
547
|
-
###
|
|
672
|
+
### Signatários
|
|
548
673
|
|
|
549
674
|
```ts
|
|
550
675
|
await client.signers.create({
|
|
551
|
-
full_name: '
|
|
552
|
-
email: '
|
|
553
|
-
cpf: '123.456.789-00', //
|
|
676
|
+
full_name: 'João Silva',
|
|
677
|
+
email: 'joao@exemplo.com.br',
|
|
678
|
+
cpf: '123.456.789-00', // entrada de compatibilidade; não dígitos são removidos
|
|
554
679
|
});
|
|
555
|
-
// → { id: '19e6…', full_name: '
|
|
680
|
+
// → { id: '19e6…', full_name: 'João Silva', email: 'joao@exemplo.com.br',
|
|
556
681
|
// whatsapp_phone_number: null, has_accepted_terms: false }
|
|
557
|
-
// (
|
|
682
|
+
// (observação: `cpf` é aceito na entrada, mas a API nunca o devolve)
|
|
558
683
|
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
await client.signers.create({ full_name: '
|
|
562
|
-
|
|
563
|
-
await client.signers.create({
|
|
564
|
-
full_name: 'Jane Doe',
|
|
565
|
-
email: 'jane@example.com',
|
|
566
|
-
});
|
|
684
|
+
// Ambos os contatos são opcionais. Um signatário só com nome não pode ser
|
|
685
|
+
// notificado até que um contato seja adicionado.
|
|
686
|
+
await client.signers.create({ full_name: 'Contato Pendente' });
|
|
567
687
|
|
|
568
688
|
await client.signers.get(signerId);
|
|
569
|
-
await client.signers.list({ page: 1, 'per-page': 50, search: '
|
|
689
|
+
await client.signers.list({ page: 1, 'per-page': 50, search: 'joao' });
|
|
570
690
|
await client.signers.update(signerId, {
|
|
571
|
-
full_name: '
|
|
572
|
-
government_id: '390.533.447-05', //
|
|
691
|
+
full_name: 'João da Silva',
|
|
692
|
+
government_id: '390.533.447-05', // campo oficial de atualização; enviado só com dígitos
|
|
573
693
|
});
|
|
574
694
|
await client.signers.delete(signerId);
|
|
575
695
|
|
|
576
|
-
const
|
|
696
|
+
const existente = await client.signers.findByEmail('joao@exemplo.com.br');
|
|
577
697
|
```
|
|
578
698
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
[Paid signing branches](#paid-signing-branches) for phone-only signers.
|
|
699
|
+
Quando um `email` é informado, `signers.create()` é idempotente por e-mail: reaproveita o signatário
|
|
700
|
+
existente com o mesmo endereço no workspace. Signatários sem e-mail são sempre criados do zero. Veja
|
|
701
|
+
[Ramos pagos de assinatura](#ramos-pagos-de-assinatura) para signatários só com telefone.
|
|
583
702
|
|
|
584
703
|
### Assignments
|
|
585
704
|
|
|
586
705
|
```ts
|
|
587
|
-
//
|
|
706
|
+
// Lista todos os assignments do workspace.
|
|
588
707
|
// → { data: IAssignment[], meta?: { current_page, per_page, total, last_page } }
|
|
589
708
|
const { data, meta } = await client.assignments.list({ page: 1, 'per-page': 20 });
|
|
590
709
|
|
|
591
|
-
//
|
|
710
|
+
// Signatários podem ser ids ou objetos — o SDK normaliza para o formato da API.
|
|
592
711
|
await client.assignments.create(documentId, {
|
|
593
712
|
method: 'virtual',
|
|
594
713
|
signers: ['signer-1', 'signer-2'],
|
|
595
|
-
message: '
|
|
714
|
+
message: 'Por favor, revise e assine',
|
|
596
715
|
expires_at: '2027-12-31T23:59:00Z',
|
|
597
|
-
copy_receivers: ['
|
|
716
|
+
copy_receivers: ['id-do-signatario-em-copia'],
|
|
598
717
|
});
|
|
599
718
|
|
|
600
|
-
//
|
|
719
|
+
// Assinatura sequencial: `step` controla a ordem (paralelo dentro de um passo).
|
|
601
720
|
await client.assignments.create(documentId, {
|
|
602
721
|
method: 'virtual',
|
|
603
722
|
signers: [
|
|
604
723
|
{ id: 'signer-1', step: 1 },
|
|
605
|
-
{ id: 'signer-2', step: 2 }, //
|
|
724
|
+
{ id: 'signer-2', step: 2 }, // notificado só depois que o passo 1 terminar
|
|
606
725
|
],
|
|
607
726
|
});
|
|
608
727
|
|
|
609
|
-
//
|
|
728
|
+
// Campos de `collect` usam pixels da imagem de página a 150 DPI, medidos do canto superior esquerdo.
|
|
610
729
|
await client.assignments.create(documentId, {
|
|
611
730
|
method: 'collect',
|
|
612
731
|
signers: [{ id: signerId }],
|
|
@@ -623,8 +742,8 @@ await client.assignments.create(documentId, {
|
|
|
623
742
|
}],
|
|
624
743
|
});
|
|
625
744
|
|
|
626
|
-
//
|
|
627
|
-
await client.assignments.estimateCost(documentId, { signers: [{}] }); //
|
|
745
|
+
// Estimativa de custo (o endpoint orça descritores de canal, não IDs) → ICostEstimate
|
|
746
|
+
await client.assignments.estimateCost(documentId, { signers: [{}] }); // Email padrão
|
|
628
747
|
// → {
|
|
629
748
|
// documents: 1, credits: 0, needs_extra_document: false, extra_document_cost: 0,
|
|
630
749
|
// total_credits: 0, breakdown: [], document_balance: 67, credit_balance: 0,
|
|
@@ -632,88 +751,108 @@ await client.assignments.estimateCost(documentId, { signers: [{}] }); // default
|
|
|
632
751
|
// }
|
|
633
752
|
|
|
634
753
|
await client.assignments.resetExpiration(documentId, assignmentId, '2027-06-30T00:00:00Z');
|
|
635
|
-
//
|
|
636
|
-
//
|
|
754
|
+
// Só compatibilidade: a requisição publicada exige uma data-hora.
|
|
755
|
+
// Confirme o suporte do destino antes de usar `null` para limpar a expiração.
|
|
637
756
|
await client.assignments.resetExpiration(documentId, assignmentId, null);
|
|
638
757
|
|
|
639
758
|
await client.assignments.resendNotification(documentId, assignmentId, signerId);
|
|
640
759
|
// → { is_sent: true, document_id: '…', signer_id: '…' }
|
|
641
760
|
|
|
642
|
-
const
|
|
643
|
-
//
|
|
644
|
-
// IResendCostEstimate
|
|
645
|
-
//
|
|
761
|
+
const custoReenvio = await client.assignments.estimateResendCost(documentId, assignmentId, signerId);
|
|
762
|
+
// Resposta oficial: ICostEstimate. Deploys antigos podem devolver o ramo compacto
|
|
763
|
+
// IResendCostEstimate, com `total` e `has_sufficient_credits`; estreite com
|
|
764
|
+
// `'total_credits' in custoReenvio` antes de ler campos específicos de um ramo.
|
|
646
765
|
```
|
|
647
766
|
|
|
648
|
-
|
|
767
|
+
A resposta de `create` é um `IAssignment`: `{ id, method, signers: [...],
|
|
649
768
|
items: [{ display_settings, ... }], signing_urls: [{ signer_id, url }], … }`.
|
|
650
769
|
|
|
651
|
-
|
|
770
|
+
Por compatibilidade, o SDK também aceita os payloads antigos `signer_ids` e `signerIds` e os
|
|
771
|
+
reescreve no formato atual `signers: [{ id }]` esperado pela API.
|
|
652
772
|
|
|
653
|
-
**
|
|
773
|
+
**Cancelar um pedido de assinatura.** A Assinafy não tem endpoint de cancelamento do lado do
|
|
774
|
+
workspace. Para interromper um pedido pendente, apague o documento (quando o status permitir) ou
|
|
775
|
+
peça que o signatário recuse:
|
|
654
776
|
|
|
655
777
|
```ts
|
|
656
|
-
await client.documents.delete(documentId);
|
|
657
|
-
await client.signerDocuments.decline(documentId, assignmentId, accessCode, '
|
|
778
|
+
await client.documents.delete(documentId); // lado do workspace
|
|
779
|
+
await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'Não é mais necessário'); // lado do signatário
|
|
658
780
|
```
|
|
659
781
|
|
|
660
|
-
###
|
|
782
|
+
### Ramos pagos de assinatura
|
|
783
|
+
|
|
784
|
+
Mantenha o fluxo por e-mail como padrão. Habilite os ramos abaixo apenas depois de confirmar que a
|
|
785
|
+
conta tem o plano ou recurso necessário e que a estimativa de custo é aceitável.
|
|
786
|
+
|
|
787
|
+
O método de verificação e o de notificação são **acoplados**: envie um, os dois ou nenhum — o lado
|
|
788
|
+
que faltar é inferido. Sem nenhum dos dois, ambos assumem `Email`.
|
|
789
|
+
|
|
790
|
+
| Verificação | Como o signatário prova quem é | Notificação permitida | Custo por signatário |
|
|
791
|
+
| --- | --- | --- | --- |
|
|
792
|
+
| `Email` *(padrão)* | Código de uso único (OTP) por e-mail | `Email` | Gratuito |
|
|
793
|
+
| `Whatsapp` | Código de uso único (OTP) por WhatsApp | `Whatsapp` | 0,45 crédito (a notificação), só em planos pagos |
|
|
794
|
+
| `DigitalCertificate` | O signatário assina com o **próprio certificado ICP-Brasil — A1 ou A3 —** pela extensão Web PKI, gerando uma assinatura **PAdES qualificada** | `Email` **ou** `Whatsapp` | 2 créditos + a notificação |
|
|
661
795
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
acceptable.
|
|
796
|
+
Apenas um método de notificação por signatário. O SDK recusa uma combinação inválida antes da
|
|
797
|
+
requisição; a API responderia `400`.
|
|
665
798
|
|
|
666
|
-
####
|
|
799
|
+
#### Verificação e notificação por WhatsApp
|
|
667
800
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
then request the `Whatsapp` channel explicitly:
|
|
801
|
+
Disponível apenas em assinaturas pagas, a 0,45 crédito por notificação. Crie um signatário só com
|
|
802
|
+
telefone (ou adicione telefone a um existente) e peça o canal `Whatsapp` explicitamente:
|
|
671
803
|
|
|
672
804
|
```ts
|
|
673
|
-
const
|
|
674
|
-
full_name: 'Mobile
|
|
805
|
+
const signatarioTelefone = await client.signers.create({
|
|
806
|
+
full_name: 'Signatário Mobile',
|
|
675
807
|
whatsapp_phone_number: '+5511999990000',
|
|
676
808
|
});
|
|
677
809
|
|
|
678
|
-
const
|
|
810
|
+
const custoWhatsapp = await client.assignments.estimateCost(documentId, {
|
|
679
811
|
method: 'virtual',
|
|
680
812
|
signers: [{ verification_method: 'Whatsapp', notification_methods: ['Whatsapp'] }],
|
|
681
813
|
});
|
|
682
814
|
|
|
683
|
-
const
|
|
815
|
+
const assignmentWhatsapp = await client.assignments.create(documentId, {
|
|
684
816
|
method: 'virtual',
|
|
685
817
|
signers: [{
|
|
686
|
-
id:
|
|
818
|
+
id: signatarioTelefone.id,
|
|
687
819
|
verification_method: 'Whatsapp',
|
|
688
820
|
notification_methods: ['Whatsapp'],
|
|
689
821
|
}],
|
|
690
822
|
});
|
|
691
823
|
|
|
692
|
-
const
|
|
824
|
+
const avisos = await client.assignments.listWhatsAppNotifications(
|
|
693
825
|
documentId,
|
|
694
|
-
|
|
826
|
+
assignmentWhatsapp.id,
|
|
695
827
|
);
|
|
696
828
|
// IWhatsAppNotification[]:
|
|
697
829
|
// [{ sent_at, header, body, buttons: [{ text, url? }], phone_number, signer_id }]
|
|
698
830
|
```
|
|
699
831
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
832
|
+
O helper de alto nível escolhe esse ramo pago automaticamente para um signatário que tenha telefone
|
|
833
|
+
e não tenha e-mail. As URLs dos botões podem conter credenciais do signatário — não registre em log
|
|
834
|
+
nem encaminhe fora do fluxo de assinatura.
|
|
835
|
+
|
|
836
|
+
#### Certificado digital ICP-Brasil
|
|
703
837
|
|
|
704
|
-
|
|
838
|
+
`DigitalCertificate` exige o recurso **Certificado Digital** na conta (planos Standard e Pro), CPF ou
|
|
839
|
+
CNPJ em `government_id` do signatário, e exatamente **um signatário por certificado naquele passo de
|
|
840
|
+
assinatura**. Custa 2 créditos por signatário, além do custo da notificação escolhida.
|
|
705
841
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
842
|
+
**A1 e A3 são mídias de certificado**, escolhidas pelo próprio signatário no navegador na hora de
|
|
843
|
+
assinar: A1 fica em software (um arquivo na máquina) e A3 em hardware (token ou cartão). A API
|
|
844
|
+
modela as duas com o único valor `DigitalCertificate` — não existe campo `A1`/`A3` a enviar, e a
|
|
845
|
+
assinatura PAdES resultante é qualificada nos dois casos.
|
|
846
|
+
|
|
847
|
+
Um CPF exige o certificado daquela pessoa (e-CPF, ou e-CNPJ que a nomeie como representante legal);
|
|
848
|
+
um CNPJ exige um e-CNPJ da empresa, de qualquer um dos seus representantes.
|
|
710
849
|
|
|
711
850
|
```ts
|
|
712
|
-
const
|
|
851
|
+
const signatarioCertificado = await client.signers.update(signerId, {
|
|
713
852
|
government_id: '390.533.447-05',
|
|
714
853
|
});
|
|
715
854
|
|
|
716
|
-
const
|
|
855
|
+
const custoCertificado = await client.assignments.estimateCost(documentId, {
|
|
717
856
|
method: 'virtual',
|
|
718
857
|
signers: [{ verification_method: 'DigitalCertificate', notification_methods: ['Email'] }],
|
|
719
858
|
});
|
|
@@ -721,7 +860,7 @@ const certificateCost = await client.assignments.estimateCost(documentId, {
|
|
|
721
860
|
await client.assignments.create(documentId, {
|
|
722
861
|
method: 'virtual',
|
|
723
862
|
signers: [{
|
|
724
|
-
id:
|
|
863
|
+
id: signatarioCertificado.id,
|
|
725
864
|
step: 1,
|
|
726
865
|
verification_method: 'DigitalCertificate',
|
|
727
866
|
notification_methods: ['Email'],
|
|
@@ -729,27 +868,37 @@ await client.assignments.create(documentId, {
|
|
|
729
868
|
});
|
|
730
869
|
```
|
|
731
870
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
871
|
+
Antes de abrir o assignment, o signatário precisa confirmar os dados de identidade e aceitar os
|
|
872
|
+
termos, com `confirmData(..., { has_accepted_terms: true })` ou `acceptTerms()`.
|
|
873
|
+
|
|
874
|
+
O endpoint comum `sign()` **rejeita** signatários por certificado: a assinatura deles é produzida por
|
|
875
|
+
um handshake de dois passos com a extensão Web PKI, pela integração de navegador da Assinafy.
|
|
876
|
+
|
|
877
|
+
```
|
|
878
|
+
POST /v1/signers/certificate/start → data.token (token da operação Web PKI)
|
|
879
|
+
↓ o navegador assina o token com o certificado do signatário
|
|
880
|
+
POST /v1/signers/certificate/complete → data.signerName
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
> Essas duas rotas são extensões implantadas **somente em produção**: o sandbox não as expõe e elas
|
|
884
|
+
> não constam do documento OpenAPI publicado.
|
|
885
|
+
|
|
886
|
+
Concluído o fluxo, `documents.download(documentId, 'pades')` devolve o artefato PAdES qualificado.
|
|
737
887
|
|
|
738
888
|
### Templates
|
|
739
889
|
|
|
740
|
-
`templates.list()`
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
[
|
|
744
|
-
|
|
745
|
-
`template.status.toLowerCase()` when branching on it.
|
|
890
|
+
`templates.list()` faz parte do documento OpenAPI atual. Integrações existentes também podem usar
|
|
891
|
+
cinco rotas de gestão de templates — `create`, `get`, `update`, `delete` e `downloadPage` — ausentes
|
|
892
|
+
daquele documento; veja as
|
|
893
|
+
[notas de compatibilidade](docs/COMPATIBILITY.md#template-management-extensions). A caixa do status
|
|
894
|
+
de template varia por deploy; normalize com `template.status.toLowerCase()` antes de comparar.
|
|
746
895
|
|
|
747
896
|
```ts
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
const
|
|
751
|
-
{ filePath: './nda.pdf' }, //
|
|
752
|
-
{ name: 'NDA
|
|
897
|
+
// Cria um template enviando um PDF (multipart). O template começa enviado e
|
|
898
|
+
// fica pronto quando suas páginas são processadas.
|
|
899
|
+
const criado = await client.templates.create(
|
|
900
|
+
{ filePath: './nda.pdf' }, // ou { buffer, fileName: 'nda.pdf' }
|
|
901
|
+
{ name: 'Template de NDA' },
|
|
753
902
|
);
|
|
754
903
|
// →
|
|
755
904
|
// {
|
|
@@ -760,27 +909,27 @@ const created = await client.templates.create(
|
|
|
760
909
|
// }
|
|
761
910
|
|
|
762
911
|
const { data, meta } = await client.templates.list({ search: 'NDA', 'per-page': 20 });
|
|
763
|
-
const template = await client.templates.get(
|
|
764
|
-
await client.templates.update(
|
|
765
|
-
const
|
|
766
|
-
if (
|
|
767
|
-
await client.templates.delete(
|
|
768
|
-
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
const
|
|
772
|
-
const
|
|
773
|
-
(
|
|
774
|
-
&&
|
|
912
|
+
const template = await client.templates.get(criado.id); // inclui pages[] + default_document_tags
|
|
913
|
+
await client.templates.update(criado.id, { name: 'NDA v2', message: 'Por favor, assine' });
|
|
914
|
+
const primeiraPagina = template.pages?.[0];
|
|
915
|
+
if (primeiraPagina) await client.templates.downloadPage(criado.id, primeiraPagina.id); // → Buffer (JPEG)
|
|
916
|
+
await client.templates.delete(criado.id);
|
|
917
|
+
|
|
918
|
+
// Cria um documento a partir de um template já configurado. Uploads novos têm
|
|
919
|
+
// apenas o papel Editor; adicione papéis de signatário no editor da Assinafy antes.
|
|
920
|
+
const configurado = await client.templates.get(templateId);
|
|
921
|
+
const papelSignatario = configurado.roles?.find(
|
|
922
|
+
(papel) => typeof papel.assignment_type === 'string'
|
|
923
|
+
&& papel.assignment_type.toLowerCase() !== 'editor',
|
|
775
924
|
);
|
|
776
|
-
if (!
|
|
925
|
+
if (!papelSignatario) throw new Error('O template não tem papel de signatário');
|
|
777
926
|
await client.documents.createFromTemplate(
|
|
778
927
|
templateId,
|
|
779
|
-
[{ role_id:
|
|
780
|
-
{ name: 'NDA -
|
|
928
|
+
[{ role_id: papelSignatario.id, id: signerId, verification_method: 'Email', notification_methods: ['Email'] }],
|
|
929
|
+
{ name: 'NDA - João Silva', message: 'Por favor, assine assim que puder.' },
|
|
781
930
|
);
|
|
782
931
|
|
|
783
|
-
//
|
|
932
|
+
// Estime o custo antes de criar → ICostEstimate
|
|
784
933
|
await client.documents.estimateCostFromTemplate(templateId, [
|
|
785
934
|
{ role_id: 'role_id', verification_method: 'Email', notification_methods: ['Email'] },
|
|
786
935
|
]);
|
|
@@ -788,41 +937,42 @@ await client.documents.estimateCostFromTemplate(templateId, [
|
|
|
788
937
|
// has_sufficient_resources: true, blocking_reason: null, breakdown: [], … }
|
|
789
938
|
```
|
|
790
939
|
|
|
791
|
-
|
|
792
|
-
`verification_method: 'DigitalCertificate'
|
|
793
|
-
[ICP-Brasil
|
|
940
|
+
Os descritores de signatário de template também aceitam
|
|
941
|
+
`verification_method: 'DigitalCertificate'`, com os mesmos pré-requisitos de
|
|
942
|
+
[Certificado digital ICP-Brasil](#certificado-digital-icp-brasil).
|
|
794
943
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
`templates.downloadPage()` so the API credential is attached.
|
|
944
|
+
Criar um template apenas envia o PDF e provisiona o papel de editor padrão — configure papéis e
|
|
945
|
+
campos no editor da Assinafy depois. Os `download_url` dos objetos de página do template são URLs
|
|
946
|
+
protegidas; prefira `templates.downloadPage()`, que anexa a credencial da API.
|
|
799
947
|
|
|
800
948
|
### Tags
|
|
801
949
|
|
|
802
|
-
|
|
950
|
+
Rótulos com escopo de workspace que podem ser anexados a documentos e templates. Os nomes são únicos
|
|
951
|
+
por workspace (sem diferenciar maiúsculas).
|
|
803
952
|
|
|
804
953
|
```ts
|
|
805
|
-
await client.tags.list({ search: '
|
|
806
|
-
const tag = await client.tags.create({ name: '
|
|
807
|
-
await client.tags.update(tag.id, { name: '
|
|
808
|
-
await client.tags.update(tag.id, { color: null }); //
|
|
809
|
-
await client.tags.delete(tag.id); // 409
|
|
810
|
-
await client.tags.delete(tag.id, { force: true }); //
|
|
954
|
+
await client.tags.list({ search: 'contrato' }); // ITag[]
|
|
955
|
+
const tag = await client.tags.create({ name: 'Contratos', color: 'ff8800' });
|
|
956
|
+
await client.tags.update(tag.id, { name: 'Contratos Comerciais' });
|
|
957
|
+
await client.tags.update(tag.id, { color: null }); // limpa a cor
|
|
958
|
+
await client.tags.delete(tag.id); // 409 se ainda estiver anexada
|
|
959
|
+
await client.tags.delete(tag.id, { force: true }); // desanexa de tudo e apaga
|
|
811
960
|
```
|
|
812
961
|
|
|
813
|
-
|
|
962
|
+
Anexe/desanexe tags em um documento específico com `client.documents.listTags / replaceTags /
|
|
963
|
+
addTags / detachTag` (veja [Documentos](#documentos)).
|
|
814
964
|
|
|
815
965
|
### Workspaces
|
|
816
966
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
967
|
+
Os schemas oficiais de criação/atualização definem `name` e `notification_sender_type`. O sandbox
|
|
968
|
+
também aceita os campos de cor abaixo; eles ficam mantidos como extensão de compatibilidade
|
|
969
|
+
documentada.
|
|
820
970
|
|
|
821
971
|
```ts
|
|
822
|
-
//
|
|
823
|
-
// '#ff0066'
|
|
972
|
+
// As cores são hex de 6 caracteres SEM '#' inicial (ao contrário das tags, que o removem).
|
|
973
|
+
// '#ff0066' é rejeitado — os endpoints de conta querem exatamente 6 caracteres.
|
|
824
974
|
await client.workspaces.create({
|
|
825
|
-
name: '
|
|
975
|
+
name: 'Meu Workspace',
|
|
826
976
|
notification_sender_type: 'Account',
|
|
827
977
|
primary_color: 'ff0066',
|
|
828
978
|
secondary_color: '0066ff',
|
|
@@ -831,13 +981,13 @@ await client.workspaces.create({
|
|
|
831
981
|
await client.workspaces.list();
|
|
832
982
|
await client.workspaces.get(accountId);
|
|
833
983
|
await client.workspaces.update(accountId, {
|
|
834
|
-
name: '
|
|
984
|
+
name: 'Renomeado',
|
|
835
985
|
notification_sender_type: 'User',
|
|
836
986
|
primary_color: '112233',
|
|
837
987
|
});
|
|
838
988
|
|
|
839
|
-
//
|
|
840
|
-
const
|
|
989
|
+
// Identidade visual
|
|
990
|
+
const tema = await client.workspaces.getTheme(accountId);
|
|
841
991
|
const logo = await client.workspaces.downloadLogo(accountId); // Buffer
|
|
842
992
|
await client.workspaces.uploadLogo(accountId, { filePath: './logo.png' });
|
|
843
993
|
await client.workspaces.uploadLogo(accountId, {
|
|
@@ -847,7 +997,7 @@ await client.workspaces.uploadLogo(accountId, {
|
|
|
847
997
|
});
|
|
848
998
|
await client.workspaces.deleteLogo(accountId);
|
|
849
999
|
|
|
850
|
-
//
|
|
1000
|
+
// Últimos 12 meses por padrão; estatísticas diárias exigem um mês YYYY-MM.
|
|
851
1001
|
await client.workspaces.getStats(accountId);
|
|
852
1002
|
await client.workspaces.getStats(accountId, {
|
|
853
1003
|
granularity: 'daily',
|
|
@@ -855,97 +1005,100 @@ await client.workspaces.getStats(accountId, {
|
|
|
855
1005
|
});
|
|
856
1006
|
|
|
857
1007
|
await client.workspaces.delete(accountId);
|
|
858
|
-
// `force`
|
|
859
|
-
//
|
|
860
|
-
await client.workspaces.delete(
|
|
1008
|
+
// `force` cancela uma assinatura paga ativa como parte da exclusão da conta. Não
|
|
1009
|
+
// é um atalho geral para outras restrições de exclusão.
|
|
1010
|
+
await client.workspaces.delete(contaRestrita, { force: true });
|
|
861
1011
|
```
|
|
862
1012
|
|
|
863
|
-
###
|
|
1013
|
+
### Definições de campo
|
|
864
1014
|
|
|
865
|
-
|
|
1015
|
+
Tipos de campo personalizados usados por assignments com método `collect`.
|
|
866
1016
|
|
|
867
1017
|
```ts
|
|
868
|
-
await client.fields.create({ type: 'text', name: '
|
|
1018
|
+
await client.fields.create({ type: 'text', name: 'Número do contrato' });
|
|
869
1019
|
await client.fields.list({ include_inactive: true, include_standard: true });
|
|
870
1020
|
await client.fields.get(fieldId);
|
|
871
|
-
await client.fields.update(fieldId, { name: '
|
|
1021
|
+
await client.fields.update(fieldId, { name: 'Nome atualizado' });
|
|
872
1022
|
await client.fields.delete(fieldId);
|
|
873
1023
|
|
|
874
|
-
//
|
|
1024
|
+
// Valida um único valor (o código de acesso só é exigido nas chamadas do signatário)
|
|
875
1025
|
await client.fields.validate(fieldId, '400.676.228-36', { signerAccessCode });
|
|
876
1026
|
|
|
877
|
-
//
|
|
1027
|
+
// Valida vários valores de uma vez
|
|
878
1028
|
await client.fields.validateMultiple(
|
|
879
1029
|
[
|
|
880
1030
|
{ field_id: 'f1', value: '1111111111111' },
|
|
881
|
-
{ field_id: 'f2', value: '
|
|
1031
|
+
{ field_id: 'f2', value: 'valor@exemplo.com.br' },
|
|
882
1032
|
],
|
|
883
1033
|
{ signerAccessCode },
|
|
884
1034
|
);
|
|
885
1035
|
|
|
886
|
-
//
|
|
1036
|
+
// Catálogo de todos os tipos de campo reconhecidos pela plataforma
|
|
887
1037
|
await client.fields.listTypes();
|
|
888
1038
|
```
|
|
889
1039
|
|
|
890
|
-
###
|
|
1040
|
+
### Autenticação / gestão de chaves de API
|
|
891
1041
|
|
|
892
|
-
|
|
1042
|
+
A maioria das integrações de servidor deve usar `X-Api-Key` diretamente. Use estes endpoints quando
|
|
1043
|
+
precisar abrir uma sessão para uma pessoa. Para apps conectados por terceiros, use
|
|
1044
|
+
[Aplicações OAuth](#aplicações-oauth).
|
|
893
1045
|
|
|
894
1046
|
```ts
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
|
|
898
|
-
const
|
|
1047
|
+
// OAuth de navegador (rota de compatibilidade): redirecione o usuário para esta URL.
|
|
1048
|
+
// O helper de callback devolve a URL de retorno da Assinafy para configurar o
|
|
1049
|
+
// provedor; nenhum dos dois segue o redirecionamento.
|
|
1050
|
+
const inicioOauth = client.auth.getSocialLoginUrl('google');
|
|
1051
|
+
const callbackOauth = client.auth.getSocialLoginCallbackUrl();
|
|
899
1052
|
|
|
900
|
-
const { access_token, user, accounts } = await client.auth.login('
|
|
1053
|
+
const { access_token, user, accounts } = await client.auth.login('eu@exemplo.com.br', 'senha');
|
|
901
1054
|
await client.auth.socialLogin({ provider: 'google', token: 'google-id-token', has_accepted_terms: true });
|
|
902
1055
|
await client.auth.linkSocialLogin({ provider: 'google', token: 'google-id-token' });
|
|
903
1056
|
|
|
904
|
-
//
|
|
905
|
-
await client.auth.createApiKey('
|
|
906
|
-
await client.auth.getApiKey(); // → { api_key: '****...nBNr' }
|
|
1057
|
+
// Chave de API pessoal
|
|
1058
|
+
await client.auth.createApiKey('senha-atual');
|
|
1059
|
+
await client.auth.getApiKey(); // → { api_key: '****...nBNr' } ou null
|
|
907
1060
|
await client.auth.deleteApiKey();
|
|
908
1061
|
|
|
909
|
-
//
|
|
910
|
-
await client.auth.changePassword({ email, password: '
|
|
911
|
-
await client.auth.requestPasswordReset('
|
|
912
|
-
await client.auth.resetPassword({ email, token: 'tk', new_password: '
|
|
1062
|
+
// Ciclo de vida da senha
|
|
1063
|
+
await client.auth.changePassword({ email, password: 'atual', new_password: 'nova' });
|
|
1064
|
+
await client.auth.requestPasswordReset('eu@exemplo.com.br');
|
|
1065
|
+
await client.auth.resetPassword({ email, token: 'tk', new_password: 'nova' });
|
|
913
1066
|
```
|
|
914
1067
|
|
|
915
|
-
###
|
|
1068
|
+
### Usuário autenticado
|
|
916
1069
|
|
|
917
1070
|
```ts
|
|
918
|
-
const
|
|
1071
|
+
const usuario = await client.users.getCurrent();
|
|
919
1072
|
// → { id, name, email, telephone, government_id, is_email_verified,
|
|
920
1073
|
// has_accepted_terms, created_at, to_be_deleted_at }
|
|
921
1074
|
|
|
922
|
-
//
|
|
923
|
-
const
|
|
924
|
-
const
|
|
1075
|
+
// Funil de documentos entre contas, últimos 12 períodos mensais por padrão.
|
|
1076
|
+
const mensal = await client.users.getStats();
|
|
1077
|
+
const diario = await client.users.getStats({
|
|
925
1078
|
granularity: 'daily',
|
|
926
1079
|
month: '2026-06',
|
|
927
1080
|
});
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
//
|
|
1081
|
+
// Cada linha traz período, totais de envio/remessa/certificação, contagens de
|
|
1082
|
+
// notificação por e-mail/WhatsApp/bypass, contagens de verificação por
|
|
1083
|
+
// e-mail/WhatsApp/bypass/certificado digital, visualizados e concluídos.
|
|
931
1084
|
|
|
932
|
-
const
|
|
1085
|
+
const preferencias = await client.users.getNotificationPreferences();
|
|
933
1086
|
await client.users.updateNotificationPreferences({
|
|
934
1087
|
SignerDeclined: false,
|
|
935
1088
|
DocumentExpired: false,
|
|
936
1089
|
});
|
|
937
|
-
//
|
|
938
|
-
//
|
|
1090
|
+
// A atualização é um merge: chaves omitidas mantêm o valor atual. Os dois
|
|
1091
|
+
// métodos devolvem o mapa completo de nove preferências.
|
|
939
1092
|
```
|
|
940
1093
|
|
|
941
1094
|
### Webhooks
|
|
942
1095
|
|
|
943
1096
|
```ts
|
|
944
1097
|
await client.webhooks.register({
|
|
945
|
-
url: 'https://
|
|
946
|
-
email: 'admin@
|
|
1098
|
+
url: 'https://exemplo.com.br/webhooks/assinafy',
|
|
1099
|
+
email: 'admin@exemplo.com.br',
|
|
947
1100
|
is_active: true,
|
|
948
|
-
// events
|
|
1101
|
+
// `events` assume o conjunto padrão do SDK, abaixo
|
|
949
1102
|
events: [
|
|
950
1103
|
'document_ready',
|
|
951
1104
|
'document_prepared',
|
|
@@ -956,26 +1109,25 @@ await client.webhooks.register({
|
|
|
956
1109
|
});
|
|
957
1110
|
|
|
958
1111
|
await client.webhooks.get(); // IWebhookSubscription | null
|
|
959
|
-
await client.webhooks.inactivate(); //
|
|
1112
|
+
await client.webhooks.inactivate(); // interrompe entregas (não existe rota de exclusão)
|
|
960
1113
|
await client.webhooks.listEventTypes();
|
|
961
|
-
const
|
|
1114
|
+
const historico = await client.webhooks.listDispatches({
|
|
962
1115
|
delivered: false,
|
|
963
1116
|
page: 1,
|
|
964
1117
|
'per-page': 20,
|
|
965
1118
|
}); // { data: IWebhookDispatch[], meta?: PaginationMeta }
|
|
966
|
-
const
|
|
1119
|
+
const reenviado = await client.webhooks.retryDispatch(dispatchId); // IWebhookDispatch
|
|
967
1120
|
```
|
|
968
1121
|
|
|
969
|
-
`register`
|
|
970
|
-
`{ events, is_active, url, email, updated_at? }`. Assinafy
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
retains only the first 2,000 characters of the receiver's response body.
|
|
1122
|
+
`register` envia `{ events, is_active, url, email }` e devolve
|
|
1123
|
+
`{ events, is_active, url, email, updated_at? }`. A Assinafy entrega cada evento como um `POST` HTTP
|
|
1124
|
+
com `Content-Type: application/json` e `Connection: close`. Qualquer `2xx` é sucesso. São no máximo
|
|
1125
|
+
duas tentativas automáticas, com três segundos de intervalo. Depois de dez eventos falhos
|
|
1126
|
+
consecutivos, a entrega comum é pausada e cerca de 5% dos eventos seguintes são tentados até um dar
|
|
1127
|
+
certo; use `retryDispatch()` para reenviar manualmente na hora. O histórico guarda apenas os
|
|
1128
|
+
primeiros 2.000 caracteres do corpo de resposta do receptor.
|
|
977
1129
|
|
|
978
|
-
|
|
1130
|
+
Cada item do histórico (ou resultado de reenvio) é um `IWebhookDispatch`:
|
|
979
1131
|
|
|
980
1132
|
```ts
|
|
981
1133
|
{
|
|
@@ -994,25 +1146,25 @@ Each history or retry result is an `IWebhookDispatch`:
|
|
|
994
1146
|
}
|
|
995
1147
|
```
|
|
996
1148
|
|
|
997
|
-
|
|
1149
|
+
Todo corpo de entrega usa este envelope:
|
|
998
1150
|
|
|
999
1151
|
```ts
|
|
1000
1152
|
{
|
|
1001
|
-
id: number; // use
|
|
1153
|
+
id: number; // use para processamento idempotente
|
|
1002
1154
|
event: string;
|
|
1003
1155
|
message: string | null;
|
|
1004
1156
|
payload: Record<string, unknown> | null;
|
|
1005
1157
|
origin: { ip?: string; 'user-agent'?: string } | null;
|
|
1006
|
-
created_at: number; // Unix
|
|
1158
|
+
created_at: number; // segundos Unix
|
|
1007
1159
|
subject: { type: 'User' | 'Signer' | 'Account' | 'Document' | 'Template'; [key: string]: unknown };
|
|
1008
1160
|
object: { type: 'User' | 'Signer' | 'Account' | 'Document' | 'Template'; [key: string]: unknown };
|
|
1009
1161
|
account_id: string;
|
|
1010
1162
|
}
|
|
1011
1163
|
```
|
|
1012
1164
|
|
|
1013
|
-
|
|
1165
|
+
Os valores por evento são:
|
|
1014
1166
|
|
|
1015
|
-
| `event` | `subject.type` | `object.type` | `payload`
|
|
1167
|
+
| `event` | `subject.type` | `object.type` | chaves de `payload` |
|
|
1016
1168
|
| --- | --- | --- | --- |
|
|
1017
1169
|
| `document_uploaded` | `User` | `Document` | — |
|
|
1018
1170
|
| `document_metadata_ready` | `User` | `Document` | — |
|
|
@@ -1020,7 +1172,7 @@ Event-specific values are:
|
|
|
1020
1172
|
| `assignment_created` | `User` | `Document` | `user_name`, `user_email`, `user_telephone` |
|
|
1021
1173
|
| `document_ready` | `Account` | `Document` | — |
|
|
1022
1174
|
| `document_processing_failed` | `Account` | `Document` | `error_message` |
|
|
1023
|
-
| `signature_requested` | `User` | `Document` | `signer_email`, `signer_full_name
|
|
1175
|
+
| `signature_requested` | `User` | `Document` | `signer_email`, `signer_full_name` ou `signer_whatsapp_phone_number`, conforme o canal |
|
|
1024
1176
|
| `signer_created` | `User` | `Signer` | `signer_full_name` |
|
|
1025
1177
|
| `signer_email_verified` | `Signer` | `Document` | `signer_email` |
|
|
1026
1178
|
| `signer_whatsapp_verified` | `Signer` | `Document` | `signer_whatsapp_phone_number` |
|
|
@@ -1033,21 +1185,19 @@ Event-specific values are:
|
|
|
1033
1185
|
| `template_processed` | `User` | `Template` | — |
|
|
1034
1186
|
| `template_processing_failed` | `Account` | `Template` | `error_message` |
|
|
1035
1187
|
|
|
1036
|
-
`payload`, `subject
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
property before delivery.
|
|
1188
|
+
`payload`, `subject` e `object` dependem do evento. Aceite campos desconhecidos para compatibilidade
|
|
1189
|
+
futura e só confirme o recebimento depois de um processamento durável e idempotente. Respostas
|
|
1190
|
+
não-`2xx`, timeouts e falhas de conexão contam como entregas falhas. `assignment_created` e
|
|
1191
|
+
`document_metadata_ready` não têm ordem garantida. Para entidades de conta, a Assinafy remove a
|
|
1192
|
+
propriedade `integration` antes de entregar.
|
|
1042
1193
|
|
|
1043
|
-
###
|
|
1194
|
+
### Verificação de webhooks
|
|
1044
1195
|
|
|
1045
|
-
`WebhookVerifier`
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
assumed header. The example below uses an application-configured header name.
|
|
1196
|
+
`WebhookVerifier` é um utilitário HMAC-SHA256 opcional, para integrações cujo ambiente Assinafy
|
|
1197
|
+
forneça um segredo compartilhado e um header de assinatura. O documento OpenAPI oficial atual
|
|
1198
|
+
**não** define esquema nem nome de header de assinatura de webhook. Confirme o contrato de entrega
|
|
1199
|
+
do seu ambiente antes de habilitar essa checagem; não rejeite callbacks de produção com base em um
|
|
1200
|
+
header presumido. O exemplo abaixo usa um nome de header configurado pela aplicação.
|
|
1051
1201
|
|
|
1052
1202
|
```ts
|
|
1053
1203
|
import express from 'express';
|
|
@@ -1055,39 +1205,39 @@ import express from 'express';
|
|
|
1055
1205
|
const webhookSecret = process.env.ASSINAFY_WEBHOOK_SECRET;
|
|
1056
1206
|
const signatureHeader = process.env.ASSINAFY_SIGNATURE_HEADER;
|
|
1057
1207
|
if (!webhookSecret || !signatureHeader) {
|
|
1058
|
-
throw new Error('
|
|
1208
|
+
throw new Error('Este ambiente não tem contrato confirmado de assinatura de webhook');
|
|
1059
1209
|
}
|
|
1060
1210
|
|
|
1061
1211
|
const webhookClient = new AssinafyClient({ webhookSecret });
|
|
1062
1212
|
|
|
1063
1213
|
app.post('/webhooks/assinafy', express.raw({ type: 'application/json' }), (req, res) => {
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1214
|
+
const assinatura = req.header(signatureHeader) ?? '';
|
|
1215
|
+
const corpoBruto = req.body as Buffer;
|
|
1066
1216
|
|
|
1067
|
-
if (!webhookClient.webhookVerifier.verify(
|
|
1068
|
-
return res.status(401).send('
|
|
1217
|
+
if (!webhookClient.webhookVerifier.verify(corpoBruto, assinatura)) {
|
|
1218
|
+
return res.status(401).send('Assinatura inválida');
|
|
1069
1219
|
}
|
|
1070
1220
|
|
|
1071
|
-
const
|
|
1072
|
-
const
|
|
1073
|
-
const
|
|
1221
|
+
const evento = webhookClient.webhookVerifier.extractEvent(corpoBruto);
|
|
1222
|
+
const tipo = webhookClient.webhookVerifier.getEventType(evento);
|
|
1223
|
+
const dados = webhookClient.webhookVerifier.getEventData(evento);
|
|
1074
1224
|
|
|
1075
|
-
switch (
|
|
1076
|
-
case 'document_ready':
|
|
1077
|
-
case 'signer_signed_document':
|
|
1078
|
-
case 'signer_rejected_document':
|
|
1079
|
-
case 'document_processing_failed':break;
|
|
1225
|
+
switch (tipo) {
|
|
1226
|
+
case 'document_ready': break;
|
|
1227
|
+
case 'signer_signed_document': break;
|
|
1228
|
+
case 'signer_rejected_document': break;
|
|
1229
|
+
case 'document_processing_failed': break;
|
|
1080
1230
|
}
|
|
1081
1231
|
res.sendStatus(200);
|
|
1082
1232
|
});
|
|
1083
1233
|
```
|
|
1084
1234
|
|
|
1085
|
-
###
|
|
1235
|
+
### Endpoints do signatário
|
|
1086
1236
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1237
|
+
Para construir portais de assinatura próprios. Quase todas as chamadas exigem o parâmetro de URL
|
|
1238
|
+
`signer-access-code` que a Assinafy envia ao signatário por e-mail/WhatsApp. O download de artefato
|
|
1239
|
+
é a exceção pública documentada; seu quarto argumento opcional de código de acesso existe apenas por
|
|
1240
|
+
compatibilidade com deploys que ainda esperam a query antiga.
|
|
1091
1241
|
|
|
1092
1242
|
```ts
|
|
1093
1243
|
await client.signerDocuments.self(accessCode);
|
|
@@ -1095,119 +1245,169 @@ await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verific
|
|
|
1095
1245
|
|
|
1096
1246
|
await client.signerDocuments.getCurrent(signerId, accessCode);
|
|
1097
1247
|
const { data } = await client.signerDocuments.list(signerId, accessCode, { 'per-page': 20 });
|
|
1098
|
-
//
|
|
1099
|
-
const
|
|
1248
|
+
// Equivalente a documents.search() do lado do signatário, autorizado pelo código de acesso.
|
|
1249
|
+
const achados = await client.signerDocuments.search(signerId, accessCode, 'nota fiscal');
|
|
1100
1250
|
await client.signerDocuments.download(signerId, documentId, 'original');
|
|
1101
|
-
//
|
|
1251
|
+
// Disponível só depois que um signatário por certificado ICP-Brasil concluir a assinatura.
|
|
1102
1252
|
await client.signerDocuments.download(signerId, documentId, 'pades');
|
|
1103
1253
|
|
|
1104
1254
|
await client.signerDocuments.confirmData(documentId, accessCode, {
|
|
1105
|
-
email: '
|
|
1106
|
-
full_name: '
|
|
1255
|
+
email: 'eu@exemplo.com.br',
|
|
1256
|
+
full_name: 'Signatário Exemplo',
|
|
1107
1257
|
government_id: '123.456.789-00',
|
|
1108
1258
|
has_accepted_terms: true,
|
|
1109
1259
|
});
|
|
1110
|
-
//
|
|
1260
|
+
// Como alternativa, aceite os termos separadamente antes de getAssignment():
|
|
1111
1261
|
// await client.signerDocuments.acceptTerms(accessCode);
|
|
1112
1262
|
|
|
1113
|
-
//
|
|
1263
|
+
// Gestão da imagem de assinatura ({ reuse: true } guarda para documentos futuros)
|
|
1114
1264
|
await client.signerDocuments.uploadSignature(accessCode, pngBuffer, { imageType: 'signature', reuse: true });
|
|
1115
1265
|
await client.signerDocuments.downloadSignature(accessCode, 'signature');
|
|
1116
1266
|
|
|
1117
|
-
//
|
|
1118
|
-
const
|
|
1119
|
-
// `sign()`
|
|
1267
|
+
// Assinar / recusar
|
|
1268
|
+
const assinavel = await client.signerDocuments.getAssignment(accessCode);
|
|
1269
|
+
// `sign()` é para assignments collect e exige o valor de cada campo posicionado.
|
|
1120
1270
|
await client.signerDocuments.sign(documentId, assignmentId, accessCode, [
|
|
1121
|
-
{ itemId, fieldId, pageId, value: '
|
|
1271
|
+
{ itemId, fieldId, pageId, value: 'Assinado por João' },
|
|
1122
1272
|
]);
|
|
1123
|
-
await client.signerDocuments.decline(documentId, assignmentId, accessCode, '
|
|
1273
|
+
await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'Sem autorização');
|
|
1124
1274
|
|
|
1125
|
-
// `signMultiple()`
|
|
1275
|
+
// `signMultiple()` é só para assignments virtual.
|
|
1126
1276
|
await client.signerDocuments.signMultiple(['doc-1', 'doc-2'], accessCode);
|
|
1127
|
-
await client.signerDocuments.declineMultiple(['doc-1'], '
|
|
1277
|
+
await client.signerDocuments.declineMultiple(['doc-1'], 'Condições desfavoráveis', accessCode);
|
|
1128
1278
|
```
|
|
1129
1279
|
|
|
1130
|
-
`sign()`
|
|
1131
|
-
|
|
1132
|
-
|
|
1280
|
+
`sign()` também exige que signatários virtuais já tenham confirmado os dados, mas assignments
|
|
1281
|
+
virtuais normalmente devem usar `signMultiple()`. Signatários por certificado não podem usar
|
|
1282
|
+
`sign()`; veja [Ramos pagos de assinatura](#ramos-pagos-de-assinatura).
|
|
1133
1283
|
|
|
1134
|
-
##
|
|
1284
|
+
## Helper de alto nível
|
|
1135
1285
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1286
|
+
Envia um PDF, reaproveita ou cria signatários por e-mail, cria um assignment virtual imediatamente e,
|
|
1287
|
+
opcionalmente, espera o processamento antes de retornar.
|
|
1138
1288
|
|
|
1139
1289
|
```ts
|
|
1140
|
-
const
|
|
1141
|
-
source: { filePath: './
|
|
1290
|
+
const resultado = await client.uploadAndRequestSignatures({
|
|
1291
|
+
source: { filePath: './contrato.pdf' },
|
|
1142
1292
|
signers: [
|
|
1143
|
-
{ name: '
|
|
1144
|
-
{ name: '
|
|
1293
|
+
{ name: 'João', email: 'joao@exemplo.com.br' },
|
|
1294
|
+
{ name: 'Maria', email: 'maria@exemplo.com.br' },
|
|
1145
1295
|
],
|
|
1146
|
-
message: '
|
|
1147
|
-
metadata: {
|
|
1296
|
+
message: 'Por favor, assine',
|
|
1297
|
+
metadata: { ano: 2026 }, // parte de compatibilidade; omita para o formato só-arquivo
|
|
1148
1298
|
waitForReady: true,
|
|
1149
1299
|
waitOptions: { maxWaitMs: 30_000, pollIntervalMs: 1_000 },
|
|
1150
1300
|
expiresAt: '2027-12-31T00:00:00Z',
|
|
1151
|
-
copyReceivers: ['
|
|
1301
|
+
copyReceivers: ['id-de-signatario-em-copia'],
|
|
1152
1302
|
});
|
|
1153
1303
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1304
|
+
resultado.document; // IDocumentDetailsResponse já processado (waitForReady: true, o padrão);
|
|
1305
|
+
// o IDocumentUploadResponse cru quando waitForReady: false
|
|
1306
|
+
resultado.assignment; // IAssignment
|
|
1307
|
+
resultado.signer_ids; // string[]
|
|
1158
1308
|
```
|
|
1159
1309
|
|
|
1160
|
-
`waitForReady: false`
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
signer IDs, not email addresses; check the returned assignment before treating
|
|
1168
|
-
a copy receiver as registered.
|
|
1310
|
+
`waitForReady: false` pula a espera pós-assignment e devolve a resposta inicial do upload. Com o
|
|
1311
|
+
padrão `true`, o helper cria o assignment primeiro e só então espera os detalhes atuais do
|
|
1312
|
+
documento. Produção e sandbox permitem assignments virtuais em `uploaded` e `metadata_processing` e
|
|
1313
|
+
os promovem automaticamente; só assignments `collect` exigem páginas renderizadas. Todos os
|
|
1314
|
+
signatários acima usam o canal de e-mail padrão. Um signatário só com telefone seleciona o ramo pago
|
|
1315
|
+
de WhatsApp descrito antes. `copyReceivers` aceita IDs de signatários existentes, não endereços de
|
|
1316
|
+
e-mail; confira o assignment devolvido antes de considerar uma cópia registrada.
|
|
1169
1317
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
whether to retry the workflow.
|
|
1318
|
+
O helper não é transacional. Um erro depois do assignment inclui `documentId`, `assignmentId` e
|
|
1319
|
+
`signerIds` criados no `context` do erro (e em `ValidationError.errors` para timeouts); inspecione
|
|
1320
|
+
esses IDs antes de decidir se repete o fluxo.
|
|
1174
1321
|
|
|
1175
|
-
##
|
|
1322
|
+
## Erros
|
|
1176
1323
|
|
|
1177
|
-
HTTP
|
|
1178
|
-
|
|
1324
|
+
Os métodos HTTP rejeitam com uma subclasse de `AssinafyError`. Helpers síncronos como
|
|
1325
|
+
`getSocialLoginUrl()` e `readAuthorizationCallback()` podem lançar antes de qualquer requisição.
|
|
1326
|
+
|
|
1327
|
+
| Classe | Quando | O que inspecionar |
|
|
1328
|
+
| --- | --- | --- |
|
|
1329
|
+
| `ValidationError` | O SDK recusou a entrada antes de enviar | `err.errors` |
|
|
1330
|
+
| `OAuthError` | Um endpoint OAuth devolveu `{ error, error_description }`, ou o callback trouxe `?error=` | `err.error`, `err.errorDescription` |
|
|
1331
|
+
| `ApiError` | A API respondeu com status de falha | `err.statusCode`, `err.responseData`, `err.challenge` |
|
|
1332
|
+
| `NetworkError` | DNS, conexão ou timeout | `err.message` (já sem credenciais) |
|
|
1333
|
+
| `AssinafyError` | Classe base de todas as acima | `err.context` |
|
|
1179
1334
|
|
|
1180
1335
|
```ts
|
|
1181
|
-
import { ApiError, ValidationError, NetworkError, AssinafyError } from '@assinafy/sdk';
|
|
1336
|
+
import { ApiError, OAuthError, ValidationError, NetworkError, AssinafyError } from '@assinafy/sdk';
|
|
1182
1337
|
|
|
1183
1338
|
try {
|
|
1184
1339
|
await client.documents.upload({ filePath: './x.pdf' });
|
|
1185
1340
|
} catch (err) {
|
|
1186
1341
|
if (err instanceof ValidationError) {
|
|
1187
|
-
console.error('
|
|
1342
|
+
console.error('Falha de validação:', err.errors);
|
|
1343
|
+
} else if (err instanceof OAuthError) {
|
|
1344
|
+
// OAuthError estende ApiError; ramifique pelo código RFC 6749, não pela mensagem.
|
|
1345
|
+
console.error('Erro OAuth:', err.error, err.errorDescription);
|
|
1188
1346
|
} else if (err instanceof ApiError) {
|
|
1189
|
-
console.error(`API
|
|
1347
|
+
console.error(`Erro da API ${err.statusCode}:`, err.responseData);
|
|
1348
|
+
// `err.challenge` traz o header WWW-Authenticate já interpretado quando a API
|
|
1349
|
+
// envia um — em um 403 ele nomeia o escopo OAuth que está faltando.
|
|
1190
1350
|
} else if (err instanceof NetworkError) {
|
|
1191
|
-
console.error('
|
|
1351
|
+
console.error('Erro de rede:', err.message);
|
|
1192
1352
|
} else if (err instanceof AssinafyError) {
|
|
1193
|
-
console.error('SDK
|
|
1353
|
+
console.error('Erro do SDK:', err.message, err.context);
|
|
1194
1354
|
}
|
|
1195
1355
|
}
|
|
1196
1356
|
```
|
|
1197
1357
|
|
|
1198
|
-
##
|
|
1358
|
+
## Ambientes
|
|
1359
|
+
|
|
1360
|
+
| | |
|
|
1361
|
+
| --- | --- |
|
|
1362
|
+
| Produção | `https://api.assinafy.com.br/v1` |
|
|
1363
|
+
| Sandbox | `https://sandbox.assinafy.com.br/v1` |
|
|
1364
|
+
|
|
1365
|
+
O sandbox é gratuito e espelha a produção para testar a integração de ponta a ponta, com a exceção
|
|
1366
|
+
das rotas de certificado digital, que existem apenas em produção.
|
|
1367
|
+
|
|
1368
|
+
O sandbox tem servidor de autorização próprio em `https://auth-sandbox.assinafy.com.br`, com a tela
|
|
1369
|
+
de consentimento em `/oauth/authorize`. Os documentos de descoberta dele, porém, não são
|
|
1370
|
+
alcançáveis: o nginx do sandbox recusa qualquer caminho iniciado por ponto, então `/.well-known/…`
|
|
1371
|
+
nunca chega à aplicação. Informe os endpoints explicitamente para pular a descoberta:
|
|
1372
|
+
|
|
1373
|
+
```ts
|
|
1374
|
+
const client = new AssinafyClient({ baseUrl: 'https://sandbox.assinafy.com.br/v1' });
|
|
1375
|
+
|
|
1376
|
+
const requisicao = await client.oauth.createAuthorizationUrl({
|
|
1377
|
+
clientId: process.env.ASSINAFY_CLIENT_ID!,
|
|
1378
|
+
redirectUri: 'https://meuapp.com.br/oauth/callback',
|
|
1379
|
+
scopes: ['documents:read'],
|
|
1380
|
+
issuer: 'https://auth-sandbox.assinafy.com.br',
|
|
1381
|
+
authorizationEndpoint: 'https://auth-sandbox.assinafy.com.br/oauth/authorize',
|
|
1382
|
+
});
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
Os endpoints de token, revogação e userinfo seguem o `baseUrl` configurado. Confirme que o deploy de
|
|
1386
|
+
sandbox os expõe antes de rodar a etapa de troca por lá — veja
|
|
1387
|
+
[docs/COMPATIBILITY.md](docs/COMPATIBILITY.md).
|
|
1388
|
+
|
|
1389
|
+
## Desenvolvimento
|
|
1199
1390
|
|
|
1200
1391
|
```bash
|
|
1201
1392
|
bun install --frozen-lockfile
|
|
1202
|
-
bun run typecheck #
|
|
1393
|
+
bun run typecheck # tipos de src, scripts e testes
|
|
1203
1394
|
bun run lint
|
|
1204
|
-
bun test # bun:test
|
|
1395
|
+
bun test # suítes bun:test
|
|
1205
1396
|
bun run test:coverage
|
|
1206
1397
|
bun run build # tsup → dist/ (CJS + ESM + .d.ts)
|
|
1207
1398
|
bun run lint:pkg # publint + arethetypeswrong
|
|
1208
|
-
bun run
|
|
1399
|
+
bun run audit:api # confere o SDK contra o OpenAPI publicado
|
|
1400
|
+
bun run verify # portão completo de release local
|
|
1209
1401
|
```
|
|
1210
1402
|
|
|
1211
|
-
##
|
|
1403
|
+
## Documentação
|
|
1404
|
+
|
|
1405
|
+
- **[README.en.md](README.en.md)** — a mesma referência completa, em inglês
|
|
1406
|
+
- [docs/API_COVERAGE.md](docs/API_COVERAGE.md) — mapa de operações
|
|
1407
|
+
- [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md) — variações de requisição e resposta por deploy
|
|
1408
|
+
- [docs/RELEASING.md](docs/RELEASING.md) — processo de publicação
|
|
1409
|
+
- [Documentação da API](https://api.assinafy.com.br/v1/docs)
|
|
1410
|
+
|
|
1411
|
+
## Licença
|
|
1212
1412
|
|
|
1213
|
-
MIT
|
|
1413
|
+
Distribuído sob a licença [MIT](LICENSE).
|