# 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. ---