@assinafy/sdk 2.1.2 → 2.3.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 +168 -1
- package/README.en.md +1373 -0
- package/README.md +763 -522
- package/SECURITY.md +17 -2
- package/dist/index.d.mts +794 -12
- package/dist/index.d.ts +794 -12
- package/dist/index.js +900 -59
- package/dist/index.mjs +894 -57
- package/docs/API_COVERAGE.md +28 -4
- package/docs/COMPATIBILITY.md +86 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,41 +1,73 @@
|
|
|
1
1
|
# @assinafy/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
*Português · [Read in English](README.en.md)*
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
templates, tags, fields, webhooks, branding, statistics, and the high-level
|
|
8
|
-
`uploadAndRequestSignatures` workflow. Five additional template-management
|
|
9
|
-
routes used by existing integrations and two legacy browser URL helpers are
|
|
10
|
-
retained for compatibility.
|
|
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.
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
15
13
|
|
|
16
|
-
|
|
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).
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
CJS and ESM imports are tested on 22 (maintenance LTS), 24 (active LTS), and
|
|
20
|
-
26 (Current); Node 20 reached end-of-life in April 2026 and is unsupported.
|
|
21
|
-
- or Bun 1.4.0 (the version pinned for development and CI)
|
|
17
|
+
## Sumário
|
|
22
18
|
|
|
23
|
-
|
|
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
|
|
24
54
|
|
|
25
55
|
```bash
|
|
26
56
|
npm install @assinafy/sdk
|
|
27
|
-
#
|
|
57
|
+
# ou
|
|
28
58
|
bun add @assinafy/sdk
|
|
29
59
|
```
|
|
30
60
|
|
|
31
|
-
|
|
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`:
|
|
32
64
|
|
|
33
65
|
```
|
|
34
66
|
@assinafy:registry=https://npm.pkg.github.com
|
|
35
67
|
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
|
|
36
68
|
```
|
|
37
69
|
|
|
38
|
-
##
|
|
70
|
+
## Início rápido
|
|
39
71
|
|
|
40
72
|
```ts
|
|
41
73
|
import { AssinafyClient } from '@assinafy/sdk';
|
|
@@ -47,140 +79,278 @@ const client = new AssinafyClient({
|
|
|
47
79
|
baseUrl,
|
|
48
80
|
});
|
|
49
81
|
|
|
50
|
-
const
|
|
51
|
-
source: { filePath: './
|
|
82
|
+
const resultado = await client.uploadAndRequestSignatures({
|
|
83
|
+
source: { filePath: './contrato.pdf' },
|
|
52
84
|
signers: [
|
|
53
|
-
{ name: '
|
|
54
|
-
{ name: '
|
|
85
|
+
{ name: 'João Silva', email: 'joao@exemplo.com.br' },
|
|
86
|
+
{ name: 'Maria Souza', email: 'maria@exemplo.com.br' },
|
|
55
87
|
],
|
|
56
|
-
message: '
|
|
88
|
+
message: 'Por favor, assine este contrato',
|
|
57
89
|
});
|
|
58
90
|
|
|
59
|
-
console.log('
|
|
60
|
-
console.log('
|
|
91
|
+
console.log('ID do documento:', resultado.document.id);
|
|
92
|
+
console.log('ID do assignment:', resultado.assignment.id);
|
|
61
93
|
```
|
|
62
94
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
[
|
|
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.
|
|
66
102
|
|
|
67
|
-
|
|
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). |
|
|
68
108
|
|
|
69
|
-
|
|
109
|
+
Prefira `apiKey` para integração servidor a servidor — corresponde ao header `X-Api-Key`
|
|
110
|
+
recomendado pela Assinafy.
|
|
70
111
|
|
|
71
112
|
```ts
|
|
72
|
-
//
|
|
113
|
+
// Preferido: header X-Api-Key
|
|
73
114
|
new AssinafyClient({ apiKey: 'k_xxx', accountId: 'acc_xxx' });
|
|
74
115
|
|
|
75
|
-
//
|
|
116
|
+
// Token de acesso: Authorization: Bearer <token>
|
|
76
117
|
new AssinafyClient({ token: 'jwt_xxx', accountId: 'acc_xxx' });
|
|
77
118
|
```
|
|
78
119
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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:
|
|
82
123
|
|
|
83
124
|
```ts
|
|
84
|
-
const
|
|
125
|
+
const clientePublico = new AssinafyClient({
|
|
85
126
|
baseUrl: 'https://sandbox.assinafy.com.br/v1',
|
|
86
127
|
});
|
|
87
128
|
|
|
88
|
-
await
|
|
89
|
-
await
|
|
90
|
-
await
|
|
129
|
+
await clientePublico.auth.login('eu@exemplo.com.br', 'senha');
|
|
130
|
+
await clientePublico.documents.getPublic(documentId);
|
|
131
|
+
await clientePublico.signerDocuments.self(signerAccessCode);
|
|
91
132
|
```
|
|
92
133
|
|
|
93
|
-
|
|
94
|
-
|
|
134
|
+
Métodos protegidos continuam exigindo `apiKey` ou `token`; sem credencial, a API devolve o `401`
|
|
135
|
+
normal.
|
|
95
136
|
|
|
96
|
-
|
|
97
|
-
`User-Agent: Assinafy-Typescript-SDK/v<
|
|
98
|
-
|
|
99
|
-
`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`.
|
|
100
140
|
|
|
101
|
-
##
|
|
141
|
+
## Aplicações OAuth
|
|
102
142
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
| `token` | string | — | Access token (sent as `Authorization: Bearer`). |
|
|
107
|
-
| `accountId` | string | — | Default workspace/account ID. |
|
|
108
|
-
| `baseUrl` | string | `https://api.assinafy.com.br/v1` | Absolute HTTP(S) API base without credentials, query, or fragment. |
|
|
109
|
-
| `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). |
|
|
110
|
-
| `timeout` | number | `30000` | Request timeout in milliseconds. |
|
|
111
|
-
| `maxRetries` | number | `2` | Auto-retries eligible HTTP 429 responses, honoring `Retry-After`. `0` disables. |
|
|
112
|
-
| `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.
|
|
113
146
|
|
|
114
|
-
|
|
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.
|
|
115
152
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
`OPTIONS`, and `DELETE` requests. `GET /sign` is excluded because it records
|
|
120
|
-
that the signer viewed the assignment. Writes are not replayed by default.
|
|
121
|
-
A non-empty `Idempotency-Key` opts a custom request into SDK replay, but it is
|
|
122
|
-
not part of the current Assinafy OpenAPI contract: confirm that the target
|
|
123
|
-
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.
|
|
124
156
|
|
|
125
|
-
|
|
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:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
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
|
|
246
|
+
|
|
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
|
|
126
301
|
|
|
127
302
|
```ts
|
|
128
|
-
//
|
|
129
|
-
const client = AssinafyClient.create('api
|
|
303
|
+
// Fábrica posicional
|
|
304
|
+
const client = AssinafyClient.create('chave-de-api', 'id-da-conta');
|
|
130
305
|
|
|
131
|
-
//
|
|
306
|
+
// A partir de um objeto simples (aceita chaves snake_case ou camelCase)
|
|
132
307
|
const client = AssinafyClient.fromConfig({
|
|
133
308
|
api_key: process.env.ASSINAFY_API_KEY!,
|
|
134
309
|
account_id: process.env.ASSINAFY_ACCOUNT_ID!,
|
|
135
310
|
});
|
|
136
311
|
```
|
|
137
312
|
|
|
138
|
-
##
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
| `client.documents`
|
|
147
|
-
| `client.signers`
|
|
148
|
-
| `client.assignments`
|
|
149
|
-
| `client.templates`
|
|
150
|
-
| `client.tags`
|
|
151
|
-
| `client.workspaces`
|
|
152
|
-
| `client.webhooks`
|
|
153
|
-
| `client.fields`
|
|
154
|
-
| `client.
|
|
155
|
-
| `client.
|
|
156
|
-
| `client.
|
|
157
|
-
| `client.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
`virtual` assignment, so no page coordinates or paid notification channel are
|
|
172
|
-
required.
|
|
173
|
-
|
|
174
|
-
### 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
|
|
175
346
|
|
|
176
347
|
```ts
|
|
177
|
-
const
|
|
348
|
+
const enviado = await client.documents.upload({ filePath: './contrato.pdf' });
|
|
178
349
|
```
|
|
179
350
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
`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`:
|
|
184
354
|
|
|
185
355
|
```ts
|
|
186
356
|
{
|
|
@@ -210,37 +380,36 @@ display-name override (used as that file part's filename) and an optional JSON
|
|
|
210
380
|
}
|
|
211
381
|
```
|
|
212
382
|
|
|
213
|
-
`DocumentStatus`
|
|
214
|
-
`
|
|
215
|
-
`
|
|
383
|
+
`DocumentStatus` cobre `uploading`, `uploaded`, `metadata_processing`, `metadata_ready`,
|
|
384
|
+
`pending_signature`, `expired`, `certificating`, `certificated`, `rejected_by_signer`,
|
|
385
|
+
`rejected_by_user` e `failed`.
|
|
216
386
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
`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.
|
|
222
391
|
|
|
223
392
|
```ts
|
|
224
|
-
const
|
|
393
|
+
const preparado = await client.documents.waitUntilReady(enviado.id, {
|
|
225
394
|
maxWaitMs: 30_000,
|
|
226
395
|
pollIntervalMs: 2_000,
|
|
227
396
|
});
|
|
228
397
|
```
|
|
229
398
|
|
|
230
|
-
### 2.
|
|
399
|
+
### 2. Crie ou reaproveite os signatários por e-mail
|
|
231
400
|
|
|
232
401
|
```ts
|
|
233
|
-
const
|
|
234
|
-
full_name: '
|
|
235
|
-
email: '
|
|
402
|
+
const signatarioA = await client.signers.create({
|
|
403
|
+
full_name: 'João Silva',
|
|
404
|
+
email: 'joao@exemplo.com.br',
|
|
236
405
|
});
|
|
237
|
-
const
|
|
238
|
-
full_name: '
|
|
239
|
-
email: '
|
|
406
|
+
const signatarioB = await client.signers.create({
|
|
407
|
+
full_name: 'Maria Souza',
|
|
408
|
+
email: 'maria@exemplo.com.br',
|
|
240
409
|
});
|
|
241
410
|
```
|
|
242
411
|
|
|
243
|
-
|
|
412
|
+
O corpo de rede é `{ full_name, email }`. Cada resposta é um `ISigner`:
|
|
244
413
|
|
|
245
414
|
```ts
|
|
246
415
|
{
|
|
@@ -249,35 +418,34 @@ The wire body is `{ full_name, email }`. Each response is an `ISigner`:
|
|
|
249
418
|
full_name: string;
|
|
250
419
|
email: string | null;
|
|
251
420
|
whatsapp_phone_number?: string | null;
|
|
252
|
-
cpf?: string | null; //
|
|
421
|
+
cpf?: string | null; // tipo de compatibilidade; a API não devolve
|
|
253
422
|
has_accepted_terms?: boolean;
|
|
254
|
-
has_signature?: boolean; //
|
|
255
|
-
has_initial?: boolean; //
|
|
256
|
-
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
|
|
257
426
|
metadata?: Record<string, unknown>;
|
|
258
427
|
}
|
|
259
428
|
```
|
|
260
429
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
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.
|
|
264
432
|
|
|
265
|
-
### 3.
|
|
433
|
+
### 3. Orce, depois peça as assinaturas
|
|
266
434
|
|
|
267
|
-
|
|
435
|
+
A estimativa de custo recebe descritores de canal, não IDs de signatário:
|
|
268
436
|
|
|
269
437
|
```ts
|
|
270
|
-
const
|
|
438
|
+
const estimativa = await client.assignments.estimateCost(enviado.id, {
|
|
271
439
|
method: 'virtual',
|
|
272
|
-
signers: [{}, {}], // `{}`
|
|
440
|
+
signers: [{}, {}], // `{}` seleciona Email para cada signatário
|
|
273
441
|
});
|
|
274
442
|
|
|
275
|
-
if (!
|
|
276
|
-
throw new Error(
|
|
443
|
+
if (!estimativa.has_sufficient_resources) {
|
|
444
|
+
throw new Error(estimativa.blocking_reason ?? estimativa.message ?? 'Recursos insuficientes');
|
|
277
445
|
}
|
|
278
446
|
```
|
|
279
447
|
|
|
280
|
-
|
|
448
|
+
A resposta é `ICostEstimate`:
|
|
281
449
|
|
|
282
450
|
```ts
|
|
283
451
|
{
|
|
@@ -295,21 +463,21 @@ The response is `ICostEstimate`:
|
|
|
295
463
|
}
|
|
296
464
|
```
|
|
297
465
|
|
|
298
|
-
|
|
466
|
+
Crie o assignment por e-mail só depois de aceitar essa estimativa:
|
|
299
467
|
|
|
300
468
|
```ts
|
|
301
|
-
const assignment = await client.assignments.create(
|
|
469
|
+
const assignment = await client.assignments.create(enviado.id, {
|
|
302
470
|
method: 'virtual',
|
|
303
471
|
signers: [
|
|
304
|
-
{ id:
|
|
305
|
-
{ id:
|
|
472
|
+
{ id: signatarioA.id, verification_method: 'Email', notification_methods: ['Email'] },
|
|
473
|
+
{ id: signatarioB.id, verification_method: 'Email', notification_methods: ['Email'] },
|
|
306
474
|
],
|
|
307
|
-
message: '
|
|
475
|
+
message: 'Por favor, revise e assine',
|
|
308
476
|
expires_at: '2027-12-31T23:59:00Z',
|
|
309
477
|
});
|
|
310
478
|
```
|
|
311
479
|
|
|
312
|
-
|
|
480
|
+
A requisição devolve um `IAssignment`:
|
|
313
481
|
|
|
314
482
|
```ts
|
|
315
483
|
{
|
|
@@ -332,240 +500,232 @@ The request returns an `IAssignment`:
|
|
|
332
500
|
}
|
|
333
501
|
```
|
|
334
502
|
|
|
335
|
-
|
|
336
|
-
secrets.
|
|
503
|
+
As URLs e as mensagens entregues contêm credenciais do signatário; trate-as como segredo.
|
|
337
504
|
|
|
338
|
-
### 4.
|
|
505
|
+
### 4. Conclua o fluxo do signatário por e-mail
|
|
339
506
|
|
|
340
|
-
Assinafy
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
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.
|
|
345
511
|
|
|
346
512
|
```ts
|
|
347
|
-
//
|
|
348
|
-
const
|
|
349
|
-
baseUrl,
|
|
350
|
-
});
|
|
513
|
+
// Cliente do signatário: nenhuma credencial da conta é necessária nem enviada.
|
|
514
|
+
const clienteSignatario = new AssinafyClient({ baseUrl });
|
|
351
515
|
|
|
352
|
-
const self = await
|
|
516
|
+
const self = await clienteSignatario.signerDocuments.self(accessCode); // ISignerSelf
|
|
353
517
|
|
|
354
518
|
// Query: signer-access-code=<accessCode>
|
|
355
|
-
//
|
|
356
|
-
await
|
|
519
|
+
// Corpo: { 'verification-code': '<código de seis dígitos>' }
|
|
520
|
+
await clienteSignatario.signerDocuments.verifyEmail({
|
|
357
521
|
signerAccessCode: accessCode,
|
|
358
522
|
verificationCode,
|
|
359
523
|
}); // Promise<void>
|
|
360
524
|
|
|
361
|
-
const
|
|
362
|
-
|
|
525
|
+
const confirmado = await clienteSignatario.signerDocuments.confirmData(
|
|
526
|
+
enviado.id,
|
|
363
527
|
accessCode,
|
|
364
528
|
{ full_name: self.full_name, email: self.email ?? undefined },
|
|
365
529
|
); // ISigner
|
|
366
530
|
|
|
367
|
-
const
|
|
368
|
-
// `getAssignment`
|
|
369
|
-
//
|
|
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.
|
|
370
534
|
|
|
371
|
-
await
|
|
372
|
-
//
|
|
535
|
+
await clienteSignatario.signerDocuments.signMultiple([assinavel.id], accessCode);
|
|
536
|
+
// Corpo de rede: { document_ids: [assinavel.id] }; o reconhecimento não traz dados.
|
|
373
537
|
```
|
|
374
538
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
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.
|
|
378
541
|
|
|
379
|
-
`signMultiple`
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
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).
|
|
386
548
|
|
|
387
|
-
### 5.
|
|
549
|
+
### 5. Acompanhe a conclusão e baixe os artefatos
|
|
388
550
|
|
|
389
|
-
|
|
390
|
-
`documents.details(documentId)`
|
|
391
|
-
|
|
392
|
-
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:
|
|
393
554
|
|
|
394
555
|
```ts
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
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');
|
|
399
560
|
|
|
400
|
-
//
|
|
401
|
-
const
|
|
561
|
+
// Valide um hash de assinatura Assinafy que o seu fluxo tenha extraído.
|
|
562
|
+
const validacao = await client.documents.verify(documentSignatureHash);
|
|
402
563
|
```
|
|
403
564
|
|
|
404
|
-
`original`, `certificated
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
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.
|
|
410
570
|
|
|
411
|
-
##
|
|
571
|
+
## Referência de recursos
|
|
412
572
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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.
|
|
416
576
|
|
|
417
|
-
###
|
|
577
|
+
### Documentos
|
|
418
578
|
|
|
419
579
|
```ts
|
|
420
|
-
//
|
|
580
|
+
// Envio a partir de um caminho de arquivo (recomendado)
|
|
421
581
|
const doc = await client.documents.upload(
|
|
422
|
-
{ filePath: './
|
|
423
|
-
{ name: '
|
|
582
|
+
{ filePath: './contrato.pdf' },
|
|
583
|
+
{ name: 'Contrato de prestação', metadata: { tipo: 'servico' } },
|
|
424
584
|
);
|
|
425
|
-
// `name`
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
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').
|
|
431
591
|
// → {
|
|
432
592
|
// resource: 'document', id: '1031…', account_id: '102d…', template_id: null,
|
|
433
|
-
// name: '
|
|
593
|
+
// name: 'Contrato de prestacao.pdf', status: 'uploaded',
|
|
434
594
|
// artifacts: { original: 'https://…/download/original' },
|
|
435
595
|
// signing_url: 'https://app…/sign/1031…',
|
|
436
|
-
// pages: [], //
|
|
596
|
+
// pages: [], // preenchido quando o status chega a `metadata_ready`
|
|
437
597
|
// tags: [], is_closed: false, created_at: '2026-…', updated_at: '2026-…'
|
|
438
598
|
// }
|
|
439
599
|
|
|
440
|
-
// …
|
|
441
|
-
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' });
|
|
442
602
|
|
|
443
|
-
//
|
|
444
|
-
const { data, meta } = await client.documents.list({ page: 1,
|
|
603
|
+
// Listagem → { data: IDocumentListItem[], meta?: { current_page, per_page, total, last_page } }
|
|
604
|
+
const { data, meta } = await client.documents.list({ page: 1, 'per-page': 20, sort: 'updated_at' });
|
|
445
605
|
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
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 });
|
|
449
609
|
|
|
450
610
|
await client.documents.details(doc.id);
|
|
451
611
|
await client.documents.activities(doc.id);
|
|
452
612
|
await client.documents.waitUntilReady(doc.id, { maxWaitMs: 30_000 });
|
|
453
613
|
|
|
454
|
-
//
|
|
455
|
-
// `metadata_processing`,
|
|
456
|
-
// (
|
|
457
|
-
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');
|
|
458
618
|
|
|
459
|
-
await client.documents.download(doc.id, 'certificated');
|
|
619
|
+
await client.documents.download(doc.id, 'certificated'); // PDF assinado
|
|
460
620
|
await client.documents.download(doc.id, 'certificate-page');
|
|
461
621
|
await client.documents.download(doc.id, 'bundle'); // ZIP
|
|
462
|
-
// `pades`
|
|
622
|
+
// `pades` existe apenas quando algum signatário usou DigitalCertificate.
|
|
463
623
|
await client.documents.download(doc.id, 'pades');
|
|
464
624
|
await client.documents.thumbnail(doc.id);
|
|
465
625
|
await client.documents.downloadPage(doc.id, pageId);
|
|
466
626
|
|
|
467
|
-
await client.documents.statuses(); //
|
|
627
|
+
await client.documents.statuses(); // todos os códigos de status + flag deletable
|
|
468
628
|
await client.documents.isFullySigned(doc.id);
|
|
469
629
|
await client.documents.getSigningProgress(doc.id);
|
|
470
630
|
await client.documents.delete(doc.id);
|
|
471
631
|
|
|
472
|
-
//
|
|
632
|
+
// Verifique um documento assinado pelo hash de assinatura Assinafy
|
|
473
633
|
await client.documents.verify('FE32EDDADE7CBDDCBB934E7402047450B0E59C02');
|
|
474
634
|
|
|
475
|
-
//
|
|
635
|
+
// Endpoints públicos (sem autenticação)
|
|
476
636
|
await client.documents.getPublic(doc.id);
|
|
477
|
-
//
|
|
478
|
-
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');
|
|
479
639
|
|
|
480
|
-
//
|
|
640
|
+
// Sobrecarga explícita de compatibilidade para deploys mais antigos:
|
|
481
641
|
// { recipient: '+5548999990000', channel: 'whatsapp' }
|
|
482
642
|
await client.documents.sendToken(doc.id, '+5548999990000', 'whatsapp');
|
|
483
643
|
|
|
484
|
-
//
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
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' });
|
|
488
648
|
await client.documents.listTags(doc.id);
|
|
489
|
-
await client.documents.replaceTags(doc.id, [
|
|
490
|
-
await client.documents.addTags(doc.id, [
|
|
491
|
-
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 }
|
|
492
652
|
```
|
|
493
653
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
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.
|
|
497
656
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
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.
|
|
503
661
|
|
|
504
|
-
|
|
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:
|
|
505
664
|
|
|
506
|
-
|
|
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.
|
|
671
|
+
|
|
672
|
+
### Signatários
|
|
507
673
|
|
|
508
674
|
```ts
|
|
509
675
|
await client.signers.create({
|
|
510
|
-
full_name: '
|
|
511
|
-
email: '
|
|
512
|
-
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
|
|
513
679
|
});
|
|
514
|
-
// → { id: '19e6…', full_name: '
|
|
680
|
+
// → { id: '19e6…', full_name: 'João Silva', email: 'joao@exemplo.com.br',
|
|
515
681
|
// whatsapp_phone_number: null, has_accepted_terms: false }
|
|
516
|
-
// (
|
|
517
|
-
|
|
518
|
-
// Both contacts are optional. A name-only signer cannot be notified until a
|
|
519
|
-
// contact is added.
|
|
520
|
-
await client.signers.create({ full_name: 'Contact Pending' });
|
|
682
|
+
// (observação: `cpf` é aceito na entrada, mas a API nunca o devolve)
|
|
521
683
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
});
|
|
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' });
|
|
526
687
|
|
|
527
688
|
await client.signers.get(signerId);
|
|
528
|
-
await client.signers.list({ page: 1,
|
|
689
|
+
await client.signers.list({ page: 1, 'per-page': 50, search: 'joao' });
|
|
529
690
|
await client.signers.update(signerId, {
|
|
530
|
-
full_name: '
|
|
531
|
-
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
|
|
532
693
|
});
|
|
533
694
|
await client.signers.delete(signerId);
|
|
534
695
|
|
|
535
|
-
const
|
|
696
|
+
const existente = await client.signers.findByEmail('joao@exemplo.com.br');
|
|
536
697
|
```
|
|
537
698
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
[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.
|
|
542
702
|
|
|
543
703
|
### Assignments
|
|
544
704
|
|
|
545
705
|
```ts
|
|
546
|
-
//
|
|
706
|
+
// Lista todos os assignments do workspace.
|
|
547
707
|
// → { data: IAssignment[], meta?: { current_page, per_page, total, last_page } }
|
|
548
708
|
const { data, meta } = await client.assignments.list({ page: 1, 'per-page': 20 });
|
|
549
709
|
|
|
550
|
-
//
|
|
710
|
+
// Signatários podem ser ids ou objetos — o SDK normaliza para o formato da API.
|
|
551
711
|
await client.assignments.create(documentId, {
|
|
552
712
|
method: 'virtual',
|
|
553
713
|
signers: ['signer-1', 'signer-2'],
|
|
554
|
-
message: '
|
|
714
|
+
message: 'Por favor, revise e assine',
|
|
555
715
|
expires_at: '2027-12-31T23:59:00Z',
|
|
556
|
-
copy_receivers: ['
|
|
716
|
+
copy_receivers: ['id-do-signatario-em-copia'],
|
|
557
717
|
});
|
|
558
718
|
|
|
559
|
-
//
|
|
719
|
+
// Assinatura sequencial: `step` controla a ordem (paralelo dentro de um passo).
|
|
560
720
|
await client.assignments.create(documentId, {
|
|
561
721
|
method: 'virtual',
|
|
562
722
|
signers: [
|
|
563
723
|
{ id: 'signer-1', step: 1 },
|
|
564
|
-
{ id: 'signer-2', step: 2 }, //
|
|
724
|
+
{ id: 'signer-2', step: 2 }, // notificado só depois que o passo 1 terminar
|
|
565
725
|
],
|
|
566
726
|
});
|
|
567
727
|
|
|
568
|
-
//
|
|
728
|
+
// Campos de `collect` usam pixels da imagem de página a 150 DPI, medidos do canto superior esquerdo.
|
|
569
729
|
await client.assignments.create(documentId, {
|
|
570
730
|
method: 'collect',
|
|
571
731
|
signers: [{ id: signerId }],
|
|
@@ -582,8 +742,8 @@ await client.assignments.create(documentId, {
|
|
|
582
742
|
}],
|
|
583
743
|
});
|
|
584
744
|
|
|
585
|
-
//
|
|
586
|
-
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
|
|
587
747
|
// → {
|
|
588
748
|
// documents: 1, credits: 0, needs_extra_document: false, extra_document_cost: 0,
|
|
589
749
|
// total_credits: 0, breakdown: [], document_balance: 67, credit_balance: 0,
|
|
@@ -591,88 +751,108 @@ await client.assignments.estimateCost(documentId, { signers: [{}] }); // default
|
|
|
591
751
|
// }
|
|
592
752
|
|
|
593
753
|
await client.assignments.resetExpiration(documentId, assignmentId, '2027-06-30T00:00:00Z');
|
|
594
|
-
//
|
|
595
|
-
//
|
|
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.
|
|
596
756
|
await client.assignments.resetExpiration(documentId, assignmentId, null);
|
|
597
757
|
|
|
598
758
|
await client.assignments.resendNotification(documentId, assignmentId, signerId);
|
|
599
759
|
// → { is_sent: true, document_id: '…', signer_id: '…' }
|
|
600
760
|
|
|
601
|
-
const
|
|
602
|
-
//
|
|
603
|
-
// IResendCostEstimate
|
|
604
|
-
//
|
|
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.
|
|
605
765
|
```
|
|
606
766
|
|
|
607
|
-
|
|
767
|
+
A resposta de `create` é um `IAssignment`: `{ id, method, signers: [...],
|
|
608
768
|
items: [{ display_settings, ... }], signing_urls: [{ signer_id, url }], … }`.
|
|
609
769
|
|
|
610
|
-
|
|
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.
|
|
611
772
|
|
|
612
|
-
**
|
|
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:
|
|
613
776
|
|
|
614
777
|
```ts
|
|
615
|
-
await client.documents.delete(documentId);
|
|
616
|
-
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
|
|
617
780
|
```
|
|
618
781
|
|
|
619
|
-
###
|
|
782
|
+
### Ramos pagos de assinatura
|
|
620
783
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
acceptable.
|
|
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.
|
|
624
786
|
|
|
625
|
-
|
|
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`.
|
|
626
789
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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 |
|
|
795
|
+
|
|
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`.
|
|
798
|
+
|
|
799
|
+
#### Verificação e notificação por WhatsApp
|
|
800
|
+
|
|
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:
|
|
630
803
|
|
|
631
804
|
```ts
|
|
632
|
-
const
|
|
633
|
-
full_name: 'Mobile
|
|
805
|
+
const signatarioTelefone = await client.signers.create({
|
|
806
|
+
full_name: 'Signatário Mobile',
|
|
634
807
|
whatsapp_phone_number: '+5511999990000',
|
|
635
808
|
});
|
|
636
809
|
|
|
637
|
-
const
|
|
810
|
+
const custoWhatsapp = await client.assignments.estimateCost(documentId, {
|
|
638
811
|
method: 'virtual',
|
|
639
812
|
signers: [{ verification_method: 'Whatsapp', notification_methods: ['Whatsapp'] }],
|
|
640
813
|
});
|
|
641
814
|
|
|
642
|
-
const
|
|
815
|
+
const assignmentWhatsapp = await client.assignments.create(documentId, {
|
|
643
816
|
method: 'virtual',
|
|
644
817
|
signers: [{
|
|
645
|
-
id:
|
|
818
|
+
id: signatarioTelefone.id,
|
|
646
819
|
verification_method: 'Whatsapp',
|
|
647
820
|
notification_methods: ['Whatsapp'],
|
|
648
821
|
}],
|
|
649
822
|
});
|
|
650
823
|
|
|
651
|
-
const
|
|
824
|
+
const avisos = await client.assignments.listWhatsAppNotifications(
|
|
652
825
|
documentId,
|
|
653
|
-
|
|
826
|
+
assignmentWhatsapp.id,
|
|
654
827
|
);
|
|
655
828
|
// IWhatsAppNotification[]:
|
|
656
829
|
// [{ sent_at, header, body, buttons: [{ text, url? }], phone_number, signer_id }]
|
|
657
830
|
```
|
|
658
831
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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.
|
|
662
835
|
|
|
663
|
-
#### ICP-Brasil
|
|
836
|
+
#### Certificado digital ICP-Brasil
|
|
664
837
|
|
|
665
|
-
`DigitalCertificate`
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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.
|
|
841
|
+
|
|
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.
|
|
669
849
|
|
|
670
850
|
```ts
|
|
671
|
-
const
|
|
851
|
+
const signatarioCertificado = await client.signers.update(signerId, {
|
|
672
852
|
government_id: '390.533.447-05',
|
|
673
853
|
});
|
|
674
854
|
|
|
675
|
-
const
|
|
855
|
+
const custoCertificado = await client.assignments.estimateCost(documentId, {
|
|
676
856
|
method: 'virtual',
|
|
677
857
|
signers: [{ verification_method: 'DigitalCertificate', notification_methods: ['Email'] }],
|
|
678
858
|
});
|
|
@@ -680,7 +860,7 @@ const certificateCost = await client.assignments.estimateCost(documentId, {
|
|
|
680
860
|
await client.assignments.create(documentId, {
|
|
681
861
|
method: 'virtual',
|
|
682
862
|
signers: [{
|
|
683
|
-
id:
|
|
863
|
+
id: signatarioCertificado.id,
|
|
684
864
|
step: 1,
|
|
685
865
|
verification_method: 'DigitalCertificate',
|
|
686
866
|
notification_methods: ['Email'],
|
|
@@ -688,27 +868,37 @@ await client.assignments.create(documentId, {
|
|
|
688
868
|
});
|
|
689
869
|
```
|
|
690
870
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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.
|
|
696
887
|
|
|
697
888
|
### Templates
|
|
698
889
|
|
|
699
|
-
`templates.list()`
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
[
|
|
703
|
-
|
|
704
|
-
`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.
|
|
705
895
|
|
|
706
896
|
```ts
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
const
|
|
710
|
-
{ filePath: './nda.pdf' }, //
|
|
711
|
-
{ 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' },
|
|
712
902
|
);
|
|
713
903
|
// →
|
|
714
904
|
// {
|
|
@@ -718,28 +908,28 @@ const created = await client.templates.create(
|
|
|
718
908
|
// pages: [], tags: [], created_at: '2026-…', updated_at: '2026-…'
|
|
719
909
|
// }
|
|
720
910
|
|
|
721
|
-
const { data, meta } = await client.templates.list({ search: 'NDA',
|
|
722
|
-
const template = await client.templates.get(
|
|
723
|
-
await client.templates.update(
|
|
724
|
-
const
|
|
725
|
-
if (
|
|
726
|
-
await client.templates.delete(
|
|
727
|
-
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
(
|
|
733
|
-
&&
|
|
911
|
+
const { data, meta } = await client.templates.list({ search: 'NDA', 'per-page': 20 });
|
|
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',
|
|
734
924
|
);
|
|
735
|
-
if (!
|
|
925
|
+
if (!papelSignatario) throw new Error('O template não tem papel de signatário');
|
|
736
926
|
await client.documents.createFromTemplate(
|
|
737
927
|
templateId,
|
|
738
|
-
[{ role_id:
|
|
739
|
-
{ 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.' },
|
|
740
930
|
);
|
|
741
931
|
|
|
742
|
-
//
|
|
932
|
+
// Estime o custo antes de criar → ICostEstimate
|
|
743
933
|
await client.documents.estimateCostFromTemplate(templateId, [
|
|
744
934
|
{ role_id: 'role_id', verification_method: 'Email', notification_methods: ['Email'] },
|
|
745
935
|
]);
|
|
@@ -747,41 +937,42 @@ await client.documents.estimateCostFromTemplate(templateId, [
|
|
|
747
937
|
// has_sufficient_resources: true, blocking_reason: null, breakdown: [], … }
|
|
748
938
|
```
|
|
749
939
|
|
|
750
|
-
|
|
751
|
-
`verification_method: 'DigitalCertificate'
|
|
752
|
-
[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).
|
|
753
943
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
`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.
|
|
758
947
|
|
|
759
948
|
### Tags
|
|
760
949
|
|
|
761
|
-
|
|
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).
|
|
762
952
|
|
|
763
953
|
```ts
|
|
764
|
-
await client.tags.list({ search: '
|
|
765
|
-
const tag = await client.tags.create({ name: '
|
|
766
|
-
await client.tags.update(tag.id, { name: '
|
|
767
|
-
await client.tags.update(tag.id, { color: null }); //
|
|
768
|
-
await client.tags.delete(tag.id); // 409
|
|
769
|
-
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
|
|
770
960
|
```
|
|
771
961
|
|
|
772
|
-
|
|
962
|
+
Anexe/desanexe tags em um documento específico com `client.documents.listTags / replaceTags /
|
|
963
|
+
addTags / detachTag` (veja [Documentos](#documentos)).
|
|
773
964
|
|
|
774
965
|
### Workspaces
|
|
775
966
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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.
|
|
779
970
|
|
|
780
971
|
```ts
|
|
781
|
-
//
|
|
782
|
-
// '#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.
|
|
783
974
|
await client.workspaces.create({
|
|
784
|
-
name: '
|
|
975
|
+
name: 'Meu Workspace',
|
|
785
976
|
notification_sender_type: 'Account',
|
|
786
977
|
primary_color: 'ff0066',
|
|
787
978
|
secondary_color: '0066ff',
|
|
@@ -790,13 +981,13 @@ await client.workspaces.create({
|
|
|
790
981
|
await client.workspaces.list();
|
|
791
982
|
await client.workspaces.get(accountId);
|
|
792
983
|
await client.workspaces.update(accountId, {
|
|
793
|
-
name: '
|
|
984
|
+
name: 'Renomeado',
|
|
794
985
|
notification_sender_type: 'User',
|
|
795
986
|
primary_color: '112233',
|
|
796
987
|
});
|
|
797
988
|
|
|
798
|
-
//
|
|
799
|
-
const
|
|
989
|
+
// Identidade visual
|
|
990
|
+
const tema = await client.workspaces.getTheme(accountId);
|
|
800
991
|
const logo = await client.workspaces.downloadLogo(accountId); // Buffer
|
|
801
992
|
await client.workspaces.uploadLogo(accountId, { filePath: './logo.png' });
|
|
802
993
|
await client.workspaces.uploadLogo(accountId, {
|
|
@@ -806,7 +997,7 @@ await client.workspaces.uploadLogo(accountId, {
|
|
|
806
997
|
});
|
|
807
998
|
await client.workspaces.deleteLogo(accountId);
|
|
808
999
|
|
|
809
|
-
//
|
|
1000
|
+
// Últimos 12 meses por padrão; estatísticas diárias exigem um mês YYYY-MM.
|
|
810
1001
|
await client.workspaces.getStats(accountId);
|
|
811
1002
|
await client.workspaces.getStats(accountId, {
|
|
812
1003
|
granularity: 'daily',
|
|
@@ -814,97 +1005,100 @@ await client.workspaces.getStats(accountId, {
|
|
|
814
1005
|
});
|
|
815
1006
|
|
|
816
1007
|
await client.workspaces.delete(accountId);
|
|
817
|
-
// `force`
|
|
818
|
-
//
|
|
819
|
-
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 });
|
|
820
1011
|
```
|
|
821
1012
|
|
|
822
|
-
###
|
|
1013
|
+
### Definições de campo
|
|
823
1014
|
|
|
824
|
-
|
|
1015
|
+
Tipos de campo personalizados usados por assignments com método `collect`.
|
|
825
1016
|
|
|
826
1017
|
```ts
|
|
827
|
-
await client.fields.create({ type: 'text', name: '
|
|
1018
|
+
await client.fields.create({ type: 'text', name: 'Número do contrato' });
|
|
828
1019
|
await client.fields.list({ include_inactive: true, include_standard: true });
|
|
829
1020
|
await client.fields.get(fieldId);
|
|
830
|
-
await client.fields.update(fieldId, { name: '
|
|
1021
|
+
await client.fields.update(fieldId, { name: 'Nome atualizado' });
|
|
831
1022
|
await client.fields.delete(fieldId);
|
|
832
1023
|
|
|
833
|
-
//
|
|
1024
|
+
// Valida um único valor (o código de acesso só é exigido nas chamadas do signatário)
|
|
834
1025
|
await client.fields.validate(fieldId, '400.676.228-36', { signerAccessCode });
|
|
835
1026
|
|
|
836
|
-
//
|
|
1027
|
+
// Valida vários valores de uma vez
|
|
837
1028
|
await client.fields.validateMultiple(
|
|
838
1029
|
[
|
|
839
1030
|
{ field_id: 'f1', value: '1111111111111' },
|
|
840
|
-
{ field_id: 'f2', value: '
|
|
1031
|
+
{ field_id: 'f2', value: 'valor@exemplo.com.br' },
|
|
841
1032
|
],
|
|
842
1033
|
{ signerAccessCode },
|
|
843
1034
|
);
|
|
844
1035
|
|
|
845
|
-
//
|
|
1036
|
+
// Catálogo de todos os tipos de campo reconhecidos pela plataforma
|
|
846
1037
|
await client.fields.listTypes();
|
|
847
1038
|
```
|
|
848
1039
|
|
|
849
|
-
###
|
|
1040
|
+
### Autenticação / gestão de chaves de API
|
|
850
1041
|
|
|
851
|
-
|
|
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).
|
|
852
1045
|
|
|
853
1046
|
```ts
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
|
|
857
|
-
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();
|
|
858
1052
|
|
|
859
|
-
const { access_token, user, accounts } = await client.auth.login('
|
|
1053
|
+
const { access_token, user, accounts } = await client.auth.login('eu@exemplo.com.br', 'senha');
|
|
860
1054
|
await client.auth.socialLogin({ provider: 'google', token: 'google-id-token', has_accepted_terms: true });
|
|
861
1055
|
await client.auth.linkSocialLogin({ provider: 'google', token: 'google-id-token' });
|
|
862
1056
|
|
|
863
|
-
//
|
|
864
|
-
await client.auth.createApiKey('
|
|
865
|
-
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
|
|
866
1060
|
await client.auth.deleteApiKey();
|
|
867
1061
|
|
|
868
|
-
//
|
|
869
|
-
await client.auth.changePassword({ email, password: '
|
|
870
|
-
await client.auth.requestPasswordReset('
|
|
871
|
-
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' });
|
|
872
1066
|
```
|
|
873
1067
|
|
|
874
|
-
###
|
|
1068
|
+
### Usuário autenticado
|
|
875
1069
|
|
|
876
1070
|
```ts
|
|
877
|
-
const
|
|
1071
|
+
const usuario = await client.users.getCurrent();
|
|
878
1072
|
// → { id, name, email, telephone, government_id, is_email_verified,
|
|
879
1073
|
// has_accepted_terms, created_at, to_be_deleted_at }
|
|
880
1074
|
|
|
881
|
-
//
|
|
882
|
-
const
|
|
883
|
-
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({
|
|
884
1078
|
granularity: 'daily',
|
|
885
1079
|
month: '2026-06',
|
|
886
1080
|
});
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
//
|
|
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.
|
|
890
1084
|
|
|
891
|
-
const
|
|
1085
|
+
const preferencias = await client.users.getNotificationPreferences();
|
|
892
1086
|
await client.users.updateNotificationPreferences({
|
|
893
1087
|
SignerDeclined: false,
|
|
894
1088
|
DocumentExpired: false,
|
|
895
1089
|
});
|
|
896
|
-
//
|
|
897
|
-
//
|
|
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.
|
|
898
1092
|
```
|
|
899
1093
|
|
|
900
1094
|
### Webhooks
|
|
901
1095
|
|
|
902
1096
|
```ts
|
|
903
1097
|
await client.webhooks.register({
|
|
904
|
-
url: 'https://
|
|
905
|
-
email: 'admin@
|
|
1098
|
+
url: 'https://exemplo.com.br/webhooks/assinafy',
|
|
1099
|
+
email: 'admin@exemplo.com.br',
|
|
906
1100
|
is_active: true,
|
|
907
|
-
// events
|
|
1101
|
+
// `events` assume o conjunto padrão do SDK, abaixo
|
|
908
1102
|
events: [
|
|
909
1103
|
'document_ready',
|
|
910
1104
|
'document_prepared',
|
|
@@ -915,26 +1109,25 @@ await client.webhooks.register({
|
|
|
915
1109
|
});
|
|
916
1110
|
|
|
917
1111
|
await client.webhooks.get(); // IWebhookSubscription | null
|
|
918
|
-
await client.webhooks.inactivate(); //
|
|
1112
|
+
await client.webhooks.inactivate(); // interrompe entregas (não existe rota de exclusão)
|
|
919
1113
|
await client.webhooks.listEventTypes();
|
|
920
|
-
const
|
|
1114
|
+
const historico = await client.webhooks.listDispatches({
|
|
921
1115
|
delivered: false,
|
|
922
1116
|
page: 1,
|
|
923
1117
|
'per-page': 20,
|
|
924
1118
|
}); // { data: IWebhookDispatch[], meta?: PaginationMeta }
|
|
925
|
-
const
|
|
1119
|
+
const reenviado = await client.webhooks.retryDispatch(dispatchId); // IWebhookDispatch
|
|
926
1120
|
```
|
|
927
1121
|
|
|
928
|
-
`register`
|
|
929
|
-
`{ events, is_active, url, email, updated_at? }`. Assinafy
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
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.
|
|
936
1129
|
|
|
937
|
-
|
|
1130
|
+
Cada item do histórico (ou resultado de reenvio) é um `IWebhookDispatch`:
|
|
938
1131
|
|
|
939
1132
|
```ts
|
|
940
1133
|
{
|
|
@@ -953,25 +1146,25 @@ Each history or retry result is an `IWebhookDispatch`:
|
|
|
953
1146
|
}
|
|
954
1147
|
```
|
|
955
1148
|
|
|
956
|
-
|
|
1149
|
+
Todo corpo de entrega usa este envelope:
|
|
957
1150
|
|
|
958
1151
|
```ts
|
|
959
1152
|
{
|
|
960
|
-
id: number; // use
|
|
1153
|
+
id: number; // use para processamento idempotente
|
|
961
1154
|
event: string;
|
|
962
1155
|
message: string | null;
|
|
963
1156
|
payload: Record<string, unknown> | null;
|
|
964
1157
|
origin: { ip?: string; 'user-agent'?: string } | null;
|
|
965
|
-
created_at: number; // Unix
|
|
1158
|
+
created_at: number; // segundos Unix
|
|
966
1159
|
subject: { type: 'User' | 'Signer' | 'Account' | 'Document' | 'Template'; [key: string]: unknown };
|
|
967
1160
|
object: { type: 'User' | 'Signer' | 'Account' | 'Document' | 'Template'; [key: string]: unknown };
|
|
968
1161
|
account_id: string;
|
|
969
1162
|
}
|
|
970
1163
|
```
|
|
971
1164
|
|
|
972
|
-
|
|
1165
|
+
Os valores por evento são:
|
|
973
1166
|
|
|
974
|
-
| `event` | `subject.type` | `object.type` | `payload`
|
|
1167
|
+
| `event` | `subject.type` | `object.type` | chaves de `payload` |
|
|
975
1168
|
| --- | --- | --- | --- |
|
|
976
1169
|
| `document_uploaded` | `User` | `Document` | — |
|
|
977
1170
|
| `document_metadata_ready` | `User` | `Document` | — |
|
|
@@ -979,7 +1172,7 @@ Event-specific values are:
|
|
|
979
1172
|
| `assignment_created` | `User` | `Document` | `user_name`, `user_email`, `user_telephone` |
|
|
980
1173
|
| `document_ready` | `Account` | `Document` | — |
|
|
981
1174
|
| `document_processing_failed` | `Account` | `Document` | `error_message` |
|
|
982
|
-
| `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 |
|
|
983
1176
|
| `signer_created` | `User` | `Signer` | `signer_full_name` |
|
|
984
1177
|
| `signer_email_verified` | `Signer` | `Document` | `signer_email` |
|
|
985
1178
|
| `signer_whatsapp_verified` | `Signer` | `Document` | `signer_whatsapp_phone_number` |
|
|
@@ -992,21 +1185,19 @@ Event-specific values are:
|
|
|
992
1185
|
| `template_processed` | `User` | `Template` | — |
|
|
993
1186
|
| `template_processing_failed` | `Account` | `Template` | `error_message` |
|
|
994
1187
|
|
|
995
|
-
`payload`, `subject
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
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.
|
|
1001
1193
|
|
|
1002
|
-
###
|
|
1194
|
+
### Verificação de webhooks
|
|
1003
1195
|
|
|
1004
|
-
`WebhookVerifier`
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
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.
|
|
1010
1201
|
|
|
1011
1202
|
```ts
|
|
1012
1203
|
import express from 'express';
|
|
@@ -1014,159 +1205,209 @@ import express from 'express';
|
|
|
1014
1205
|
const webhookSecret = process.env.ASSINAFY_WEBHOOK_SECRET;
|
|
1015
1206
|
const signatureHeader = process.env.ASSINAFY_SIGNATURE_HEADER;
|
|
1016
1207
|
if (!webhookSecret || !signatureHeader) {
|
|
1017
|
-
throw new Error('
|
|
1208
|
+
throw new Error('Este ambiente não tem contrato confirmado de assinatura de webhook');
|
|
1018
1209
|
}
|
|
1019
1210
|
|
|
1020
1211
|
const webhookClient = new AssinafyClient({ webhookSecret });
|
|
1021
1212
|
|
|
1022
1213
|
app.post('/webhooks/assinafy', express.raw({ type: 'application/json' }), (req, res) => {
|
|
1023
|
-
const
|
|
1024
|
-
const
|
|
1214
|
+
const assinatura = req.header(signatureHeader) ?? '';
|
|
1215
|
+
const corpoBruto = req.body as Buffer;
|
|
1025
1216
|
|
|
1026
|
-
if (!webhookClient.webhookVerifier.verify(
|
|
1027
|
-
return res.status(401).send('
|
|
1217
|
+
if (!webhookClient.webhookVerifier.verify(corpoBruto, assinatura)) {
|
|
1218
|
+
return res.status(401).send('Assinatura inválida');
|
|
1028
1219
|
}
|
|
1029
1220
|
|
|
1030
|
-
const
|
|
1031
|
-
const
|
|
1032
|
-
const
|
|
1221
|
+
const evento = webhookClient.webhookVerifier.extractEvent(corpoBruto);
|
|
1222
|
+
const tipo = webhookClient.webhookVerifier.getEventType(evento);
|
|
1223
|
+
const dados = webhookClient.webhookVerifier.getEventData(evento);
|
|
1033
1224
|
|
|
1034
|
-
switch (
|
|
1035
|
-
case 'document_ready':
|
|
1036
|
-
case 'signer_signed_document':
|
|
1037
|
-
case 'signer_rejected_document':
|
|
1038
|
-
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;
|
|
1039
1230
|
}
|
|
1040
1231
|
res.sendStatus(200);
|
|
1041
1232
|
});
|
|
1042
1233
|
```
|
|
1043
1234
|
|
|
1044
|
-
###
|
|
1235
|
+
### Endpoints do signatário
|
|
1045
1236
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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.
|
|
1050
1241
|
|
|
1051
1242
|
```ts
|
|
1052
1243
|
await client.signerDocuments.self(accessCode);
|
|
1053
1244
|
await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verificationCode: '123456' });
|
|
1054
1245
|
|
|
1055
1246
|
await client.signerDocuments.getCurrent(signerId, accessCode);
|
|
1056
|
-
const { data } = await client.signerDocuments.list(signerId, accessCode, {
|
|
1057
|
-
//
|
|
1058
|
-
const
|
|
1247
|
+
const { data } = await client.signerDocuments.list(signerId, accessCode, { 'per-page': 20 });
|
|
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');
|
|
1059
1250
|
await client.signerDocuments.download(signerId, documentId, 'original');
|
|
1060
|
-
//
|
|
1251
|
+
// Disponível só depois que um signatário por certificado ICP-Brasil concluir a assinatura.
|
|
1061
1252
|
await client.signerDocuments.download(signerId, documentId, 'pades');
|
|
1062
1253
|
|
|
1063
1254
|
await client.signerDocuments.confirmData(documentId, accessCode, {
|
|
1064
|
-
email: '
|
|
1065
|
-
full_name: '
|
|
1255
|
+
email: 'eu@exemplo.com.br',
|
|
1256
|
+
full_name: 'Signatário Exemplo',
|
|
1066
1257
|
government_id: '123.456.789-00',
|
|
1067
1258
|
has_accepted_terms: true,
|
|
1068
1259
|
});
|
|
1069
|
-
//
|
|
1260
|
+
// Como alternativa, aceite os termos separadamente antes de getAssignment():
|
|
1070
1261
|
// await client.signerDocuments.acceptTerms(accessCode);
|
|
1071
1262
|
|
|
1072
|
-
//
|
|
1263
|
+
// Gestão da imagem de assinatura ({ reuse: true } guarda para documentos futuros)
|
|
1073
1264
|
await client.signerDocuments.uploadSignature(accessCode, pngBuffer, { imageType: 'signature', reuse: true });
|
|
1074
1265
|
await client.signerDocuments.downloadSignature(accessCode, 'signature');
|
|
1075
1266
|
|
|
1076
|
-
//
|
|
1077
|
-
const
|
|
1078
|
-
// `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.
|
|
1079
1270
|
await client.signerDocuments.sign(documentId, assignmentId, accessCode, [
|
|
1080
|
-
{ itemId, fieldId, pageId, value: '
|
|
1271
|
+
{ itemId, fieldId, pageId, value: 'Assinado por João' },
|
|
1081
1272
|
]);
|
|
1082
|
-
await client.signerDocuments.decline(documentId, assignmentId, accessCode, '
|
|
1273
|
+
await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'Sem autorização');
|
|
1083
1274
|
|
|
1084
|
-
// `signMultiple()`
|
|
1275
|
+
// `signMultiple()` é só para assignments virtual.
|
|
1085
1276
|
await client.signerDocuments.signMultiple(['doc-1', 'doc-2'], accessCode);
|
|
1086
|
-
await client.signerDocuments.declineMultiple(['doc-1'], '
|
|
1277
|
+
await client.signerDocuments.declineMultiple(['doc-1'], 'Condições desfavoráveis', accessCode);
|
|
1087
1278
|
```
|
|
1088
1279
|
|
|
1089
|
-
`sign()`
|
|
1090
|
-
|
|
1091
|
-
|
|
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).
|
|
1092
1283
|
|
|
1093
|
-
##
|
|
1284
|
+
## Helper de alto nível
|
|
1094
1285
|
|
|
1095
|
-
|
|
1096
|
-
|
|
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.
|
|
1097
1288
|
|
|
1098
1289
|
```ts
|
|
1099
|
-
const
|
|
1100
|
-
source: { filePath: './
|
|
1290
|
+
const resultado = await client.uploadAndRequestSignatures({
|
|
1291
|
+
source: { filePath: './contrato.pdf' },
|
|
1101
1292
|
signers: [
|
|
1102
|
-
{ name: '
|
|
1103
|
-
{ name: '
|
|
1293
|
+
{ name: 'João', email: 'joao@exemplo.com.br' },
|
|
1294
|
+
{ name: 'Maria', email: 'maria@exemplo.com.br' },
|
|
1104
1295
|
],
|
|
1105
|
-
message: '
|
|
1106
|
-
metadata: {
|
|
1296
|
+
message: 'Por favor, assine',
|
|
1297
|
+
metadata: { ano: 2026 }, // parte de compatibilidade; omita para o formato só-arquivo
|
|
1107
1298
|
waitForReady: true,
|
|
1108
1299
|
waitOptions: { maxWaitMs: 30_000, pollIntervalMs: 1_000 },
|
|
1109
1300
|
expiresAt: '2027-12-31T00:00:00Z',
|
|
1110
|
-
copyReceivers: ['
|
|
1301
|
+
copyReceivers: ['id-de-signatario-em-copia'],
|
|
1111
1302
|
});
|
|
1112
1303
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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[]
|
|
1117
1308
|
```
|
|
1118
1309
|
|
|
1119
|
-
`waitForReady: false`
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
signer IDs, not email addresses; check the returned assignment before treating
|
|
1127
|
-
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.
|
|
1128
1317
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
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.
|
|
1133
1321
|
|
|
1134
|
-
##
|
|
1322
|
+
## Erros
|
|
1135
1323
|
|
|
1136
|
-
HTTP
|
|
1137
|
-
|
|
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` |
|
|
1138
1334
|
|
|
1139
1335
|
```ts
|
|
1140
|
-
import { ApiError, ValidationError, NetworkError, AssinafyError } from '@assinafy/sdk';
|
|
1336
|
+
import { ApiError, OAuthError, ValidationError, NetworkError, AssinafyError } from '@assinafy/sdk';
|
|
1141
1337
|
|
|
1142
1338
|
try {
|
|
1143
1339
|
await client.documents.upload({ filePath: './x.pdf' });
|
|
1144
1340
|
} catch (err) {
|
|
1145
1341
|
if (err instanceof ValidationError) {
|
|
1146
|
-
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);
|
|
1147
1346
|
} else if (err instanceof ApiError) {
|
|
1148
|
-
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.
|
|
1149
1350
|
} else if (err instanceof NetworkError) {
|
|
1150
|
-
console.error('
|
|
1351
|
+
console.error('Erro de rede:', err.message);
|
|
1151
1352
|
} else if (err instanceof AssinafyError) {
|
|
1152
|
-
console.error('SDK
|
|
1353
|
+
console.error('Erro do SDK:', err.message, err.context);
|
|
1153
1354
|
}
|
|
1154
1355
|
}
|
|
1155
1356
|
```
|
|
1156
1357
|
|
|
1157
|
-
##
|
|
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
|
|
1158
1390
|
|
|
1159
1391
|
```bash
|
|
1160
1392
|
bun install --frozen-lockfile
|
|
1161
|
-
bun run typecheck #
|
|
1393
|
+
bun run typecheck # tipos de src, scripts e testes
|
|
1162
1394
|
bun run lint
|
|
1163
|
-
bun test # bun:test
|
|
1395
|
+
bun test # suítes bun:test
|
|
1164
1396
|
bun run test:coverage
|
|
1165
1397
|
bun run build # tsup → dist/ (CJS + ESM + .d.ts)
|
|
1166
1398
|
bun run lint:pkg # publint + arethetypeswrong
|
|
1167
|
-
bun run
|
|
1399
|
+
bun run audit:api # confere o SDK contra o OpenAPI publicado
|
|
1400
|
+
bun run verify # portão completo de release local
|
|
1168
1401
|
```
|
|
1169
1402
|
|
|
1170
|
-
##
|
|
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
|
|
1171
1412
|
|
|
1172
|
-
MIT
|
|
1413
|
+
Distribuído sob a licença [MIT](LICENSE).
|