# SignXP Docs
> Assinatura eletrônica com evidências e selo PAdES, por API.
Documento gerado automaticamente a partir da documentação oficial em https://docs.signxp.com.br/llms.txt.
---
# Introdução
O **SignXP** é um serviço de assinatura eletrônica pensado para ser usado *por outros softwares*. Não há tela para o seu usuário final operar o sistema: o seu software envia o documento por API e o SignXP cuida de tudo o que vem depois — convite ao signatário, coleta de evidências, captura da assinatura, selagem criptográfica e verificação pública.
## O que o serviço entrega
- **Assinatura eletrônica com evidências**: IP, dispositivo, data/hora, geolocalização, consentimento LGPD e, opcionalmente, foto do documento de identidade e selfie.
- **Validação por IA das imagens**: as fotos são conferidas na hora da assinatura e, se você informar o CPF/CNPJ do signatário, a IA verifica se o documento apresentado é mesmo dele — veja [validação por IA](/validacao-por-ia).
- **Selo criptográfico PAdES**: o PDF final é assinado com o certificado do SignXP, o que torna qualquer alteração posterior detectável.
- **Termo de Evidências**: um relatório anexado ao PDF final, com uma página por signatário.
- **Verificação pública**: cada documento ganha uma URL e um QR Code que qualquer pessoa pode usar para conferir a autenticidade. Quem só tem o arquivo em mãos confere pelo [validador público](https://validar.signxp.com.br), por código ou enviando o próprio PDF.
- **Assinatura dentro do seu sistema**: se preferir que o seu cliente não saia da sua tela, a assinatura pode ser [embutida](/assinatura-embutida) — sem apontar domínio nenhum para nós.
## Como o fluxo funciona
1. O seu sistema gera o PDF e sabe em que posição de cada página a assinatura deve aparecer.
2. O seu sistema chama `POST /api/v1/documents` enviando o PDF, os signatários e as coordenadas dos campos.
3. O SignXP registra os campos e cria um link de assinatura por signatário. Se o signatário tem e-mail e você não pediu o contrário, o convite sai por e-mail; senão, o link fica com você para entregar pelo canal que preferir.
4. O signatário assina pelo celular ou pelo computador, passando pelas etapas de evidência configuradas.
5. Quando todos assinam, o PDF é selado e o seu sistema recebe um **callback assinado**.
6. O seu sistema baixa a via final ou apenas guarda a URL de verificação.
```
Seu software ──POST /documents──▶ SignXP ──convite──▶ Signatário
▲ │
└────────callback assinado─────────┘
```
## Antes de começar
Você vai precisar de:
- Uma **conta no portal**, que você mesmo cria em [app.signxp.com.br/criar-conta](https://app.signxp.com.br/criar-conta) — não é preciso falar com ninguém para começar.
- Uma **chave de API** de teste e uma de produção, emitidas por você no portal (veja [Autenticação](/autenticacao)).
- Um **endpoint público** no seu sistema para receber os callbacks (opcional, mas recomendado).
- As coordenadas de cada campo de assinatura. O **[Posicionador de campos](/posicionador)** gera esse bloco para você a partir do próprio PDF.
> O ambiente de teste não envia e-mail para o signatário e não consome a sua quota contratada. Use-o à vontade durante a integração.
Enquanto a conta não tiver um plano contratado, ela não envia documentos em produção. Isso não atrapalha o começo: a integração inteira pode ser escrita e exercitada antes disso, com a chave de teste.
## Testando sem escrever código
As páginas de **Autenticação**, **Enviar um documento** e **Consultar o status** trazem um painel *Testar agora*: você cola uma chave `sk_test_`, ajusta o corpo já preenchido com dados fictícios (inclusive o PDF) e executa a chamada de verdade, vendo a resposta na hora.
Comece pela [Autenticação](/autenticacao): a consulta da licença não cria nada e já prova que a sua chave funciona.
## Integrando com um agente de IA
Toda esta documentação também é publicada em texto puro, no formato [llms.txt](https://llmstxt.org):
| Endereço | Conteúdo |
|---|---|
| `/llms.txt` | Índice de todas as páginas, com um resumo de cada uma. |
| `/llms-full.txt` | Toda a documentação em um único arquivo. |
| `/{pagina}.md` | Uma página específica em Markdown. |
Esses endereços são públicos e não exigem login — aponte o seu agente para eles e ele lê a integração inteira sozinho.
---
# Autenticação e ambientes
Todas as chamadas são autenticadas por uma **chave de API** da sua licença, enviada no cabeçalho `Authorization`.
```http
POST /api/v1/documents HTTP/1.1
Host: api.signxp.com.br
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
```
## Ambientes
O prefixo da chave indica o ambiente:
| Prefixo | Ambiente | Comportamento |
|---|---|---|
| `sk_live_` | Produção | Envia o convite por e-mail ao signatário e consome a quota contratada. |
| `sk_test_` | Teste | **Nunca** envia e-mail e **não** consome quota. |
Em produção o convite por e-mail depende de duas coisas: o signatário ter
`email` e o envio não pedir `delivery: "none"`. Nos dois ambientes o
`signing_url` de cada signatário vem na [consulta de status](/consultar-status),
então você sempre tem como entregar o link por conta própria.
Use a chave de teste durante toda a integração. O fluxo é idêntico ao de produção — inclusive a selagem do PDF e os callbacks.
## Consultando a sua licença
Uma chave sabe responder sobre si mesma. Use este endpoint no arranque do seu
sistema — ou numa tela de diagnóstico — para conferir qual ambiente a chave abre,
quanto da quota já foi gasto e quais recursos a licença inclui, sem depender do
portal nem do suporte:
```http
GET /api/v1/license
```
```json
{
"client": { "reference": "acme", "name": "Acme Indústria Ltda." },
"key": { "name": "servidor de produção", "environment": "live", "masked": "sk_live_••••3f2a" },
"license": {
"plan": "Profissional",
"status": "active",
"usable": true,
"block_reason": null,
"starts_at": "2026-01-01",
"expires_at": "2026-12-31",
"rate_limit_per_minute": 60,
"features": {
"ai_validation": true,
"evidence_capture": true,
"initials_and_order": true,
"public_verification": true
}
},
"usage": {
"period": "2026-07",
"documents_used": 139,
"documents_limit": 500,
"documents_remaining": 361
}
}
```
`usable` é a pergunta que importa antes de enviar: quando ele é `false`,
`block_reason` traz em português por que os envios estão bloqueados (licença fora
de vigência, suspensa, cliente inativo). `features` diz o que a licença libera —
é o que evita descobrir por um `422` que a rúbrica não está contratada. Veja
[erros e limites](/erros-e-limites).
A consulta não consome quota e continua respondendo mesmo quando os envios estão
bloqueados — é justamente aí que ela é útil.
## Gerenciando as chaves
Em **[Chaves de API](https://app.signxp.com.br/chaves)**, no portal da sua conta, você pode:
- emitir novas chaves (por exemplo, uma por servidor);
- revogar uma chave imediatamente;
- **agendar a expiração** em 24h ou 72h, para rotacionar sem derrubar a integração.
A chave em claro aparece **uma única vez**, no momento da emissão. Guarde-a em um cofre de segredos; nós armazenamos apenas o hash.
> Nunca coloque a chave em código do lado do cliente (navegador ou app). Ela dá acesso a todos os documentos da sua licença.
## Rotação recomendada
1. Emita a nova chave.
2. Agende a expiração da antiga para 24h ou 72h.
3. Publique o seu sistema usando a nova chave.
4. Confirme, em **Registros**, que nenhuma chamada usa mais a chave antiga.
## Erros de autenticação
| Código | Significado |
|---|---|
| `401` | Chave ausente, inválida, revogada ou expirada. |
| `403` | Cliente inativo — fale com o time SignXP. |
| `402` | Licença fora de vigência ou quota mensal esgotada. |
Um `402` **não** interrompe as assinaturas já em andamento: os links continuam válidos e a consulta de status segue funcionando. Apenas o envio de novos documentos é bloqueado.
---
# Enviar um documento
```http
POST /api/v1/documents
```
Cria o documento, posiciona os campos de assinatura nas coordenadas informadas e convida o primeiro signatário. Responde **202 Accepted** — o processamento do PDF é assíncrono.
## Corpo da requisição
```json
{
"external_id": "contrato-2026-0001",
"source_system": "meu-erp",
"title": "Contrato de Prestação de Serviços",
"callback_url": "https://meu-sistema.com.br/webhooks/signxp",
"download_policy": "all_signed",
"customer": { "reference": "cli-4471", "name": "Acme Indústria Ltda." },
"evidence": { "document": true, "selfie": true },
"pdf_base64": "JVBERi0xLjcK...",
"signers": [
{
"key": "contratante",
"name": "Maria Souza",
"email": "maria@empresa.com.br",
"tax_id": "529.982.247-25",
"role": "SIGNER",
"signing_order": 1
},
{
"key": "contratada",
"name": "João Lima",
"email": "joao@fornecedor.com.br",
"signing_order": 2
}
],
"fields": [
{ "signer_key": "contratante", "type": "SIGNATURE", "page": 3, "x": 12.5, "y": 78 },
{ "signer_key": "contratada", "type": "SIGNATURE", "page": 3, "x": 55, "y": 78 },
{ "signer_key": "contratante", "type": "INITIALS", "page": 1, "x": 80, "y": 90, "width": 14, "height": 7 }
]
}
```
### Campos
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `title` | string | sim | Nome do documento, exibido ao signatário e no painel. |
| `pdf_base64` | string | sim* | PDF em base64. *Alternativamente envie `pdf` como upload multipart (máx. 20 MB). |
| `signers` | array | sim | Ao menos um signatário. |
| `fields` | array | sim* | Ao menos um campo, e todo signatário com papel `SIGNER` precisa de pelo menos um. *Não se aplica (e é recusado) quando `placement` é `interactive` ou `signature_page`. |
| `external_id` | string | não | O identificador do documento no **seu** sistema. Deve ser único por licença. |
| `source_system` | string | não | Nome do sistema de origem, útil quando você integra vários. |
| `callback_url` | url | não | Recebe as notificações de mudança de status. |
| `download_policy` | enum | não | `all_signed` (padrão), `anytime` ou `disabled`. Ignorado quando a sua conta desliga a entrega pelo SignXP — veja [verificação](/verificacao). |
| `delivery` | enum | não | Quem entrega o link ao signatário: `email` (padrão) ou `none`. Veja "Entregando o link você mesmo", abaixo. |
| `customer` | objeto | não | O **seu** cliente, aquele para quem o documento foi emitido. Veja "Separando o acervo por cliente", abaixo. |
| `evidence.document` | bool | não | Exigir foto da frente e do verso do documento de identidade. |
| `evidence.selfie` | bool | não | Exigir selfie segurando o documento. |
| `evidence.geo` | bool | não | Pedir a geolocalização do signatário no momento da assinatura. |
| `placement` | enum | não | Onde a assinatura entra no PDF. `inline` (padrão) traz as coordenadas aqui; `interactive` cria o documento sem campos e devolve uma tela de posicionamento — veja [posicionamento assistido](/posicionamento-assistido); `signature_page` dispensa campos e coleta as assinaturas numa **Página de Assinaturas** anexada ao fim do documento. |
Os três campos de `evidence` são de três estados: `true` exige, `false` dispensa e
**omitir** deixa valer o que a sua licença já traz configurado. Mande-os apenas
quando este documento precisar de algo diferente do padrão.
### Signatários
| Campo | Descrição |
|---|---|
| `key` | Identificador do signatário dentro deste documento. É o que liga o signatário aos campos. |
| `name` | Nome do signatário, obrigatório. Vai para o Termo de Evidências. |
| `email` | **Opcional.** Quando informado, é para lá que o convite vai. Sem ele, o SignXP não notifica essa pessoa — nem convite, nem conclusão, nem recusa — e a entrega do link fica por sua conta. |
| `tax_id` | CPF ou CNPJ, opcional. Com ou sem máscara. É o que permite à [validação por IA](/validacao-por-ia) conferir se o documento de identidade fotografado pertence a quem você convidou. Enviar um número inválido recusa o envio com **422**; se você não o tem, omita o campo — o próprio signatário informa na hora de assinar. |
| `role` | `SIGNER` (padrão), `APPROVER`, `VIEWER`, `CC` ou `ASSISTANT`. Apenas `SIGNER` assina. |
| `signing_order` | Ordem da assinatura. Signatários com o mesmo número assinam em paralelo; o número seguinte só é convidado quando o anterior conclui. Sem `signing_order`, todos são convidados de uma vez. |
## Separando o acervo por cliente
Se o seu software emite documentos **para vários clientes seus**, mande quem é o
cliente de cada envio:
```json
{
"customer": {
"reference": "cli-4471",
"name": "Acme Indústria Ltda."
}
}
```
| Campo | Descrição |
|---|---|
| `customer.reference` | O identificador do cliente **no seu sistema**. É por ele que o acervo é agrupado e filtrado. |
| `customer.name` | Nome do cliente na data do envio. Viaja junto para o histórico não mudar quando o cadastro mudar. |
O objeto inteiro é opcional — a maioria das integrações não separa carteira. A
`reference` não é conferida contra cadastro nenhum: quem manda o identificador é
você, e basta usar sempre o mesmo valor para o mesmo cliente.
No portal, os documentos passam a ser filtráveis por cliente, e o licenciado pode
manter uma carteira com esses mesmos identificadores.
## Entregando o link você mesmo
Por padrão, quem convida o signatário somos nós: assim que o documento fica
pronto, sai um e-mail com o link de assinatura. Isso atrapalha quando o canal do
seu cliente é outro — WhatsApp, SMS, ou a própria tela do seu sistema. A pessoa
recebe um e-mail que não vai abrir e o link "de verdade" chega por outro caminho.
Enviando `delivery: "none"`, **nenhum e-mail sai** e a entrega passa a ser sua:
```json
{
"title": "Contrato de Prestação de Serviços",
"delivery": "none",
"pdf_base64": "JVBERi0xLjcK...",
"signers": [
{ "key": "cliente", "name": "Marina Alves", "email": "marina@empresa.com.br" }
],
"fields": [
{ "signer_key": "cliente", "type": "SIGNATURE", "page": 1, "x": 15, "y": 78 }
]
}
```
O link de cada signatário sai na [consulta de status](/consultar-status), no
campo `signing_url`. Ele **não** vem na resposta do envio: o PDF ainda está sendo
processado e os links só existem depois disso. Espere o documento sair de `draft`
e pegue os links:
```http
GET /api/v1/documents/128
```
```json
{
"id": 128,
"status": "pending",
"delivery": "none",
"signers": [
{
"key": "cliente",
"email": "marina@empresa.com.br",
"status": "pending",
"signing_url": "https://app.signxp.com.br/assinar/9f2c7a1b..."
}
]
}
```
Alguns pontos que valem saber:
- **O `email` do signatário é opcional** — veja a seção seguinte. Quando ele
existe e o `delivery` é `none`, o endereço fica registrado no Termo de
Evidências e recebe o aviso de conclusão, mas não o convite.
- **A ordem de assinatura continua valendo.** Com `signing_order`, o link do
segundo signatário só passa a existir quando o primeiro conclui; consulte o
status de novo para pegá-lo.
- O modo é gravado no documento e aparece como `delivery` na consulta de status.
- `delivery` não tem relação com o ambiente: uma chave `sk_test_` nunca envia
e-mail, com ou sem ele.
> **O link de assinatura é a credencial.** Quem o tem assina no lugar da pessoa.
> Entregue-o pelo mesmo canal em que você já identifica o seu cliente, e nunca em
> um lugar público.
Se em vez de entregar o link você quer que a pessoa assine **dentro da sua
tela**, veja [assinatura embutida](/assinatura-embutida).
## Signatário sem e-mail
Nem todo cliente final tem e-mail. Como quem entrega o link é você, o campo é
**opcional** — basta omiti-lo:
```json
{
"signers": [
{ "key": "cliente", "name": "Marina Alves" },
{ "key": "contratante", "name": "Acme Ltda.", "email": "contratos@acme.com.br" }
]
}
```
Um documento pode misturar os dois casos, como no exemplo: o contratante recebe o
convite por e-mail normalmente, e o cliente recebe o link pelo seu canal.
O que muda para quem está sem e-mail:
- **Nenhum aviso nosso chega até ele** — nem convite, nem conclusão, nem recusa.
- O `signing_url` dele sai na [consulta de status](/consultar-status), como o dos
demais. É o único caminho até essa pessoa, então guarde-o ou entregue-o na hora.
- No **Termo de Evidências**, a linha de identificação vem sem o endereço. Mande
o `tax_id` sempre que tiver: com o e-mail ausente, ele passa a ser o
identificador forte da pessoa — e é o que permite à
[validação por IA](/validacao-por-ia) conferir o documento fotografado.
> Opcional não quer dizer "aceita qualquer coisa": um endereço malformado
> continua devolvendo `422`. Falhar no envio é melhor do que descobrir semanas
> depois que o convite nunca saiu.
## Posicionando os campos
Cada item de `fields` diz **onde** a assinatura será carimbada no PDF.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| `signer_key` | string | sim | Precisa corresponder a uma `key` da lista de signatários. |
| `page` | inteiro | sim | Número da página, começando em **1**. |
| `x` | número | sim | Distância da **borda esquerda**, em % da largura da página (0 a 100). |
| `y` | número | sim | Distância do **topo**, em % da altura da página (0 a 100). |
| `width` | número | não | Largura do campo, em % da largura da página. Padrão: 20. |
| `height` | número | não | Altura do campo, em % da altura da página. Padrão: 8. |
| `type` | enum | não | `SIGNATURE` (padrão) ou `INITIALS` para rúbrica. |
| `label` | string | não | Rótulo livre exibido no painel do SignXP. Não aparece no documento. |
### O sistema de coordenadas
A origem é o **canto superior esquerdo** da página, e os valores são **percentuais** — não milímetros nem pontos. Assim o mesmo campo funciona em A4, Carta ou paisagem, sem conversão.
```
(0,0) (100,0)
+---------------------------------------+
| |
| x --------> |
| +---------------+ ^ |
| y | assinatura | | height |
| | +---------------+ v |
| v <---- width ----> |
| |
+---------------------------------------+
(0,100) (100,100)
```
Um campo com `x: 12.5, y: 78, width: 20, height: 8` começa a 12,5% da esquerda e a 78% do topo, ocupando 20% da largura e 8% da altura da página.
> **Use o [Posicionador de campos](/posicionador).** Você abre o PDF, arrasta o retângulo onde a assinatura deve ficar e copia o bloco `fields` pronto. O arquivo é lido pelo próprio navegador e não é enviado a lugar nenhum.
> **Quem escolhe o lugar é o usuário do seu sistema, e não o seu código?** Então não envie `fields`: use o [posicionamento assistido](/posicionamento-assistido) e o SignXP devolve uma tela pronta para ele clicar onde cada assinatura vai.
### Regras de validação
- `x + width` e `y + height` não podem passar de 100. Um campo que vaza da página é recusado com `422` no ato do envio.
- A `page` precisa existir no PDF. Como o total de páginas só é conhecido ao abrir o arquivo, essa conferência acontece no processamento: se a página não existir, o documento fica `failed` e **ninguém é convidado**, com o motivo no campo `error` da consulta de status.
- O PDF precisa ser legível pelo nosso motor de selagem. Arquivos corrompidos ou protegidos por senha também resultam em `failed` — é melhor falhar no envio do que entregar ao signatário um contrato que não poderá ser selado no final.
## Assinando sem posicionar campos
Nem todo documento tem lugar marcado para assinar — e nem todo integrador quer
calcular coordenadas. Enviando `placement: "signature_page"`, o campo `fields`
deixa de ser exigido (e passa a ser recusado, para você não achar que mandou
coordenadas que serão ignoradas):
```json
{
"title": "Contrato de Prestação de Serviços",
"placement": "signature_page",
"pdf_base64": "JVBERi0xLjQK...",
"signers": [
{ "key": "cliente", "name": "Marina Alves", "email": "marina@empresa.com.br" }
]
}
```
As páginas do seu PDF chegam **intactas** ao signatário. Na cópia certificada,
o SignXP anexa ao fim do contrato uma **Página de Assinaturas** com a assinatura
desenhada de cada pessoa, nome, e-mail, data, hora e IP — e só depois dela vem o
Termo de Evidências.
O que **não** muda: o signatário continua desenhando a assinatura e passando
pelas mesmas evidências que a sua licença exige. O documento também não passa
por posicionamento — ele já nasce pronto e os convites saem assim que o preparo
termina, sem `placement_url` na resposta.
## Resposta
```json
{
"id": 128,
"status": "draft",
"status_url": "https://api.signxp.com.br/api/v1/documents/128"
}
```
Guarde o `id`: ele identifica o documento nas consultas e nos callbacks.
## Idempotência
Um `POST` que sofre timeout pode ter sido processado. Para não criar o documento duas vezes, envie um cabeçalho `Idempotency-Key` com um valor único por operação:
```http
Idempotency-Key: contrato-2026-0001-envio-1
```
- Mesma chave + mesmo conteúdo → devolvemos **200** com o documento original, sem criar outro.
- Mesma chave + conteúdo diferente → **409**.
- Sem `Idempotency-Key`, um `external_id` repetido também devolve **409**, com o `id` do documento já existente.
---
# Posicionamento assistido
Nem todo mundo que envia um contrato sabe — ou precisa saber — o que é coordenada percentual. Quando quem escolhe o lugar da assinatura é o **usuário do seu sistema**, e não o seu código, use o posicionamento assistido: o SignXP devolve uma tela pronta onde ele abre o documento, clica onde cada assinatura vai e confirma.
Enquanto ninguém confirma, **nenhum signatário é convidado**.
## Como funciona
```
1. seu sistema ──POST /documents (placement: interactive)──▶ SignXP
│
placement_url ◀───────┘ status: awaiting_placement
2. usuário do seu sistema abre a placement_url, clica e confirma
3. SignXP ──convite──▶ signatário (por e-mail, se ele tiver um)
──callback document.placed──▶ seu sistema
```
## 1. Crie o documento sem os campos
```json
{
"external_id": "contrato-2026-0001",
"title": "Contrato de Prestação de Serviços",
"callback_url": "https://meu-sistema.com.br/webhooks/signxp",
"placement": "interactive",
"pdf_base64": "JVBERi0xLjcK...",
"signers": [
{ "key": "contratante", "name": "Maria Souza", "email": "maria@empresa.com.br" }
]
}
```
`placement: "interactive"` troca o `fields` por uma tela. As duas coisas são mutuamente exclusivas: enviar `fields` junto devolve **422** — recusar é melhor do que aceitar e ignorar, que faria você achar que posicionou.
### Resposta
```json
{
"id": 128,
"status": "draft",
"status_url": "https://api.signxp.com.br/api/v1/documents/128",
"placement_url": "https://app.signxp.com.br/posicionar/9f2c7a1b..."
}
```
A `placement_url` é o que você entrega ao seu usuário. Ela também aparece na consulta de status enquanto o documento estiver em `awaiting_placement`, então não precisa ser guardada.
## 2. Leve o usuário até a tela
### Opção A — abrir a URL
O caminho mais simples: abra a `placement_url` em uma nova aba (ou redirecione). Não exige nenhuma linha de JavaScript.
```html
Posicionar assinaturas
```
### Opção B — widget dentro do seu sistema
O usuário não sai da sua tela. Carregue o widget e abra o posicionador em uma janela modal:
```html
```
Para embutir sem modal, dentro de um elemento seu:
```javascript
SignXP.mountPositioner({
url: placementUrl,
container: '#area-do-posicionador',
onPlaced: function () { /* ... */ }
});
```
| Método | Parâmetros |
|---|---|
| `SignXP.openPositioner` | `url` (obrigatório), `onPlaced`, `onCancel`. Devolve `{ close() }`. |
| `SignXP.mountPositioner` | `url` e `container` (obrigatórios), `onPlaced`, `onCancel`. Devolve `{ destroy() }`. |
> O endereço `https://app.signxp.com.br/embed/positioner.js` é fixo e versionado por nós — não copie o arquivo para o seu servidor, ou você deixará de receber correções.
**Libere o seu domínio.** Por padrão a tela pode ser embutida em qualquer site (o segredo é o token da URL). Se quiser restringir ao seu domínio — recomendado —, cadastre as **origens autorizadas** em Configurações, no portal da sua conta; o navegador passa a recusar o iframe em qualquer outro lugar. A mesma lista vale para a [assinatura embutida](/assinatura-embutida).
## 3. Saiba quando terminou
Três formas, use a que couber:
- **Callback** `document.placed`, se você informou `callback_url`. É o caminho recomendado — chega no momento exato.
- **`onPlaced`** do widget, quando estiver usando a opção B.
- **Consulta de status**: o documento sai de `awaiting_placement` e entra em `pending`.
O corpo do callback é o mesmo dos demais eventos, com `event: "document.placed"` e `status: "pending"`.
## O que o usuário vê
A tela lista os signatários do documento, mostra o PDF página a página e pede um clique no lugar da assinatura de cada um. Ele pode arrastar para definir o tamanho ou apenas clicar — nesse caso o campo entra no tamanho padrão. O botão de confirmação só libera quando **todos** os signatários têm ao menos um campo, então é impossível concluir deixando alguém sem lugar para assinar.
Quando a licença inclui rúbrica, aparece também a opção de posicionar rúbricas.
## Limites
| Situação | O que acontece |
|---|---|
| Documento já posicionado | Nova confirmação devolve **409**; a tela passa a exibir o estado concluído. |
| Campo fora da página | **422** com a mensagem para o usuário corrigir; nada é gravado. |
| Página inexistente no PDF | **422**. |
| Ninguém confirma | O documento fica em `awaiting_placement` indefinidamente e não consome nada além da quota já contada no envio. |
> A `placement_url` é pessoal e intransferível como o link de assinatura: quem a tem pode definir onde as assinaturas ficam. Trate-a com o mesmo cuidado.
---
# Assinatura dentro do seu sistema
Por padrão, o signatário recebe um link e assina numa página nossa. Quando o seu
software **já é** a tela em que o cliente final está, mandá-lo para fora é uma
quebra no meio do fluxo — e cada saída é uma chance de a pessoa não voltar.
Há três formas de resolver isso, da mais barata para a mais trabalhosa:
| | Quem faz a tela | O que você escreve | Evidência |
|---|---|---|---|
| **Domínio próprio** | nós | nada | nossa responsabilidade |
| **Assinador embutido** | nós, dentro da sua tela | um `
```
Para embutir sem modal, dentro de um elemento seu:
```javascript
SignXP.mountSigner({
url: signingUrl,
container: '#area-de-assinatura',
onSigned: function () { /* ... */ }
});
```
| Método | Parâmetros |
|---|---|
| `SignXP.openSigner` | `url` (obrigatório), `showDocument`, `onSigned`, `onRejected`, `onCancel`. Devolve `{ close() }`. |
| `SignXP.mountSigner` | `url` e `container` (obrigatórios), `showDocument`, `onSigned`, `onRejected`. Devolve `{ destroy() }`. |
> O endereço `https://app.signxp.com.br/embed/signer.js` é fixo e versionado por
> nós — não copie o arquivo para o seu servidor, ou deixará de receber correções.
> Ele convive com o `positioner.js`: os dois penduram métodos no mesmo objeto
> `SignXP`.
### Quando você já mostra o documento
Por padrão o assinador abre numa etapa de revisão, com o PDF dentro dele. Se a
sua tela **já exibe o documento** — num visualizador ao lado, numa aba própria,
num painel do seu sistema —, essa etapa vira uma segunda cópia do mesmo PDF e
empurra a assinatura para baixo. Desligue-a:
```javascript
SignXP.mountSigner({
url: signingUrl,
container: '#area-de-assinatura',
showDocument: false,
onSigned: function () { /* ... */ }
});
```
O que muda:
- a etapa de revisão sai do fluxo, e o assinador abre direto na etapa seguinte
(consentimento, localização, fotos — o que a sua licença coletar);
- o PDF **não é baixado** pelo iframe, então a tela carrega mais leve;
- na hora de assinar continua havendo um link discreto para abrir o documento.
Ele fica: quem assina precisa poder conferir o que assina, e o seu visualizador
pode estar fora do campo de visão naquele momento.
> Só vale embutido. Se o signatário abrir a `signing_url` direto no navegador, a
> etapa de revisão volta — fora do seu sistema não existe outra tela mostrando o
> documento, e assinar sem ele à vista não é opção.
### De onde sai a `signing_url`
Da [consulta de status](/consultar-status), no campo `signing_url` de cada
signatário.
Se a assinatura vai acontecer na sua tela, o nosso convite por e-mail só
atrapalha — a pessoa recebe um link que leva para fora do seu sistema. Duas
formas de calá-lo, as duas descritas em
[enviar um documento](/enviar-documento):
- **`delivery: "none"`** no envio, que suprime o convite de todos os
signatários daquele documento;
- **omitir o `email`** do signatário, quando ele simplesmente não tem um.
### Autorize a sua origem
Por padrão a página pode ser embutida em qualquer site: o segredo é o token da
URL. Para restringir — recomendado —, cadastre as **origens autorizadas** da sua
licença em **Configurações**, no portal. O navegador passa a recusar o iframe em
qualquer outro lugar.
Uma origem por linha, com esquema:
```
https://sistema.meu-erp.com.br
https://app.meu-erp.com.br
```
Isso **não** exige apontar domínio nenhum para nós: autorizar uma origem e ter um
domínio de white-label são coisas separadas.
### Câmera e geolocalização
Quando a sua licença coleta foto do documento, selfie ou localização, o navegador
precisa que a página de fora **delegue** essas permissões ao iframe. O widget já
faz isso (`allow="camera; geolocation"`). O que você precisa garantir é que a sua
própria página seja servida em **HTTPS** — sem isso o navegador bloqueia a câmera
antes de chegar em nós.
### Quando o cliente está no computador
Se a licença coleta imagens e a pessoa abriu no desktop, o fluxo mostra um QR
Code para ela continuar no celular, e a tela embutida fica aguardando. O
`onSigned` dispara quando ela conclui pelo telefone — você não precisa tratar o
handoff.
## Fluxo próprio, sobre a API de sessão
Se você quer desenhar a experiência inteira, a mesma API que a nossa página
consome está aberta para você. A autenticação é o próprio `signing_token` da
URL — **não** use a sua chave de API aqui: essas chamadas saem do navegador do
signatário.
```http
GET /api/signing/{token} estado inicial da sessão
GET /api/signing/{token}/status o que já foi coletado (usado no handoff)
GET /api/signing/{token}/document redireciona para o PDF
POST /api/signing/{token}/consent registra o aceite do termo
POST /api/signing/{token}/tax-id CPF/CNPJ autodeclarado
POST /api/signing/{token}/geo latitude, longitude, accuracy, denied
POST /api/signing/{token}/photo kind + image (multipart)
POST /api/signing/{token}/validate-image pede o veredito da IA para uma imagem
POST /api/signing/{token}/signature image + type (drawn|typed)
POST /api/signing/{token}/initials image + type (drawn|typed)
POST /api/signing/{token}/progress salva a etapa, para retomar depois
POST /api/signing/{token}/complete conclui a assinatura
POST /api/signing/{token}/reject recusa, com motivo opcional
```
O `GET` inicial devolve tudo o que a tela precisa decidir:
```json
{
"document": { "title": "Contrato", "is_test": false, "verification_url": "...", "download_enabled": true },
"signer": { "name": "Maria Souza", "email": "maria@empresa.com.br", "tax_id": "529.982.247-25", "status": "pending" },
"brand": { "name": "Acme", "color": "#00a3ff", "logo": "..." },
"state": "open",
"collect": { "document": true, "selfie": true, "geo": true, "initials": false, "tax_id": false },
"consent": { "required": true, "version": "1", "text": "Declaro, para os devidos fins..." },
"progress": { "current_step": "geo", "photos": {}, "has_signature": false, "has_initials": false }
}
```
O que merece atenção:
- **`collect` manda.** Ele já combina o que o documento pediu com o que a licença
contratou. Coletar menos do que ele diz enfraquece a prova; coletar mais não
adianta, porque o motor só guarda o que conhece.
- **`state` diferente de `open`** significa que não é a vez desta pessoa, que ela
já assinou, ou que o documento acabou. Pare o fluxo e mostre o motivo.
- **O texto do consentimento é o do `consent.text`**, com a versão que vem junto.
Escrever o seu próprio texto invalida o casamento entre o que a pessoa leu e o
que o Termo de Evidências afirma que ela leu.
- **`progress.current_step`** é o que permite retomar de onde parou. Salvá-lo é
opcional; não salvar faz a pessoa recomeçar a cada recarga.
- **A ordem é sua, o `complete` é o fim.** Só chame quando tiver coletado o que o
`collect` pediu — depois dele o documento segue para selagem.
Para o navegador poder falar com o motor a partir do seu domínio, cadastre a sua
origem em **Configurações → origens autorizadas**, como no widget. Sem isso o
navegador barra as chamadas por CORS.
> **A evidência passa a depender do seu fluxo.** O motor continua validando tudo
> do lado dele e é a única fonte da prova, mas quem garante que a pessoa passou
> por cada etapa é a sua tela. É o preço de desenhar a experiência inteira — e a
> razão de o widget existir.
---
# Validação por IA
Quando a sua licença coleta **foto do documento** ou **selfie**, o SignXP pode
conferir cada imagem com inteligência artificial no momento em que o signatário
a envia — antes de deixá-lo seguir para a assinatura.
A validação responde três perguntas:
| Verificação | O que confere |
|---|---|
| Documento (frente e verso) | Se é mesmo um documento de identificação, se está legível e se o titular confere com o signatário convidado. |
| Selfie | Se há um rosto humano visível e nítido o suficiente para reconhecimento. |
| Conferência facial | Se a pessoa da selfie é a mesma da foto do documento. |
Imagem reprovada não passa: o signatário recebe o motivo em português na própria
tela e tira outra foto. Você não precisa fazer nada — nem tratar nada na sua
integração — para que isso aconteça.
> A validação por IA é um recurso contratado por licença e configurado pela
> equipe SignXP. Se a sua licença não o tem, nada nesta página muda o seu fluxo.
## Conferindo a identidade do signatário
Envie `tax_id` (CPF ou CNPJ) em cada signatário e a IA passa a comparar o número
e o nome com o que está impresso no documento fotografado:
```json
{
"key": "contratante",
"name": "Maria Souza",
"email": "maria@empresa.com.br",
"tax_id": "529.982.247-25"
}
```
O campo é opcional e aceita máscara. Sem ele, a IA continua conferindo a
qualidade e a legibilidade da imagem, mas **não tem como saber** se o documento
é da pessoa certa — nesse caso o próprio signatário informa o número na página
de assinatura, e o Termo de Evidências registra que ele foi autodeclarado.
Um `tax_id` com dígito verificador inválido recusa o envio com **422**. É
proposital: um número errado no cadastro reprovaria justamente o signatário
certo.
## Quando a IA não consegue decidir
Duas situações não têm resposta automática: o serviço de IA fica indisponível,
ou o signatário esgota as tentativas permitidas para a mesma imagem.
Em ambas, o SignXP **não trava o signatário** — ele conclui a assinatura
normalmente. O que acontece é do lado do documento:
1. O documento assume o status **`awaiting_review`**.
2. A selagem é **retida**: não há cópia certificada nem `document.completed`.
3. O licenciado confere as imagens e **aprova** ou **recusa** — no portal ou pela
API, sem sair do seu sistema (veja "Decidindo pela API", abaixo).
| Decisão | O que você recebe |
|---|---|
| Aprovada | O documento segue para a selagem e você recebe `document.completed`, como em qualquer conclusão. |
| Recusada | O documento é encerrado e você recebe `document.rejected`, com o motivo escrito pelo licenciado. |
Ou seja: um documento em `awaiting_review` **ainda pode virar qualquer um dos
dois desfechos**. Trate-o como pendente, nunca como concluído.
## Decidindo pela API, sem sair do seu sistema
Se o seu software **já é** a tela que o cliente final usa, mandá-lo ao nosso
portal para destravar um contrato é uma quebra no meio do fluxo. A conferência
inteira cabe na API — achar, abrir, ver as fotos e decidir:
```http
GET /api/v1/reviews
GET /api/v1/documents/{id}/signers/{signer_id}/review
GET /api/v1/documents/{id}/signers/{signer_id}/review/images/{kind}
POST /api/v1/documents/{id}/signers/{signer_id}/review/approve
POST /api/v1/documents/{id}/signers/{signer_id}/review/reject
```
A fila traz o que está esperando na sua licença, mais antigo primeiro — é a ordem
em que os contratos ficaram parados:
```json
{
"data": [
{
"document": { "id": 128, "external_id": "contrato-2026-0001", "title": "Contrato", "status": "awaiting_review" },
"signer": { "id": 91, "key": "contratante", "name": "Maria Souza", "email": "maria@empresa.com.br", "tax_id": "529.982.247-25", "tax_id_self_declared": false, "signed_at": "2026-08-03T14:02:11-03:00" },
"reasons": ["O serviço de validação não respondeu."],
"ai_checks": { "document": { "status": "unverified" } },
"images": {
"document_front": "https://arquivos.signxp.com.br/...",
"document_back": null,
"selfie": "https://arquivos.signxp.com.br/..."
},
"images_expire_at": "2026-08-03T14:20:00-03:00",
"images_purged_at": null,
"waiting_since": "2026-08-03T14:05:00-03:00"
}
],
"meta": { "current_page": 1, "last_page": 1, "per_page": 25, "total": 1 }
}
```
### As imagens
Cada chave de `images` traz a **URL da foto** ou `null` quando ela não existe.
São links assinados e temporários, servidos direto do nosso armazenamento: a
imagem não passa pelo corpo da resposta, não vai para o seu log e o endereço
morre em **15 minutos** (`images_expire_at`).
Quando o link expirar antes de a pessoa decidir, peça outro — um de cada vez:
```http
GET /api/v1/documents/128/signers/91/review/images/selfie
```
```json
{ "url": "https://arquivos.signxp.com.br/...", "expires_at": "2026-08-03T14:35:00-03:00" }
```
Os valores de `{kind}` são `document_front`, `document_back` e `selfie`. Uma
imagem que essa evidência não tem responde **404**.
> **`images_purged_at` preenchido significa que não há mais o que olhar.** As
> imagens de evidência têm prazo de retenção; passado ele, são descartadas e o
> pedido de imagem responde **410**. A pendência continua decidível — só que
> sobre o resto da evidência, não sobre a foto.
A conferência e as imagens só respondem por assinaturas que **passaram pela
validação manual**. Um signatário que nunca entrou na fila responde 404: esta é
uma porta para destravar o que a IA não resolveu, não um jeito de baixar as
imagens de qualquer assinatura da licença.
### Abrindo uma pendência específica
Recebeu `document.awaiting_review` no callback? Vá direto nela, sem paginar a
fila atrás do documento:
```http
GET /api/v1/documents/128/signers/91/review
```
A resposta é a mesma linha da fila, mais o veredito — e ela **continua
respondendo depois de decidida**, com quem decidiu, quando e o que escreveu:
```json
{
"review_status": "approved",
"reviewed_at": "2026-08-03T14:31:07-03:00",
"reviewed_by": "ana@meu-erp.com.br",
"note": "Documento confere com o cadastro."
}
```
### Decidindo
A decisão leva **quem decidiu**:
```json
{ "reviewer": "ana@meu-erp.com.br", "note": "Documento confere com o cadastro." }
```
| Campo | Aprovar | Recusar |
|---|---|---|
| `reviewer` | obrigatório | obrigatório |
| `note` | opcional | **obrigatório** — vira o motivo comunicado às partes |
A resposta diz no que o documento parou:
```json
{ "review_status": "approved", "document_status": "completed" }
```
Aprovar solta a selagem retida e o `document.completed` sai como em qualquer
conclusão. Recusar encerra o documento, avisa todas as partes e dispara
`document.rejected`.
> **`reviewer` vai para o Termo de Evidências.** É o nome que constará como
> responsável pela conferência, ao lado da data e da observação. Mande quem de
> fato decidiu no seu sistema — uma aprovação sem responsável não é evidência de
> nada. Nós não temos como conferir esse nome: a responsabilidade por ele é sua.
Decidir duas vezes sobre a mesma assinatura devolve `422`: só o que está
realmente pendente pode ser decidido.
## O que a sua integração precisa fazer
Se você já trata `completed` e `rejected`, o essencial continua funcionando. Duas
recomendações:
- **Aceite o status `awaiting_review`** na consulta de status sem tratá-lo como
erro. Ele significa "assinado, aguardando conferência humana".
- **Escute o evento `document.awaiting_review`** se você acompanha o documento
por [callback](/callbacks). Sem ele, um documento retido para conferência
parece simplesmente parado — e a espera pode durar horas ou dias, porque
depende de uma pessoa.
```json
{
"event": "document.awaiting_review",
"id": 128,
"external_id": "contrato-2026-0001",
"status": "awaiting_review",
"completed_at": null,
"signers": [
{ "key": "contratante", "email": "maria@empresa.com.br", "status": "signed", "signed_at": "2026-07-28T13:58:02-03:00" }
]
}
```
## O que fica registrado
O **Termo de Evidências** anexado ao PDF final traz o resultado de cada
verificação — aprovada, reprovada ou **não verificada** —, o modelo usado e o
número de tentativas. Quando houve conferência humana, ele registra também quem
decidiu, quando e a observação escrita.
Uma imagem liberada por indisponibilidade do serviço nunca aparece como
"aprovada": o Termo não afirma o que ninguém conferiu.
---
# Consultar o status
```http
GET /api/v1/documents/{id}
```
```json
{
"id": 128,
"external_id": "contrato-2026-0001",
"title": "Contrato de Prestação de Serviços",
"environment": "live",
"status": "completed",
"delivery": "email",
"error": null,
"customer": { "reference": "cli-4471", "name": "Acme Indústria Ltda." },
"created_at": "2026-07-28T13:40:00-03:00",
"completed_at": "2026-07-28T14:03:11-03:00",
"verification_url": "https://app.signxp.com.br/validar/9f2c...",
"signers": [
{
"key": "contratante",
"name": "Maria Souza",
"email": "maria@empresa.com.br",
"status": "signed",
"signed_at": "2026-07-28T13:58:02-03:00",
"signing_url": "https://app.signxp.com.br/assinar/9f2c7a1b..."
}
]
}
```
O `email` do signatário vem `null` quando o documento foi enviado sem ele — nesse
caso o SignXP não notifica essa pessoa, e a entrega do link é sua. Veja
[signatário sem e-mail](/enviar-documento).
## O link de assinatura
`signing_url` vem em **todos os ambientes**, inclusive produção. É por ele que
você entrega o convite pelo seu próprio canal — veja
[entregando o link você mesmo](/enviar-documento).
O link é a credencial de quem assina: quem o tem assina no lugar da pessoa.
Trate-o como trata a sua chave de API — nunca em página pública, nunca em log
compartilhado.
## Status do documento
| Status | Significado |
|---|---|
| `draft` | Recebido; o PDF ainda está sendo processado. |
| `awaiting_placement` | Esperando alguém posicionar os campos. `placement_url` vem junto na resposta. Ninguém foi convidado ainda. |
| `pending` | Pronto e aguardando assinatura. |
| `awaiting_review` | Todos assinaram, mas uma imagem de evidência ficou sem veredito da IA e o licenciado precisa conferir. **O documento ainda não vale**: não há cópia selada, e ele pode terminar como `completed` ou `rejected`. Veja [validação por IA](/validacao-por-ia). |
| `completed` | Todos assinaram; o PDF selado está disponível. |
| `rejected` | Um signatário recusou. |
| `cancelled` | A tentativa foi encerrada — por você, pela API, pelo licenciado no portal dele, ou pela equipe SignXP a pedido. Os links pendentes deixaram de valer. Veja [cancelar um documento](/cancelar-documento). |
| `failed` | Falha no processamento — veja `error` (página inexistente, PDF ilegível, selagem que não fechou). |
## Status do signatário
| Status | Significado |
|---|---|
| `pending` | Ainda não assinou. |
| `opened` | Abriu o link. |
| `signed` | Assinou. |
| `rejected` | Recusou. |
| `cancelled` | O documento foi [cancelado](/cancelar-documento) antes desta pessoa assinar. Diferente de `rejected`: a decisão foi de quem enviou, não de quem assinaria. |
## Listando o acervo
```http
GET /api/v1/documents
```
Devolve os documentos da sua licença, do mais recente para o mais antigo, com o
**mesmo formato** de item da consulta acima — quem lista e depois abre um
documento não precisa aprender dois vocabulários para a mesma coisa.
É com isto que você monta o acervo dentro do seu sistema, em vez de mandar o seu
usuário ao portal.
```json
{
"data": [ { "id": 128, "status": "completed", "...": "..." } ],
"meta": { "current_page": 1, "last_page": 4, "per_page": 25, "total": 87 }
}
```
### Filtros
| Parâmetro | O que faz |
|---|---|
| `status` | Um dos status da tabela acima. |
| `phase` | `active` (em andamento) ou `archived` (encerrado **ou** arquivado à mão pelo licenciado). Ver abaixo. |
| `customer_reference` | Só os de um cliente seu. Use `none` para os que foram enviados sem `customer`. |
| `from` / `to` | Intervalo de datas de criação (`AAAA-MM-DD`), inclusivo nas duas pontas. |
| `search` | Busca no título, no `external_id` e no nome do cliente. |
| `per_page` | Itens por página. Padrão 25, teto 100. |
| `page` | A página desejada. |
Os filtros combinam entre si:
```http
GET /api/v1/documents?status=pending&customer_reference=cli-4471&per_page=50
```
> A ordenação é estável (por id, decrescente), então paginar não faz um documento
> aparecer duas vezes nem sumir entre uma página e a seguinte.
### O que `phase` separa
`phase` divide o trabalho do dia a dia do histórico, e olha **dois eixos** ao
mesmo tempo:
- **O status.** O que encerrou — `completed`, `rejected`, `cancelled`, `failed` —
sai do `active` sozinho, sem ninguém precisar fazer nada.
- **O arquivamento manual**, feito pelo licenciado no portal. Ele existe para
tirar da frente um documento que ainda está aberto e que ninguém vai tocar.
Então `phase=active` é *em curso **e** não arquivado*, e `phase=archived` é
*encerrado **ou** arquivado*. Um `pending` esquecido pode ser arquivado; um
`completed` vai para o histórico por conta própria.
O `awaiting_review` conta como **em curso**: a assinatura já aconteceu, mas o
documento só vale depois de uma decisão humana — e é o estado que mais depende de
alguém agir, então escondê-lo no histórico o tiraria da vista de quem precisa
resolvê-lo.
Esta consulta **não consome quota** e continua respondendo com a licença
bloqueada — cortar o acesso ao próprio acervo por causa de uma fatura em atraso
seria punir o lado errado.
## Polling ou callback?
Prefira o [callback](/callbacks): ele avisa no momento exato da mudança. Todo desfecho tem evento — inclusive `failed` e `cancelled`. Use a consulta de status como rede de segurança — por exemplo, uma varredura a cada 30 minutos nos documentos que ainda estão `pending`.
Se precisar fazer polling frequente, respeite o limite de requisições da sua licença (veja **Erros e limites**).
---
# Cancelar um documento
```http
POST /api/v1/documents/{id}/cancel
```
```json
{
"reason": "Contrato substituído pela revisão 2."
}
```
O `reason` é opcional (texto livre, até 500 caracteres), mas escreva-o. Meses
depois, um `cancelled` sem motivo é indistinguível de um cancelamento feito por
engano — e como o documento continua consultável, o motivo passa a fazer parte
do registro.
## O que o cancelamento faz
**Derruba os links de assinatura que ainda estão pendentes.** É o efeito
principal. Quem recebeu o convite antes do cancelamento e abrir o link agora vê
"documento cancelado" em vez do fluxo de assinatura, e nenhuma assinatura é mais
aceita naquele documento.
Isso resolve o caso real: o contrato foi substituído, ou foi assinado por outro
caminho, e o link que já saiu por e-mail continuaria valendo.
**Muda o status para `cancelled` e dispara o [callback](/callbacks)
`document.cancelled`.**
**Encerra as assinaturas que ainda não aconteceram.** Quem estava `pending` ou
`opened` passa a `cancelled`. Sem isso, a assinatura ficaria "pendente" para
sempre — e a [página de verificação](/verificacao) seguiria prometendo uma
assinatura que nunca viria.
Quem **já assinou ou já recusou não é tocado**. Aquilo aconteceu, está no Termo
de Evidências, e reescrever apagaria o que a pessoa fez.
**Não apaga o documento.** Ele continua no [acervo](/consultar-status), continua
aparecendo na listagem e a [URL de verificação](/verificacao) continua
respondendo. Cancelar é registrar que a tentativa terminou, não sumir com ela —
inclusive porque quem já assinou antes do cancelamento tem o direito de
encontrar o que assinou.
## A resposta
Devolve o documento no **mesmo formato** da consulta de status, acrescido do
bloco de quota:
```json
{
"id": 128,
"status": "cancelled",
"cancelled_at": "2026-08-04T16:22:41-03:00",
"cancellation_reason": "Contrato substituído pela revisão 2.",
"verification_url": "https://app.signxp.com.br/validar/9f2c...",
"quota": {
"refunded": false,
"limit": 500,
"used": 139,
"period": "2026-08"
}
}
```
`cancelled_at` e `cancellation_reason` também vêm na consulta de status e na
listagem do acervo — você não precisa guardar a resposta desta chamada para
saber depois por que o documento foi cancelado.
## O cancelamento não devolve a quota
`quota.refunded` é sempre `false`. O consumo é contado no momento do **envio**,
no período do envio, e não volta.
São duas razões, e as duas importam:
- Devolver creditaria o mês errado. Um documento enviado em julho e cancelado em
agosto abateria a quota de agosto, que não foi onde ele foi gasto.
- Quota que volta é quota que se contorna. Enviar e cancelar em sequência
permitiria disparar convites sem limite — e o convite é e-mail que já saiu, com
o link e o seu nome dentro.
Se isso apertar o seu volume, o caminho é rever a quota do plano com o time
SignXP, não cancelar para reciclar.
> Envios no ambiente de **teste** (`sk_test_`) nunca consumiram quota, então aqui
> não há o que devolver.
## Quando o cancelamento é recusado
| Status do documento | Resposta |
|---|---|
| `draft`, `awaiting_placement`, `pending` | **200** — cancelado. |
| `cancelled` | **200** — idempotente (veja abaixo). |
| `completed` | **409** — assinatura concluída não se cancela. |
| `awaiting_review` | **409** — use a reprovação da [validação por IA](/validacao-por-ia). |
| `rejected`, `failed` | **409** — a tentativa já terminou por outro motivo. |
Um documento `completed` já foi selado, e a via com validade jurídica pode já
estar na mão das partes. Mudar o status aqui não desassinaria nada — só faria o
seu acervo mentir. Se o contrato assinado precisa ser desfeito, isso é um
distrato, e um distrato é um documento novo.
Em `awaiting_review` todos já assinaram e o documento espera a conferência de uma
pessoa. A saída dali é reprovar a validação, que encerra o documento com o motivo
ligado à assinatura reprovada — cancelar por fora apagaria essa ligação.
Em `rejected` e `failed` a tentativa já acabou, e a causa está registrada.
Sobrescrever com "cancelado" perderia a informação de que houve uma recusa ou uma
falha.
## Repetir é seguro
Cancelar um documento que já está `cancelled` devolve **200** com o mesmo corpo —
inclusive o `cancelled_at` e o `reason` do cancelamento **original**, que não são
reescritos.
A repetição também **não dispara o callback de novo**: quem recebe
`document.cancelled` vê o evento uma vez por documento. Isso torna a chamada
segura para reenviar quando você perdeu a resposta e não sabe se ela chegou.
## Cancelar não é arquivar
Cancelar encerra a tentativa; **arquivar** apenas tira o documento das telas do
dia a dia. São coisas diferentes:
- Um documento `completed` pode ser arquivado, mas nunca cancelado.
- Um documento ainda aberto pode ser arquivado sem ser cancelado: o link segue
valendo e a assinatura ainda pode acontecer — ele só saiu da lista de trabalho.
O arquivamento é uma ação do portal, feita pelo licenciado. Na
[listagem do acervo](/consultar-status), `status=cancelled` filtra pelo
cancelamento, e `phase` separa o dia a dia do histórico olhando o status **e** o
arquivamento juntos. Um documento cancelado já sai do `active` por causa do
status, sem precisar ser arquivado.
## Erros
Vale a tabela de [erros e limites](/erros-e-limites). Específico daqui:
| Código | Quando acontece |
|---|---|
| `404` | O `id` não existe **ou** é de outro licenciado. Nunca revelamos qual dos dois. |
| `409` | O documento está num estado que não aceita cancelamento (tabela acima). O motivo vem em `message`. |
| `422` | `reason` acima de 500 caracteres. |
Esta chamada **continua funcionando com a licença fora de vigência ou com a quota
esgotada** — o que fica bloqueado nessa situação é o envio, não o cancelamento. É
deliberado: quem está assim é justamente quem mais precisa conseguir derrubar um
link pendente, e recusar aqui manteria vivos os links que você está tentando
matar.
A exceção é o **cliente inativo**, que recebe `403` em toda a API, aqui como em
qualquer outra rota.
---
# Callbacks
Quando um documento muda de estado, o SignXP faz um `POST` para o `callback_url` informado no envio.
## Corpo
```json
{
"event": "document.completed",
"id": 128,
"reference": "acme",
"external_id": "contrato-2026-0001",
"environment": "live",
"status": "completed",
"completed_at": "2026-07-28T14:03:11-03:00",
"verification_url": "https://app.signxp.com.br/validar/9f2c...",
"signers": [
{ "key": "contratante", "email": "maria@empresa.com.br", "status": "signed", "signed_at": "2026-07-28T13:58:02-03:00" }
]
}
```
Eventos possíveis:
| Evento | Quando acontece |
|---|---|
| `document.completed` | Todos assinaram e o PDF selado está disponível. |
| `document.rejected` | Um signatário recusou — ou o licenciado recusou na conferência manual. |
| `document.failed` | Falha no processamento; veja `error`. |
| `document.cancelled` | A tentativa foi encerrada e os links pendentes deixaram de valer — por você, pela [API](/cancelar-documento), pelo licenciado no portal dele, ou pela equipe SignXP a pedido. Sai uma vez por documento: repetir o cancelamento não reemite o evento. |
| `document.placed` | Campos posicionados; os signatários foram liberados para assinar (e o convite saiu, para quem recebe por e-mail). |
| `document.awaiting_review` | Todos assinaram, mas a [validação por IA](/validacao-por-ia) não concluiu e o licenciado precisa conferir as imagens. A conclusão fica retida até a decisão dele. |
O `document.awaiting_review` **não é um desfecho**: depois dele ainda vem um
`document.completed` ou um `document.rejected`. Trate-o como aviso de espera.
O campo `reference` identifica a conta dona do documento. Se você integra uma
conta só, ele é sempre o mesmo e pode ser ignorado. Ele existe para quem recebe
notificações de **várias contas no mesmo endpoint** — o caso de quem embute o
SignXP num produto multiempresa.
## Um `completed` pode ser desmentido
A cópia certificada é montada **depois** que o `document.completed` sai — selar
leva tempo e ninguém fica esperando por isso. Se a selagem não fechar, o
documento volta para `failed` e você recebe um **`document.failed` com o mesmo
`id`**. Quando uma retentativa consegue selar, vem um novo `document.completed`.
É raro, mas não é hipotético. Duas consequências para o seu código:
- **Não trate `completed` como imutável.** Um `document.failed` que chega depois
de um `completed` para o mesmo documento é a correção, não um evento fora de
ordem: o estado atual é sempre o do último evento.
- **Baixe a via final quando ela importar**, não no instante do `completed`. Se o
download responder 404 logo depois do aviso, a selagem ainda está em curso —
tente de novo em alguns minutos ou espere o desfecho se estabilizar.
## Cabeçalhos
| Cabeçalho | Conteúdo |
|---|---|
| `X-SignXP-Event` | Nome do evento. |
| `X-SignXP-Delivery` | UUID da entrega. O mesmo valor se repete nas retentativas. |
| `X-SignXP-Reference` | Conta dona do documento — o mesmo valor do campo `reference` do corpo. Repetido aqui para você rotear a entrega sem precisar desserializar o JSON antes. |
| `X-SignXP-Timestamp` | Unix timestamp da assinatura. |
| `X-SignXP-Signature` | `t=,v1=` |
## Validando a assinatura
A assinatura é um HMAC-SHA256 de `"."`, usando o **segredo de callback** da sua licença como chave. Sempre valide antes de confiar no conteúdo — e sempre com o corpo **bruto**, antes de qualquer parse.
```php
// PHP
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_SIGNXP_SIGNATURE'] ?? '';
$secret = getenv('SIGNXP_CALLBACK_SECRET');
parse_str(str_replace(',', '&', $header), $parts); // t=..., v1=...
$expected = hash_hmac('sha256', $parts['t'].'.'.$payload, $secret);
if (! hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(401);
exit;
}
// Rejeite entregas antigas (proteção contra replay).
if (abs(time() - (int) $parts['t']) > 300) {
http_response_code(401);
exit;
}
```
```javascript
// Node.js (Express com body bruto)
const crypto = require('crypto');
app.post('/webhooks/signxp', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-SignXP-Signature') || '';
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', process.env.SIGNXP_CALLBACK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ''))) {
return res.sendStatus(401);
}
// ... processa e responde rápido
res.sendStatus(200);
});
```
## O que esperamos do seu endpoint
- Responder **2xx** em até 15 segundos. Faça o processamento pesado em fila.
- Ser **idempotente**: a mesma entrega pode chegar mais de uma vez. Use `X-SignXP-Delivery` ou o par (`id`, `status`) para deduplicar.
Falhas são registradas e reenviadas automaticamente (até 3 tentativas). Você acompanha todas as entregas em **[Registros](https://app.signxp.com.br/registros)**, no portal da sua conta, e reenvia você mesmo a entrega que ficou pelo caminho — sem depender do suporte.
## Trocando o segredo
O segredo de callback pode ser regenerado a pedido. O valor antigo para de validar imediatamente, então combine a troca com a sua equipe antes.
---
# Erros e limites
## Códigos de resposta
| Código | Quando acontece | O que fazer |
|---|---|---|
| `202` | Documento aceito e em processamento. | Guarde o `id`. |
| `200` | Repetição de um envio idempotente. | Nada — é o mesmo documento. |
| `401` | Chave ausente, inválida, revogada ou expirada. | Verifique a chave; emita outra se necessário. |
| `402` | Licença fora de vigência ou quota esgotada. | Fale com o time SignXP. Não repita a chamada em loop. |
| `403` | Cliente inativo. | Fale com o time SignXP. |
| `409` | `Idempotency-Key` reutilizada com conteúdo diferente, `external_id` já usado, ou [cancelamento](/cancelar-documento) de um documento que já foi encerrado. | Confira o estado atual do documento antes de repetir. |
| `422` | Payload inválido. | Corrija conforme o corpo do erro. |
| `429` | Limite de requisições por minuto excedido. | Respeite o `Retry-After` e reduza a frequência. |
Erros de validação seguem o formato padrão do Laravel:
```json
{
"message": "O campo title é obrigatório.",
"errors": {
"title": ["O campo title é obrigatório."]
}
}
```
## Quota mensal
A quota é contada em **documentos enviados e aceitos**, por mês. O período reinicia no dia 1º.
Nas respostas de envio bem-sucedidas devolvemos:
```http
X-Quota-Limit: 500
X-Quota-Remaining: 361
```
Quando a quota se esgota, novos envios recebem `402`:
```json
{
"message": "Quota mensal de 500 documentos esgotada.",
"quota": { "limit": 500, "used": 500, "period": "2026-07" }
}
```
**Assinaturas em andamento não são afetadas**: os links continuam válidos, os documentos são selados normalmente e a consulta de status segue respondendo. Apenas o envio de documentos novos é bloqueado.
Envios com chave `sk_test_` **não** consomem quota.
**[Cancelar](/cancelar-documento) um documento não devolve a quota.** O consumo é contado no envio, no período do envio, e não volta — devolver creditaria o mês errado e faria o ciclo enviar/cancelar disparar convites sem limite. O [cancelamento](/cancelar-documento) continua permitido com a quota esgotada, porém: derrubar um link pendente é o que você mais precisa poder fazer nessa situação.
Acompanhe o consumo em tempo real em **[Consumo](https://app.signxp.com.br/consumo)**, no portal da sua conta.
## Limite de requisições
Cada licença tem um teto de requisições por minuto (padrão: 60). Ao ultrapassá-lo, a API responde `429` com o cabeçalho `Retry-After`.
Boas práticas:
- prefira callbacks a polling;
- se precisar de polling, espace as consultas e use backoff exponencial nos erros;
- não paralelize envios em massa sem controle de concorrência.
## Recursos da licença
Alguns recursos são contratados à parte. Se a sua licença não os inclui, o envio que tentar usá-los recebe `422`:
| Recurso | O que fica indisponível |
|---|---|
| Coleta de evidências | `evidence.document` e `evidence.selfie`. |
| Rúbrica e ordem de assinatura | Campos `INITIALS` e `signing_order`. |
| Validação por IA | Conferência automática de documento e selfie durante a assinatura, e a conferência de `tax_id` contra o documento fotografado. Veja [validação por IA](/validacao-por-ia). |
| Verificação pública | Página `/validar`, o [validador público](/verificacao), os endpoints `/api/verify/...` e o download da cópia certificada. |
A seção **Chaves de API** mostra quais recursos estão liberados na sua licença, e `GET /api/v1/license` devolve os mesmos dados para o seu código (veja [Autenticação](/autenticacao)).
---
# Documento final e verificação
## O que o PDF final contém
Quando o último signatário conclui, o SignXP monta a via definitiva:
1. as **assinaturas e rúbricas** carimbadas nas coordenadas informadas no envio;
2. o **Termo de Evidências**, com uma página geral e uma página por signatário (IP, dispositivo, data/hora, geolocalização, consentimento e imagens coletadas);
3. um **rodapé em todas as páginas** com o SHA-256 do conteúdo original e a numeração `Página X de Y`, para que nenhuma folha possa ser trocada;
4. um **selo criptográfico PAdES** aplicado com o certificado do SignXP — qualquer edição posterior invalida o selo e o Adobe Reader acusa.
## Verificação pública
Cada documento recebe uma URL de verificação, devolvida na consulta de status e no callback:
```
https://app.signxp.com.br/validar/{token}
```
A página é pública e mostra a validade, o hash do conteúdo, a lista de signatários e um QR Code. O mesmo QR fica impresso no Termo de Evidências, permitindo conferir a autenticidade a partir de uma via impressa.
> Publique essa URL no seu sistema: ela é a prova de autenticidade e continua válida independentemente de onde o arquivo esteja — e continua respondendo mesmo depois que os arquivos saem do nosso armazenamento (veja "Retenção dos arquivos", abaixo).
## Quem recebeu o documento e não tem o link
Nem sempre quem precisa conferir tem a URL: o contrato chega por e-mail, é
impresso, é reenviado adiante. Para esses casos existe o **validador público**:
```
https://validar.signxp.com.br
```
Ele aceita duas entradas, e nenhuma delas exige conta:
| Entrada | Como funciona |
|---|---|
| **Código de verificação** | A pessoa digita o código de 40 caracteres que vem impresso ao lado do QR Code, no Termo de Evidências — ou cola a URL de verificação inteira, que nós recortamos. É a mesma resposta da página `/validar`. |
| **Arquivo PDF** | A pessoa envia a via que tem em mãos. Calculamos o SHA-256 do arquivo e o casamos contra o que foi gravado no momento da selagem. |
A verificação por arquivo é a mais forte das duas: casar o hash prova, byte a
byte, que o PDF em mãos é exatamente o que o SignXP selou — nada foi acrescentado
nem removido depois. O validador ainda confere a **assinatura PAdES** embutida e
fixa o certificado contra o certificado público do SignXP, então um PDF assinado
por outra pessoa não passa por documento nosso.
### Verificando pelo seu próprio código
As duas provas também são endpoints públicos do motor. Não exigem chave de API:
```http
GET /api/verify/{token}
GET /api/verify/by-hash/{sha256}
GET /api/verify/certificate
```
- **`/api/verify/{token}`** e **`/api/verify/by-hash/{sha256}`** devolvem o mesmo
corpo: título, licenciado, situação, `content_hash` (SHA-256 do PDF original),
`sealed_hash` (SHA-256 da cópia selada), signatários e se o download está
liberado. Um `200` no `by-hash` já é a prova de integridade; `404` significa que
o arquivo não saiu daqui ou foi alterado depois de selado.
- **`/api/verify/certificate`** publica o certificado PÚBLICO do SignXP em PEM,
com o `fingerprint_sha256`. É o que permite ao seu código validar a assinatura
PAdES sem confiar no certificado que vem dentro do próprio arquivo.
O `sha256` é o hash da **cópia selada** — a via que o signatário recebeu, com
Termo de Evidências, rodapé e selo. Não confunda com o `content_hash`, que é o do
PDF original enviado por você: eles são diferentes de propósito.
> Verificação pública é um recurso da licença. Quando ela não o inclui, todos
> estes endereços respondem `404` — inclusive a página `/validar`.
## Retenção dos arquivos
Os arquivos (PDF original, cópia selada, Termo e imagens de identificação) podem
ter prazo de guarda definido na sua licença — dois prazos, na verdade, um para as
imagens de identidade (dado sensível, prazo em geral mais curto) e outro para os
documentos. Passado o prazo, eles saem do nosso armazenamento. Sem prazo
configurado, nada é apagado.
**A verificação não morre com o arquivo.** Metadados, hashes, signatários e o
token de verificação são preservados para sempre: a página pública continua
provando que o documento existiu, quem assinou e qual era exatamente o seu
conteúdo. O que deixa de existir é o download.
Na prática: publique a URL de verificação **e** guarde uma cópia do PDF final no
seu sistema, se você precisa do arquivo a longo prazo. O `document.completed` é o
sinal para baixá-lo — mas a selagem termina logo depois do aviso, então trate um
404 ali como "ainda não" e tente de novo, em vez de erro.
## Baixar a via final
O download da cópia certificada é controlado por `download_policy`, definido no envio:
| Valor | Comportamento |
|---|---|
| `all_signed` (padrão) | A via só fica disponível quando todos assinam. |
| `anytime` | O documento pode ser baixado a qualquer momento (o original, antes de concluir). |
| `disabled` | Nenhum download pela página pública; apenas a conferência de autenticidade. |
Use `disabled` para documentos sensíveis, em que a verificação deve confirmar a existência e a integridade sem expor o conteúdo.
> **A sua conta pode desligar o download inteiro.** Em **Configurações → Entrega
> do documento assinado**, no portal, você decide se o SignXP oferece a cópia a
> quem assinou. Desligado, o arquivo não sai por nenhuma ponta voltada ao
> signatário — tela de conclusão, e-mail de documento concluído e esta página —,
> vale para os documentos já em assinatura e o `download_policy` do envio passa
> a ser **ignorado**. É para quem entrega o documento pelo próprio canal.
>
> A verificação continua funcionando: o que sai é o arquivo, não a prova. E o
> seu acervo no portal não muda — você continua baixando o que é seu.
---
# Canais
Esta página é para quem embute o SignXP **dentro do próprio produto** e o oferece
aos clientes dele — um ERP, um CRM, um SaaS vertical. É diferente da integração
comum: lá existe uma conta e uma chave; aqui existem dezenas ou centenas, uma por
cliente seu, e nenhuma delas passa por uma tela nossa.
> **Acesso por contrato.** A API de canal não é auto-serviço: o segredo é emitido
> junto do contrato. Se você chegou aqui pela integração comum, a página que você
> procura é [Autenticação](/autenticacao).
## O modelo
**Cada cliente seu vira uma conta completa no SignXP** — com licença, quota,
marca, chaves e acervo próprios.
Poderia ser diferente: uma conta só para você, com um identificador de cliente em
cada documento. Não é, e a razão é prática. Quota, marca do e-mail, domínio de
assinatura, segredo de callback e separação do acervo são todos por conta. Numa
conta coletiva, seus clientes disputariam a mesma quota, receberiam e-mails com a
marca errada, e um veria o contrato do outro no acervo.
Você continua sendo o dono da relação: provisiona, suspende, reativa e acompanha
o consumo de todos por esta API. O que muda é que cada cliente tem o próprio
compartimento.
## Base e autenticação
```http
POST /api/partner/v1/tenants HTTP/1.1
Host: admin.signxp.com.br
Authorization: Bearer pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
```
O segredo `pk_` identifica o canal e vale para todas as rotas desta página.
**Ele não é uma chave de API de assinatura**: não envia documento nenhum. As
chaves `sk_live_`/`sk_test_` que assinam documentos são de cada conta, e saem no
provisionamento.
Um segredo de canal desativado responde `403` — o segredo está certo, o contrato
é que acabou.
## Provisionar uma conta
```http
POST /api/partner/v1/tenants
```
```json
{
"external_ref": "tenant-4711",
"name": "Solar Prime Engenharia",
"person_type": "company",
"document": "19.131.243/0001-97",
"legal_name": "Solar Prime Engenharia Ltda",
"zip_code": "01310-100",
"street": "Avenida Paulista",
"street_number": "1578",
"city": "São Paulo",
"state": "SP",
"technical_contact_name": "Marina Alves",
"technical_contact_email": "marina@solarprime.com.br",
"plan": "canal-basico",
"embed_origins": ["https://app.solarprime.com.br"]
}
```
O `external_ref` é **o id do cliente no seu sistema**, e é o que torna a chamada
idempotente: repetir com o mesmo valor devolve a conta que já existe em vez de
criar uma segunda. Mande sempre o mesmo valor — um timeout de rede não pode
virar cadastro duplicado.
O `plan` é opcional; sem ele vale o plano do seu contrato de canal. Sem nenhum
dos dois a conta nasce **sem plano** e não assina nada — útil se você quer criar
o cadastro antes de vender o módulo.
O cadastro da empresa é obrigatório aqui (ao contrário do que acontece quando a
equipe SignXP licencia pela tela). Por API não existe um "depois" em que alguém
volta para completar a ficha, e uma empresa sem CNPJ não tem como ser
identificada nem faturada no dia em que ela mesma contratar um plano.
### A resposta
```json
{
"reference": "solar-prime-engenharia",
"external_ref": "tenant-4711",
"name": "Solar Prime Engenharia",
"status": "active",
"billing_mode": "partner",
"plan": "canal-basico",
"monthly_document_quota": 25,
"keys": {
"live": "sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"test": "sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"callback_secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
```
**Guarde as chaves.** Elas existem em claro uma única vez, nesta resposta.
Repetir a chamada devolve `200` com os mesmos dados e **sem** o bloco `keys` — um
retry de rede não pode trocar as credenciais de uma conta que já está integrada e
funcionando. Quem perdeu o valor emite outra em
[Chaves](#emitir-uma-chave-nova).
O `reference` é o identificador da conta no SignXP. Ele volta nos callbacks e é o
caminho de todas as rotas seguintes — guarde-o ao lado do seu `external_ref`.
### Códigos de resposta
| Código | Significado |
|---|---|
| `201` | Conta criada. É a única resposta que traz as chaves. |
| `200` | Já existia com esse `external_ref`. Idempotente, sem chaves. |
| `409` | O CNPJ já pertence a outra conta do SignXP. |
| `422` | Cadastro incompleto ou inválido. |
| `503` | Não foi possível concluir — **repita a chamada**. |
O `409` merece atenção: ele significa que a empresa **já é cliente do SignXP**,
por conta própria ou por outro canal. A resposta traz o `reference` dela e um
`same_partner` dizendo se é sua. Criar de novo daria à mesma empresa duas
licenças, duas quotas e dois pagadores — então a saída é vincular a conta que já
existe, falando com o cliente.
```json
{
"message": "Esta empresa já tem uma conta no SignXP.",
"reference": "solar-prime-engenharia",
"name": "Solar Prime Engenharia",
"same_partner": false
}
```
O `503` vem com `"retryable": true` e significa que **nada foi criado** — a
resposta é binária de propósito: ou existe uma conta pronta com credenciais na
mão, ou não existe nada e repetir recomeça limpo. Nunca entregamos uma chave
antes de a conta estar operante do outro lado, porque uma chave que responde
`401` nos primeiros minutos derruba justamente o "ativar com um clique".
## Operar uma conta
Todas as rotas abaixo usam o `reference` devolvido no provisionamento. Conta de
outro canal responde `404` — você nunca enxerga o que não é seu.
```http
GET /api/partner/v1/tenants
GET /api/partner/v1/tenants/{reference}
PATCH /api/partner/v1/tenants/{reference}
POST /api/partner/v1/tenants/{reference}/suspend
POST /api/partner/v1/tenants/{reference}/resume
GET /api/partner/v1/tenants/{reference}/usage
POST /api/partner/v1/tenants/{reference}/keys
POST /api/partner/v1/tenants/{reference}/portal-link
```
O `PATCH` aceita `name`, `technical_contact_name`, `technical_contact_email`,
`embed_origins` e `plan`. O cadastro fiscal (documento, razão social, endereço)
fica de fora: é a identidade da empresa, e quem a corrige é ela mesma ou a equipe
SignXP.
**Suspender corta a emissão de documentos novos e nada mais.** O cliente
continua acessando o que já assinou, e a URL de verificação de cada documento
continua respondendo. Foi decisão de produto: ninguém deveria perder o acervo por
ter parado de contratar.
### Consumo e o aviso de franquia
```http
GET /api/partner/v1/tenants/{reference}/usage
```
```json
{
"period": "2026-08",
"documents_used": 20,
"documents_limit": 25,
"documents_remaining": 5,
"percent_used": 80,
"reference": "solar-prime-engenharia",
"external_ref": "tenant-4711"
}
```
Use o `percent_used` para avisar o cliente **antes** de a franquia acabar. Sem
esse aviso, o primeiro sinal de que o limite estourou é uma recusa da API no meio
de uma venda — e a essa altura você já perdeu a chance de oferecer o upgrade com
calma.
Estourar a quota não gera cobrança extra para o canal: gera o bloqueio, e o
caminho de saída é o cliente contratar um plano direto no SignXP, pelo
[link de acesso ao portal](#acesso-ao-portal-sob-demanda).
### A fronteira do canal
Quando um cliente seu contrata um plano diretamente com o SignXP, ele passa a
`billing_mode: "direct"`. Ele **continua sendo do seu canal** — você segue
atualizando contatos, nome e origens de embed, e segue lendo o consumo. O que
muda é o comercial: `plan` e `suspend` passam a responder `409`.
É proposital. A partir do momento em que a conta paga o SignXP, o plano e o
interruptor deixam de ser do canal — nenhum parceiro deve conseguir rebaixar ou
desligar uma conta que paga por fora.
### Emitir uma chave nova
```http
POST /api/partner/v1/tenants/{reference}/keys
```
```json
{ "name": "Rotação", "environment": "live" }
```
A resposta traz o `token` em claro. **A chave anterior continua válida** — quem a
revoga é o painel do SignXP ou o próprio cliente. Derrubá-la aqui cortaria a
integração no instante em que a substituta ainda não foi publicada do seu lado.
## Enviando documentos
Daqui para a frente é a [API de assinatura](/enviar-documento) normal,
autenticada com a chave `sk_live_` **da conta do cliente**. Não existe um envio
"de canal": quota, validação, idempotência e preparo são exatamente os mesmos.
Uma recomendação que vale para quase todo canal: como o PDF sai de um **template
seu**, as coordenadas dos campos são estáveis. Use `placement: "inline"` com
coordenadas fixas por template e o cliente nunca vê uma tela de posicionamento —
o documento sai pronto do seu app. O [posicionamento
assistido](/posicionamento-assistido) só se justifica quando o layout varia
documento a documento.
## Callback de canal
Os [callbacks](/callbacks) de todas as suas contas chegam numa **URL só**,
assinados com **um segredo só** — o do canal, entregue junto do `pk_`. É a razão
de o canal existir como conceito: sem isso você guardaria um `callback_secret`
por cliente só para conferir assinatura.
Cada entrega diz de quem é, no corpo e no cabeçalho:
```json
{
"event": "document.awaiting_review",
"id": 128,
"reference": "solar-prime-engenharia",
"external_id": "proposta-4711-0003",
"status": "awaiting_review"
}
```
```http
X-SignXP-Reference: solar-prime-engenharia
```
A URL do canal é o **padrão**, não uma imposição: se um envio trouxer
`callback_url` próprio, ele ganha, e a entrega vai assinada com o segredo daquela
conta. Um padrão não sobrepõe uma escolha feita na chamada.
A validação da assinatura é idêntica à da integração comum — só a chave muda.
Veja os exemplos em [Callbacks](/callbacks).
## Acesso ao portal sob demanda
Seu cliente vive dentro do seu produto e não deveria precisar de mais um login.
Mas três coisas só existem no portal do SignXP: contratar um plano quando a
franquia acaba, **liberar um documento retido pela [validação por
IA](/validacao-por-ia)**, e ver o acervo completo.
Para isso existe o link de uso único:
```http
POST /api/partner/v1/tenants/{reference}/portal-link
```
```json
{
"email": "marina@solarprime.com.br",
"target": "reviews"
}
```
```json
{
"url": "https://app.signxp.com.br/acesso/xxxxxxxx",
"expires_at": "2026-08-07T14:35:00-03:00",
"target": "reviews"
}
```
A conta no portal é criada na primeira vez, sem senha e sem convite por e-mail. O
link abre a sessão direto na tela pedida.
| `target` | Leva para |
|---|---|
| `plans` | Contratar ou trocar de plano. |
| `reviews` | Fila de validação manual — documentos esperando conferência. |
| `documents` | Acervo. |
| `billing` | Faturas e assinatura. |
| `dashboard` | Painel (padrão). |
**Peça o link no clique do usuário, não no cadastro dele.** Ele vale cinco
minutos, morre no primeiro uso, e emitir um novo invalida o anterior — a URL é a
credencial, e uma credencial guardada em banco ou em log é uma credencial
vazada.
Um `409` aqui significa que o e-mail informado já pertence a **outra** conta do
SignXP. Contas nunca trocam de dono por esta rota, nem para o canal que
provisionou o cliente: use outro endereço.
## O ciclo que fecha
A combinação que mais importa na prática:
1. Seu cliente envia uma proposta pela API, com a chave dele.
2. Todos assinam, mas a validação por IA não fecha o veredito de uma imagem.
3. Você recebe `document.awaiting_review` na URL do canal, com o `reference`.
4. Seu app mostra "1 documento aguardando sua conferência" para aquele cliente.
5. No clique, você pede um `portal-link` com `target: "reviews"`.
6. Ele confere, aprova, e o documento conclui — e volta para o seu app.
Sem o passo 3 o documento fica parado sem ninguém saber que a espera é de uma
pessoa, não de um signatário.
---