API v1

Documentação da API

Integre assinatura simples por código de e-mail ou WhatsApp, com a marca do parceiro, ou assinatura qualificada ICP-Brasil com certificado em nuvem (Bird ID, VIDaaS) ou A1 — direto ou em sessão opcional de até 7 dias. Envie PDF ou JSON e acompanhe o resultado por webhook ou consulta de status. Serve para empresas, clínicas, ERPs e plataformas SaaS.

Os exemplos usam dados e credenciais fictícios. Para receber suas credenciais, cadastre-se como parceiro.

Novo: gere um exemplo de integração completo na sua linguagem e framework com o gerador de código com IA.

Gerar meu código

Assinatura eletrônica simples · whitelabel

Use mode: "simple" para confirmar o aceite com código de uso único, sem certificado ICP-Brasil. Envie um signatário (signer) ou várias pessoas em paralelo (signers, até 20): cada uma recebe link e código próprios, e o documento é concluído quando todas assinarem. A rubrica desenhada é opcional e não substitui o código.

POST /signatures/create · execute no servidor
{
  "mode": "simple",
  "document_name": "Contrato de exemplo",
  "document": {
    "title": "Contrato",
    "body_text": "Texto de exemplo para revisão."
  },
  "signer": {
    "full_name": "Maria Exemplo",
    "email": "maria@example.com"
  },
  "send_email_invitation": true,
  "expires_in_hours": 168,
  "allow_drawn_signature": true,
  "external_id": "contrato-123"
}
  • • Envie pdf_base64 ou document. Por segurança, pdf_url não é aceito neste modo.
  • • Informe signer.email, signer.phone ou os dois. Use otp_channel: "email" (padrão) ou "whatsapp" (Zapclient); sem o campo, o canal é o e-mail quando existe, senão WhatsApp. Telefones brasileiros de 10 ou 11 dígitos recebem DDI 55 automaticamente; outros países exigem país + DDD + número. Nome é obrigatório; CPF é opcional e declarado, não validado como identidade pelo código.
  • • Para várias pessoas no mesmo PDF, envie signers (lista de 1 a 20) no lugar de signer. Cada pessoa tem seu próprio canal, link e código; signer e signers não podem ser enviados juntos.
  • • Expiração: 1 a 720 horas; padrão 168 horas. Código: 10 minutos, 5 tentativas, intervalo de 60 segundos e até 10 envios por documento.
  • • test_mode não é aceito no modo simples: a confirmação precisa ser real.
  • • Configure nome, logo e cor em Área do cliente → Sua marca. A marca é preservada em cada solicitação.
  • • Cadastre a origem do app em Área do cliente → Credenciais para receber eventos seguros do embed.
  • • Convite por WhatsApp: envie send_whatsapp_invitation: true e cada pessoa com phone recebe uma mensagem com o nome dela, o nome da sua marca, o documento e o botão "Revisar e assinar". Pode ser combinado com send_email_invitation.
  • • Resposta: signature_id, sign_url, expires_at, invitation_sent, email_invitation_sent e whatsapp_invitation_sent. Com signers, recebe também signer_count e a lista signers com um sign_url por pessoa e o resultado do convite de cada uma. Se o convite falhar, use o link retornado.
  • • O PDF concluído inclui aceite, método, data UTC, IP quando disponível, navegador, hash original e QR de conferência. Não possui certificado ICP-Brasil; avalie a adequação ao seu documento.
  • • As consultas de status, download, cancelamento e webhooks são as mesmas da assinatura em nuvem.

Confirmação pelo WhatsApp

O código é enviado pelo Zapclient usando o template codigo_temporario, em pt_BR, com botão de copiar. O canal e o destinatário ficam fixos na criação. As credenciais de envio são da plataforma: não envie chaves Zapclient, template ou código OTP na chamada da API. A marca personaliza a tela, os e-mails e o registro final; o template do WhatsApp não inclui a logo do parceiro.

POST /signatures/create · assinatura simples por WhatsApp
{
  "mode": "simple",
  "otp_channel": "whatsapp",
  "document_name": "Termo de aceite",
  "document": {
    "title": "Termo de aceite",
    "body_text": "Texto fictício para revisão e aceite."
  },
  "signer": {
    "full_name": "Maria Exemplo",
    "email": "maria@example.com",
    "phone": "5511999999999"
  },
  "send_email_invitation": false,
  "expires_in_hours": 168,
  "allow_drawn_signature": true,
  "external_id": "termo-123",
  "webhook_url": "https://seusistema.com.br/webhooks/assinatura"
}
Dados fictícios. O código só é enviado quando o signatário o solicita na tela de assinatura; criar a solicitação não dispara OTP.
201 Created · exemplo de resposta
{
  "signature_id": "UUID_DA_SOLICITACAO",
  "status": "pending_authorization",
  "mode": "simple",
  "provider": "whatsapp",
  "sign_url": "https://assinatura.online/assinar/TOKEN_DA_SOLICITACAO",
  "expires_at": "2026-10-16T17:00:00.000Z",
  "invitation_sent": false
}

Várias pessoas no mesmo documento

Envie signers no lugar de signer para colheita em paralelo: cada pessoa abre um link próprio, recebe o código pelo seu canal e vê o progresso das demais no próprio painel. Enquanto faltarem assinaturas, cada uma baixa uma cópia individual com o seu carimbo; o documento consolidado, com todas as assinaturas, é liberado quando a última pessoa assinar.

POST /signatures/create · vários signatários
{
  "mode": "simple",
  "document_name": "Contrato de prestação de serviços",
  "document": {
    "title": "Contrato",
    "body_text": "Texto fictício para revisão e aceite."
  },
  "signers": [
    {
      "full_name": "Maria Exemplo",
      "email": "maria@example.com"
    },
    {
      "full_name": "João Exemplo",
      "phone": "5511999998888",
      "otp_channel": "whatsapp"
    },
    {
      "full_name": "Ana Exemplo",
      "email": "ana@example.com",
      "phone": "5521999997777",
      "otp_channel": "whatsapp"
    }
  ],
  "send_email_invitation": true,
  "send_whatsapp_invitation": true,
  "expires_in_hours": 168,
  "external_id": "contrato-124"
}
Dados fictícios. Envie signer ou signers, nunca os dois.
201 Created · um link por pessoa
{
  "signature_id": "UUID_DA_SOLICITACAO",
  "status": "pending_authorization",
  "mode": "simple",
  "provider": "email",
  "sign_url": "https://assinatura.online/assinar/TOKEN_DA_PRIMEIRA_PESSOA",
  "expires_at": "2026-10-16T17:00:00.000Z",
  "invitation_sent": true,
  "signer_count": 3,
  "signers": [
    {
      "name": "Maria Exemplo",
      "email": "maria@example.com",
      "phone": null,
      "channel": "email",
      "sign_url": "https://assinatura.online/assinar/TOKEN_1",
      "status": "pending"
    },
    {
      "name": "João Exemplo",
      "email": null,
      "phone": "5511999998888",
      "channel": "whatsapp",
      "sign_url": "https://assinatura.online/assinar/TOKEN_2",
      "status": "pending"
    },
    {
      "name": "Ana Exemplo",
      "email": "ana@example.com",
      "phone": null,
      "channel": "whatsapp",
      "sign_url": "https://assinatura.online/assinar/TOKEN_3",
      "status": "pending"
    }
  ]
}

Papel de cada pessoa

Envie role (até 60 caracteres) em cada signatário, por exemplo "Contratante", "Contratado" ou "Testemunha". O papel aparece na tela de assinatura, no registro do PDF, na consulta de status e nos webhooks.

signers com papel
{
  "signers": [
    {
      "full_name": "Maria Exemplo",
      "email": "maria@example.com",
      "role": "Contratante"
    },
    {
      "full_name": "João Exemplo",
      "phone": "5511999998888",
      "role": "Testemunha"
    }
  ]
}

Ordem de assinatura

Por padrão, signing_order: "parallel": todos podem assinar ao mesmo tempo. Com "sequential", a ordem da lista signers vale: só a primeira pessoa recebe o convite; cada uma é avisada automaticamente (pelos canais de convite escolhidos) quando a anterior assinar. Quem abrir o link antes da vez pode revisar o documento, mas não recebe código (erro not_your_turn).

assinatura em fila
{
  "signing_order": "sequential",
  "send_email_invitation": true,
  "signers": [
    {
      "full_name": "Maria Exemplo",
      "email": "maria@example.com",
      "role": "Locadora"
    },
    {
      "full_name": "João Exemplo",
      "email": "joao@example.com",
      "role": "Locatário"
    }
  ]
}

Rubrica visual nas páginas

Envie stamps em cada signatário para colocar a rubrica (nome, data, canal, desenho quando houver e QR de conferência) em páginas do próprio documento. page começa em 1; x e y são frações de 0 a 1 medidas a partir do canto superior esquerdo. Até 50 posições por pessoa. A rubrica só aparece depois que a pessoa assina; o registro completo continua nas folhas finais.

signers com rubrica
{
  "signers": [
    {
      "full_name": "Maria Exemplo",
      "email": "maria@example.com",
      "stamps": [
        {
          "page": 1,
          "x": 0.08,
          "y": 0.85
        },
        {
          "page": 3,
          "x": 0.6,
          "y": 0.8
        }
      ]
    }
  ]
}

Lembrete antes de expirar

reminder_hours_before (padrão 24, de 0 a 168; 0 desativa) define quando quem ainda não assinou recebe um lembrete por e-mail e/ou WhatsApp, pelos contatos cadastrados. É enviado um lembrete por pessoa; na ordem sequencial, só para quem está na vez.

Reenviar convite

POST /api/public/v1/signatures/resend reenvia o convite (como lembrete) para quem ainda não assinou. Sem signer_sign_url, vale para todas as pessoas pendentes. channels é opcional (email, whatsapp); por padrão usa os contatos cadastrados. Limite: um reenvio a cada 10 minutos por pessoa e no máximo 10 convites.

POST /signatures/resend
{
  "signature_id": "UUID_DA_SOLICITACAO",
  "signer_sign_url": "https://assinatura.online/assinar/TOKEN_2",
  "channels": [
    "whatsapp"
  ]
}
200 OK
{
  "signature_id": "UUID_DA_SOLICITACAO",
  "status": "pending_authorization",
  "resent": [
    {
      "name": "João Exemplo",
      "sign_url": "https://assinatura.online/assinar/TOKEN_2",
      "email_sent": false,
      "whatsapp_sent": true,
      "skipped": null
    }
  ]
}
skipped indica rate_limited ou limit_reached quando o envio não foi feito.

Enviar ou reenviar o PDF assinado

POST /api/public/v1/signatures/deliver envia o PDF assinado de um documento concluído para todas as pessoas do documento, nos canais indicados. A cobrança é por envio (cada pessoa em cada canal), debitada da carteira: WhatsApp R$ 0,30 e e-mail R$ 0,10. Pessoas sem o contato do canal não são cobradas; sem saldo, o envio é pulado; se o envio falhar, o valor é estornado. Limite: 10 envios por documento por hora. Indisponível para contas cobradas pela Prescri. No WhatsApp, a mensagem usa um template aprovado pela Meta com o PDF anexo e botão para abrir o documento — o parceiro não precisa criar nem configurar template; paciente, clínica, sistema, documento e assinante são preenchidos automaticamente.

Opcional: envie delivery.filename na criação (simples ou qualificada) para definir o nome do anexo. No envio/reenvio, o mesmo campo substitui o nome somente daquela chamada, sem alterar o PDF ou a assinatura. Acentos são preservados; caracteres de caminho são removidos e a extensão é sempre .pdf. Sem personalização, usamos o nome do documento com _Assinado.pdf. Isso não ativa o envio automático nem muda a cobrança.

POST /signatures/deliver
{
  "signature_id": "UUID_DA_SOLICITACAO",
  "channels": [
    "whatsapp",
    "email"
  ],
  "delivery": {
    "filename": "Relatório Médico.pdf"
  }
}

Entregar a quem não assina (ex.: paciente)

Envie recipients (1 a 5 pessoas, cada uma com phone e/ou email) para entregar o PDF somente a essas pessoas — por exemplo, o paciente de uma receita assinada só pelo médico com Bird ID ou VIDaaS. Vale para assinatura qualificada e simples; quem assinou não recebe nessa chamada. A cobrança é a mesma: por pessoa e por canal.

POST /signatures/deliver — paciente
{
  "signature_id": "UUID_DA_SOLICITACAO",
  "channels": [
    "whatsapp"
  ],
  "recipients": [
    {
      "name": "Nome do Paciente",
      "phone": "5549999990000"
    }
  ],
  "delivery": {
    "filename": "Receita.pdf"
  }
}
Exemplo fictício. Telefone com DDD (10 a 13 dígitos). Sem saldo na carteira, o envio é pulado (skipped).

Envio automático ao concluir: no POST /signatures/create (Bird ID, VIDaaS ou A1), informe delivery.whatsapp/delivery.email com phone/email_to (ou o objeto patient). Assim que o médico assinar, o PDF vai ao paciente sem nova chamada — o médico continua o único assinante. doctor_cpf é aceito como sinônimo de signer.cpf. Mesma cobrança por pessoa e canal; sem saldo, o envio é pulado sem afetar a assinatura.

POST /signatures/create — envio automático ao paciente
{
  "provider": "birdid",
  "doctor_cpf": "12345678900",
  "document_name": "Receita",
  "pdf_url": "https://seusistema.com/receita.pdf",
  "patient": {
    "full_name": "João da Silva",
    "phone": "5511999998888"
  },
  "delivery": {
    "whatsapp": true,
    "email": true,
    "phone": "5511999998888",
    "email_to": "joao@email.com",
    "patient_name": "João da Silva",
    "filename": "Receita.pdf"
  }
}
Exemplo fictício. Os campos clinic_name e system_name também são aceitos (formato compatível com a Prescri).
200 OK
{
  "signature_id": "UUID_DA_SOLICITACAO",
  "sent": 3,
  "failed": 0,
  "skipped": 0,
  "charged_cents": 70,
  "deliveries": [
    {
      "channel": "whatsapp",
      "recipient": "***4321",
      "status": "sent",
      "charged_cents": 30,
      "reason": null
    },
    {
      "channel": "whatsapp",
      "recipient": "***8765",
      "status": "sent",
      "charged_cents": 30,
      "reason": null
    },
    {
      "channel": "email",
      "recipient": "m***@example.com",
      "status": "sent",
      "charged_cents": 10,
      "reason": null
    }
  ]
}
Erros: 404 not_found, 409 not_completed, 403 not_available, 429 rate_limited.

Recusa pelo signatário

Na tela de assinatura, a pessoa pode escolher "Não concordo e quero recusar" e informar o motivo. O documento passa para status: "rejected", os demais links deixam de aceitar assinatura e você recebe o webhook signature.rejected com nome, papel, motivo e data. Na consulta de status, o signatário traz rejected_at e rejection_reason.

webhook · signature.rejected
{
  "event": "signature.rejected",
  "signature_id": "UUID_DA_SOLICITACAO",
  "data": {
    "status": "rejected",
    "mode": "simple",
    "signer": "João Exemplo",
    "role": "Testemunha",
    "reason": "Valor diferente do combinado",
    "rejected_at": "2026-10-10T14:00:00.000Z"
  }
}

Documento concluído

Quando a última pessoa assina, o PDF final é gerado e cada signatário com e-mail recebe automaticamente o link para baixar o documento e o registro. Você recebe o webhook signature.completed.

Neste modo, provider na resposta indica o canal (email ou whatsapp), não um certificado. Não há endpoint público para o parceiro enviar ou confirmar OTP: o fluxo acontece na tela de assinatura. Não altere a identidade do destinatário durante a confirmação. Assinatura avançada não está disponível.

Embed no aplicativo do parceiro · sem credenciais no navegador
// Carregue https://assinatura.online/assinatura-sdk.js no app.
// Obtenha sign_url do SEU servidor, que guarda API Key e Secret.
AssinaturaOnline.open({
  signUrl: resposta.sign_url,
  mode: "iframe",
  container: "#assinatura",
  height: 900,
  onEvent(event) {
    if (event.type === "signature.completed") {
      // Consulte o status pelo seu servidor antes de liberar o documento.
    }
  }
});

Início rápido

  1. 1. Crie a solicitação com um PDF ou com um documento em JSON.
  2. 2. Envie o sign_url ao signatário (ou abra pelo SDK embed). Exceção: no A1 direto ou sessão A1, a resposta já vem com status: "signed" e o PDF, sem link.
  3. 3. Na simples, o signatário revisa, aceita e confirma o código. Na qualificada, aprova com Bird ID ou VIDaaS, ou envia o próprio A1 na página hospedada.
  4. 4. Receba o webhook signature.completed e baixe o PDF assinado.
base-url
BASE_URL=https://assinatura.online/api/public/v1

Teste em 2 minutos

Cole no terminal com suas credenciais. O test_mode gera um envio sem assinatura válida.

terminal
curl -X POST "https://assinatura.online/api/public/v1/signatures/create" \
  -H "X-API-Key: $ASSINATURA_API_KEY" \
  -H "X-API-Secret: $ASSINATURA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"provider":"birdid","document_name":"Contrato de exemplo","pdf_url":"https://seusistema.com.br/exemplo.pdf","signer":{"cpf": "52998224725","full_name":"Maria Exemplo"},"test_mode":true}'
Baixar coleção Postman / Insomnia

Autenticação

Toda chamada leva sua chave e seu segredo nos cabeçalhos. Faça as chamadas só a partir do seu servidor e guarde o segredo em variáveis de ambiente.

headers
X-API-Key: pk_exemplo_0000000000000000
X-API-Secret: sk_exemplo_0000000000000000
Content-Type: application/json
Nunca coloque o segredo em código que roda no navegador. O mesmo segredo assina os webhooks.

Certificados suportados

Certificado A1 (.pfx / .p12)

Usa certificados A1 ICP-Brasil em arquivo .pfx ou .p12 compatível. Valide o arquivo e a cadeia do seu certificado antes de usar documentos reais. Pela API: página hospedada (sign_url), A1 direto no corpo da chamada ou sessão A1 opcional de até 7 dias. Também disponível no assinador online.

Certificado em nuvem

Integração direta com Bird ID (Soluti) e VIDaaS (Valid). O signatário aprova no aplicativo do celular com PIN ou biometria.

Sem plugin de assinatura no navegador

Não precisa de WebPKI, drivers nem extensões no navegador. Para Bird ID e VIDaaS, é necessário ter o certificado em nuvem e o aplicativo do provedor configurados. No A1, o arquivo e a senha são enviados ao serviço para assinar: no fluxo direto e na página hospedada ficam só em memória; na sessão A1 opcional ficam cifrados até a sessão expirar ou ser revogada.

  • Padrão PAdES, conferível no validador oficial do ITI.
  • Tokens e cartões USB (A3 físico) não são aceitos, porque dependem de software instalado no computador.

Criar solicitação com PDF

POST /signatures/create
curl -X POST "$BASE_URL/signatures/create" \
  -H "X-API-Key: $ASSINATURA_API_KEY" \
  -H "X-API-Secret: $ASSINATURA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "birdid",
    "document_name": "Contrato de prestação de serviço",
    "pdf_url": "https://seusistema.com.br/contratos/2431.pdf",
    "signer": { "cpf": "52998224725", "full_name": "Maria Exemplo", "email": "maria@exemplo.com.br" },
    "external_id": "pedido-2431",
    "metadata": { "unidade": "matriz" },
    "webhook_url": "https://seusistema.com.br/webhooks/assinatura",
    "redirect_url": "https://seusistema.com.br/assinatura-concluida"
  }'
201 Created
{
  "signature_id": "6f1c…",
  "status": "pending_authorization",
  "sign_url": "https://assinatura.online/assinar/EXEMPLO",
  "expires_at": "2026-10-06T15:30:00.000Z",
  "provider": "birdid"
}
  • provider: birdid, vidaas ou a1. Com a1, o sign_url abre uma página onde o signatário envia o próprio certificado A1 (.pfx) e a senha, usados só em memória durante a assinatura; o CPF do certificado precisa ser igual a signer.cpf (senão o erro é certificate_cpf_mismatch).
  • A1 síncrono (sistemas com cofre próprio): com provider: "a1", envie também certificate: { "pfx_base64": "…", "password": "…" }. A assinatura acontece na mesma chamada e a resposta já traz status: "signed", synchronous: true, signature_token, pdf_url e pdf_base64 (envie return_pdf: false para omitir). O certificado e a senha são usados só em memória e não são gravados. Sem certificate, continua valendo o sign_url para o titular assinar no navegador. Para não reenviar o arquivo a cada chamada, use a sessão A1 opcional (até 604800 s / 7 dias), em que o titular autoriza e o material fica cifrado até expirar ou ser revogado.
  • Envie pdf_url (HTTPS) ou pdf_base64 — nunca os dois. Limite de 15 MB.
  • signer.cpf é obrigatório: o provedor usa o CPF para localizar o certificado.
  • Opcional: stamp_options para posicionar o carimbo com QR Code. Por padrão, documentos sem número SNCR recebem o carimbo no canto superior direito do cabeçalho (primeira página); com SNCR, o quadro oficial ocupa o topo e o carimbo vai para o canto inferior direito, acima do rodapé.
  • Na assinatura qualificada, stamp_options.page aceita first, last ou all. Essa escolha prevalece sobre pageIndex (índice iniciado em 0, ainda aceito). all aplica o carimbo antes da assinatura em todas as páginas; o quadro SNCR permanece somente na primeira. Na simples, rubricas continuam usando signers[].stamps com página numérica iniciada em 1.
Opções na criação qualificada
{
  "stamp_options": {
    "page": "last",
    "position": "bottom-right"
  },
  "delivery": {
    "filename": "Relatório Médico.pdf"
  }
}

Criar solicitação com JSON (sem PDF)

Envie o objeto document no lugar do PDF. Nós montamos o PDF com cabeçalho da sua marca, paginação automática, rodapé com endereço e o carimbo com QR Code de validação. Use body_text para texto corrido (contratos, termos, atestados, laudos) e items para listas (cláusulas, medicamentos, orientações).

POST /signatures/create — document
{
  "provider": "vidaas",
  "document_name": "Termo de aceite",
  "signer": { "cpf": "52998224725", "full_name": "Maria Exemplo" },
  "document": {
    "title": "Termo de aceite de serviço",
    "subtitle": "Versão 3",
    "body_text": "Declaro que li e aceito as condições...\n\nSegundo parágrafo.",
    "items": [
      { "title": "Prazo", "description": "12 meses a partir da assinatura." },
      { "title": "Dipirona 500 mg", "quantity": "1 caixa", "instructions": "1 comprimido a cada 6 horas" }
    ],
    "patient_name": "João Exemplo",
    "place_and_date": "São Paulo, 5 de outubro de 2026",
    "logo_url": "https://seusistema.com.br/logo.png",
    "clinic_name": "Empresa Exemplo",
    "clinic_cnpj": "00.000.000/0001-00",
    "clinic_phone": "(11) 0000-0000",
    "address": { "street": "Rua Exemplo", "number": "100", "district": "Centro",
                 "city": "São Paulo", "state": "SP", "zip": "00000-000" }
  }
}
title e (body_text ou items) são obrigatórios. Logo em PNG ou JPG por HTTPS, até 2 MB.

Modelos de saúde prontos (document.type)

Informe document.type e a API monta o modelo completo: cabeçalho da clínica, profissional, caixa do paciente, itens, data por extenso, identificação do prescritor centralizada abaixo da data (com a menção “Assinado digitalmente”) e espaço à direita, acima do rodapé, para o carimbo de assinatura com QR Code. A lista de modelos fica em GET /api/public/v1/document-types (sem autenticação). Vale para assinatura qualificada e para mode: "simple".

  • • prescription_simple — receita simples; itens com continuous_use recebem "[USO CONTÍNUO]".
  • • prescription_rce — Receita de Controle Especial (RCE), 2 vias com comprador/fornecedor e quadro de numeração SNCR do médico.
  • • prescription_ret — Receita Sujeita à Retenção (RET), 2 vias e numeração SNCR; inclui antimicrobianos e agonistas GLP-1 conforme as regras da Anvisa.
  • • prescription_special_control — depreciado, sem numeração (equivale ao "C" antigo). Aceito só por compatibilidade; veja Saúde e SNCR.
  • • notification_b / notification_a — número em destaque no topo. Sem document.sncr_number, usamos a carteira SNCR do médico (ou o sncr_reservation_token).
  • • medical_certificate — exige days_off (0 = comparecimento). O CID só aparece se você enviar cid, com nota de autorização do paciente.
  • • medical_report — exige body_text; seções opcionais history, conduct, conclusion.
  • • exam_request — lista de exames em items e clinical_indication.
POST /signatures/create — document.type
{
  "provider": "birdid",
  "document_name": "Receita 001",
  "signer": {
    "cpf": "52998224725"
  },
  "document": {
    "type": "prescription_ret",
    "clinic": {
      "name": "Clínica Exemplo",
      "cnes": "1234567",
      "phone": "(11) 0000-0000",
      "address": "Av. Exemplo, 100 - São Paulo - SP",
      "logo_url": "https://seusistema.com.br/logo.png"
    },
    "prescriber": {
      "name": "Dra. Maria Exemplo",
      "crm": "123456",
      "crm_uf": "SP",
      "specialty": "Neurologia",
      "rqe": "9876"
    },
    "patient": {
      "name": "João Exemplo",
      "cpf": "12345678909",
      "birth_date": "1985-04-12",
      "address": "Rua Exemplo, 1 - São Paulo/SP"
    },
    "items": [
      {
        "name": "Amoxicilina 500 mg",
        "quantity": "21 cápsulas",
        "instructions": "1 cápsula de 8 em 8 horas por 7 dias.",
        "continuous_use": false
      }
    ],
    "notes": "Retorno em 30 dias.",
    "city": "São Paulo",
    "state": "SP",
    "issued_at": "2026-10-15"
  }
}
Dados fictícios. prescription_ret exige número SNCR do tipo RET na carteira do médico (sem saldo: 409 sncr_no_numbers; em test_mode recebe TESTE-SEM-VALOR). Campos inválidos retornam 422 invalid_document com a lista de campos. O formato genérico (title, body_text, items) continua aceito.

Envelopes: vários PDFs em um único link

Reúna de 1 a 10 PDFs em documents. Cada pessoa recebe um único link para revisar todos os documentos e confirmar o aceite com um código. Os mesmos signatários assinam todos os documentos, em paralelo ou em ordem.

POST /signatures/create — envelope simples
{
  "mode": "simple",
  "document_name": "Admissão — contrato e termo",
  "documents": [
    {
      "document_name": "Contrato",
      "pdf_base64": "BASE64_DO_CONTRATO"
    },
    {
      "document_name": "Termo de confidencialidade",
      "pdf_base64": "BASE64_DO_TERMO"
    }
  ],
  "signers": [
    {
      "full_name": "Maria Exemplo",
      "email": "maria@example.com",
      "stamps": [
        {
          "document_index": 0,
          "page": 1,
          "x": 0.1,
          "y": 0.7
        },
        {
          "document_index": 1,
          "page": 1,
          "x": 0.1,
          "y": 0.7
        }
      ]
    }
  ],
  "send_email_invitation": true,
  "signing_order": "parallel",
  "external_id": "admissao-123"
}
Exemplo fictício: substitua os conteúdos base64 por PDFs reais. document_index começa em 0; page começa em 1 dentro de cada PDF.
  • Limites: 10 PDFs, 10 MB por arquivo, 20 MB no total e 500 páginas no envelope. PDFs protegidos por senha não são aceitos.
  • Não combine documents com pdf_base64, pdf_url ou document na raiz. Envelopes são exclusivos do modo simples, sem qualified_signer.
  • Criação retorna signature_id, document_count, documents (id, name, position, sha256) e signers com um link por pessoa. O envelope não pode ser alterado depois de enviado.
  • Todos os PDFs só são concluídos quando todas as pessoas assinarem. Recusa, cancelamento e expiração encerram o envelope inteiro, sem consumo. Cobrança por documento concluído: 3 PDFs = 3 documentos, independentemente da quantidade de signatários.
  • GET /signatures/get?signature_id=… retorna a lista e os hashes de cada PDF. O webhook signature.completed é único por envelope e inclui document_count e documents. Confirme o resultado consultando a API.
  • GET /signatures/pdf?signature_id=…&document_id=…: PDF individual. GET /signatures/pdf?signature_id=…&format=zip: todos os PDFs com registro de aceite e manifesto de integridade. Sem parâmetros adicionais: PDF reunido com registro consolidado. Downloads da API exigem credenciais do parceiro.
  • Envios pagos de PDF continuam por destinatário e canal: o anexo enviado é a cópia reunida do envelope; não há cobrança de envio por PDF interno.

Consultar status

GET /signatures/get
curl "$BASE_URL/signatures/get?signature_id=6f1c…" \
  -H "X-API-Key: $ASSINATURA_API_KEY" -H "X-API-Secret: $ASSINATURA_API_SECRET"

{
  "signature_id": "6f1c…",
  "status": "signed",
  "signer_count": 2,
  "pending_signers": 0,
  "signers": [
    { "name": "Maria Exemplo", "channel": "email", "status": "signed", "signed_at": "2026-10-05T12:01:02.110Z" },
    { "name": "João Exemplo", "channel": "whatsapp", "status": "signed", "signed_at": "2026-10-05T12:04:11.482Z" }
  ],
  "pdf_url": "https://assinatura.online/d/EXEMPLO",
  "signed_at": "2026-10-05T12:04:11.482Z",
  "external_id": "pedido-2431",
  "dispensed_at": null,
  "sncr_number": null,
  "test_mode": false
}

Estados: pending_authorization, authorized, signed, failed, cancelled, expired. O PDF assinado pode ser baixado em GET /signatures/pdf?signature_id=…. A consulta também retorna modee otp_channel (email ou whatsapp na simples; null nas demais modalidades). Use mode: "simple" para distinguir registro de aceite de assinatura com certificado.

Cancelar

POST /signatures/cancel
curl -X POST "$BASE_URL/signatures/cancel" \
  -H "X-API-Key: $ASSINATURA_API_KEY" -H "X-API-Secret: $ASSINATURA_API_SECRET" \
  -H "Content-Type: application/json" -d '{ "signature_id": "6f1c…" }'

{ "signature_id": "6f1c…", "status": "cancelled" }

Assinatura híbrida: simples + ICP-Brasil

Combine, no mesmo documento, várias pessoas assinando com código (e-mail ou WhatsApp) e uma pessoa final assinando com certificado ICP-Brasil (Bird ID, VIDaaS ou A1). Envie mode: "simple" com a lista de signers e o bloco qualified_signer.

POST /signatures/create — híbrida
{
  "mode": "simple",
  "document_name": "Contrato de locação",
  "pdf_base64": "JVBERi0xLjQK...",
  "signing_order": "sequential",
  "send_email_invitation": true,
  "send_whatsapp_invitation": true,
  "signers": [
    { "full_name": "Maria Exemplo", "email": "maria@exemplo.com.br", "otp_channel": "email", "role": "Locatária" },
    { "full_name": "João Exemplo", "phone": "5561900000000", "otp_channel": "whatsapp", "role": "Fiador" }
  ],
  "qualified_signer": {
    "full_name": "Ana Exemplo",
    "cpf": "52998224725",
    "email": "ana@exemplo.com.br",
    "provider": "birdid"
  },
  "webhook_url": "https://seusistema.com.br/webhooks/assinatura"
}
  • A etapa ICP-Brasil acontece sempre por último, depois que todos os signatários simples concluírem, sobre o PDF final — qualquer alteração posterior invalidaria a assinatura PAdES.
  • qualified_signer.provider: birdid, vidaas ou a1. O CPF precisa corresponder ao certificado usado.
  • Se qualified_signer.email for informado, a pessoa recebe o link por e-mail quando chegar a vez dela.
  • O webhook de conclusão só é emitido depois da etapa ICP-Brasil. O status permanece pendente até lá.
  • Os signatários simples continuam sem certificado ICP-Brasil; apenas a assinatura final é qualificada.

Sessões: uma autorização, vários documentos

Para Bird ID e VIDaaS, crie uma sessão e envie o authorize_url ao signatário. Ele aprova uma vez no aplicativo do certificado. Enquanto a sessão estiver ativa, cada POST /signatures/create com session_id é assinado na hora, sem nova aprovação. O CPF do documento precisa ser o mesmo da sessão. A duração máxima depende do provedor. Para certificado A1 também existe sessão opcional (até 604800 s / 7 dias) — veja A1 direto e sessão A1.

POST /sessions/create
{ "provider": "birdid", "signer": { "cpf": "00000000000", "full_name": "Maria Exemplo" },
  "session_lifetime_seconds": 28800, "redirect_url": "https://seusistema.com.br/voltar" }

# 201 Created
{ "session_id": "a1b2…", "status": "pending_authorization",
  "authorize_url": "https://assinatura.online/sessao/…", "session_lifetime_seconds": 28800 }
Depois de autorizada
GET  /sessions/get?session_id=a1b2…   → { "status": "authorized", "signatures_count": 0, … }
POST /signatures/create  { "provider": "birdid", "session_id": "a1b2…", "signer": { "cpf": "00000000000" }, … }
     → { "status": "signed", "signature_token": "…", "sign_url": "https://assinatura.online/d/…" }
POST /sessions/revoke    { "session_id": "a1b2…" }   → encerra antes do prazo

Abrir a sessão na primeira assinatura

Se preferir não criar a sessão antes, envie request_session: true no primeiro POST /signatures/create (somente birdid ou vidaas, sem session_id e fora do modo de testes). A resposta já traz o session_id. Quando o signatário autorizar esse primeiro documento no sign_url, o documento é assinado e a mesma autorização ativa a sessão — os próximos documentos com esse session_id são assinados na hora. O prazo segue session_lifetime_seconds, limitado ao máximo de cada provedor (Bird ID até 7 dias; VIDaaS até 12 horas). Se o primeiro documento não for autorizado, a sessão não é ativada.

POST /signatures/create com request_session
{ "provider": "vidaas", "request_session": true, "session_lifetime_seconds": 43200,
  "document_name": "Receita 001", "pdf_url": "https://seusistema.com.br/r/001.pdf",
  "signer": { "cpf": "00000000000", "full_name": "Maria Exemplo" } }

# 201 Created
{ "signature_id": "…", "status": "pending_authorization", "sign_url": "https://assinatura.online/assinar/…",
  "session_id": "a1b2…", "session_status": "pending_authorization", "session_lifetime_seconds": 43200 }

# Depois que o signatário autorizar o primeiro documento:
GET  /sessions/get?session_id=a1b2…   → { "status": "authorized", "signatures_count": 1, … }
POST /signatures/create  { "provider": "vidaas", "session_id": "a1b2…", … }   → { "status": "signed", … }

Boa prática: envie poucas requisições em paralelo (por exemplo 3 a 5) ao assinar vários documentos na mesma sessão. Não há endpoint de lote: cada PDF tem hash, código e carimbo próprios.

Dispensação e conferência

Registre que um documento assinado foi conferido ou usado — por exemplo receita dispensada na farmácia, retirada de produto ou conferência no balcão. Cada documento só pode ser dispensado uma vez.

POST /signatures/dispense
{
  "signature_id": "6f1c…",
  "dispensed_by": "Farmácia Exemplo — CNPJ 00.000.000/0001-00",
  "notes": "Dispensado integralmente"
}

# 200 OK
{ "signature_id": "6f1c…", "dispensed_at": "2026-10-05T14:20:00.000Z" }

Webhooks

Cadastre uma URL padrão na Área do Cliente (aba Webhooks, com botão de teste) ou envie webhook_url em cada requisição — a URL da requisição tem prioridade. Enviamos um POST nos eventos abaixo, assinado com HMAC-SHA256 usando seu API Secret. Em caso de falha, tentamos de novo até 4 vezes. Valide sempre com o corpo cru da requisição.

cabeçalhos
X-Assinatura-Event: signature.completed
X-Assinatura-Timestamp: 1791203051
X-Assinatura-Signature: t=1791203051,v1=<hex>
X-Assinatura-Delivery: <id>
X-Assinatura-Attempt: 1
signature.completed — documento assinado
{
  "event": "signature.completed",
  "signature_id": "6f1c…",
  "status": "signed",
  "external_id": "pedido-2431",
  "pdf_url": "https://assinatura.online/d/EXEMPLO",
  "signature_token": "EXEMPLO",
  "signed_at": "2026-10-05T12:04:11.482Z",
  "metadata": { "unidade": "matriz" }
}
signature.participant_signed — uma pessoa assinou (documento com várias)
{
  "event": "signature.participant_signed",
  "signature_id": "6f1c…",
  "status": "pending_authorization",
  "signer": "Maria Exemplo",
  "signer_status": "signed",
  "signed_at": "2026-10-05T12:01:02.110Z",
  "pending": 1
}
Enviado a cada assinatura registrada enquanto faltarem pessoas. O documento ainda não está concluído: aguarde signature.completed.
signature.cancelled — cancelada
{
  "event": "signature.cancelled",
  "signature_id": "6f1c…",
  "status": "cancelled",
  "cancelled_by": "organization",
  "external_id": "pedido-2431",
  "occurred_at": "2026-10-05T12:10:00.000Z"
}
signature.expired — prazo encerrado
{
  "event": "signature.expired",
  "signature_id": "6f1c…",
  "status": "expired",
  "expired_at": "2026-10-06T15:30:00.000Z",
  "external_id": "pedido-2431"
}
A expiração é aplicada na leitura do status ou pela varredura agendável. Sem agendamento, o evento pode ser enviado somente após uma consulta; não dependa de entrega exatamente no horário de vencimento.
signature.dispensed — conferência registrada
{
  "event": "signature.dispensed",
  "signature_id": "6f1c…",
  "status": "signed",
  "pdf_url": "https://assinatura.online/d/EXEMPLO",
  "dispensed_at": "2026-10-05T14:20:00.000Z",
  "dispensed_by": "Farmácia Exemplo",
  "notes": "Dispensado integralmente"
}
validacao-hmac.ts
import crypto from "node:crypto";

const TOLERANCIA_S = 300; // mesma tolerância do verifyWebhook do SDK

/** Valida o webhook antes de qualquer efeito. O segredo fica só no servidor. */
export function webhookValido(timestamp: string, header: string, rawBody: string): boolean {
  if (!/^\d{1,12}$/.test(timestamp)) return false;                    // Unix em segundos
  const agora = Math.floor(Date.now() / 1000);
  if (Math.abs(agora - Number(timestamp)) > TOLERANCIA_S) return false; // passado ou futuro
  const recebido = /(?:^|,)\s*v1=([0-9a-f]{64})(?:,|$)/i.exec(header)?.[1]?.toLowerCase();
  if (!recebido) return false;                                         // 64 hex antes de comparar
  const esperado = crypto
    .createHmac("sha256", process.env.ASSINATURA_API_SECRET!)
    .update(`${timestamp}.${rawBody}`)                              // corpo BRUTO, sem reserializar
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(recebido, "hex"), Buffer.from(esperado, "hex"));
}

// No handler: rawBody = await request.text(); se !webhookValido(...) → 401.
// Depois (ex.: signature.completed): consulte GET /signatures/get; se não confirmar
// "signed" → 503. Então atualize o pedido E grave a deduplicação na MESMA transação
// (helper seu) e só responda 2xx após o COMMIT; erro → 503 para nova tentativa.
// O payload não tem event_id. signature_id + event basta para completed, mas
// participant_signed ocorre várias vezes: inclua signer e signed_at na chave.

Saúde e SNCR

Módulo para clínicas, prontuários e telemedicina. Precisa ser habilitado na sua conta (campos médicos). Inclua medical_fields (CRM, UF e especialidade) para que apareçam no carimbo.

Antes de liberar: enquanto o módulo não estiver habilitado, qualquer requisição com medical_fields, prescription_type, cnes ou reserva SNCR retorna 403 medical_fields_disabled e nada é assinado. Para assinar documentos comuns no mesmo momento, envie a requisição sem esses campos. Para habilitar, fale com o suporte; depois da liberação, reenvie a mesma requisição.

  • Período de transição até 29/10/2026. A partir de 30/10/2026, a Receita de Controle Especial (RCE) e a Receita Sujeita à Retenção (RET, que engloba antimicrobianos e agonistas GLP-1), quando eletrônicas, devem ser emitidas integradas ao SNCR, com numeração. O papel continua sendo opção, conforme as regras da Anvisa. Fonte: Anvisa — nova etapa do SNCR e documentos do SNCR.
  • Notificações com numeração: NRA (amarela), NRB (azul B1), NRB2 (azul B2), NRR (retinoides) e NRT (talidomida). Os aliases A, B, B2, R e T também são aceitos.
  • Receitas numeradas sem cota prévia da VISA: RCE (Receita de Controle Especial) e RET (Receita Sujeita à Retenção: antimicrobianos e agonistas GLP-1; retinoides são NRR). Não depender de cota da VISA não dispensa a numeração. O médico pede pelo link gov.br um bloco fixo de 1.000 números; a Anvisa devolve um intervalo inicio/fim, que expandimos sem perda de zeros ou prefixos. prescription_type: "RCE" ou "RET" sempre exige número da carteira — sem saldo, 409 sncr_no_numbers. Modelos JSON próprios: prescription_rce e prescription_ret.
  • Compatibilidade depreciada — "C" / prescription_special_control: a API ainda aceita esses IDs, sem numeração, para não quebrar integrações antigas. Eles não atendem à numeração exigida para nova emissão eletrônica de RCE/RET a partir de 30/10/2026. Migre: "C" → "RCE" ou "RET"; prescription_special_control → prescription_rce ou prescription_ret. Não há conversão automática nem classificação pelo título; a escolha do tipo é de quem prescreve.
  • Notificações (NRA, NRB, NRB2, NRR, NRT): de 10 a 50 números por pedido, a partir da cota liberada pela VISA. A quantidade também é validada no servidor.
  • GET /sncr/balance mantém o objeto balance por tipo (institutional/doctor) e acrescenta sncr_type, requires_sncr, required, can_issue (com doctor_cpf), visa_quota_required, min_per_request e fixed_block. visa_quota_required: false não significa dispensa de número: só indica que a cota não depende da VISA. O saldo conta apenas números de produção; homologation.available mostra os de homologação, que nunca são reservados.
  • Modo de testes (test_mode: true) nunca reserva nem consome numeração real; modelos numerados recebem o marcador TESTE-SEM-VALOR.
  • Reservas (sncr_reservation_token) só são vinculadas a solicitações do mesmo médico (signer_cpf), do mesmo tipo e de produção — senão 409 sncr_reservation_mismatch/sncr_invalid_reservation.
  • Sem cadastro prévio de médicos: o médico é identificado pelo CPF do signatário (com CRM/UF em medical_fields) e aparece automaticamente na sua Área do Cliente. Você também pode cadastrar, listar e remover médicos antecipadamente em /doctors, respeitando o limite contratado.
  • Numeração oficial (SNCR instituído pela RDC 873/2024; receituários eletrônicos na RDC 1.000/2025): os números só vêm do SNCR da Anvisa — nas Notificações, a partir da cota que a Vigilância Sanitária liberou para o CPF/CRM do médico; em RCE/RET, por bloco de 1.000 sem cota da VISA. O médico autoriza com login gov.br — basta conta gov.br válida para obter os números (Manual API SNCR, p. 5); a assinatura final mantém exigências próprias — e os números entram na carteira dele. Faixas de talonário de papel não são aceitas.
  • A reserva usa bloqueio de linha: duas receitas simultâneas nunca recebem o mesmo número. Se a solicitação for cancelada, expirar ou falhar, o número volta ao saldo; ao concluir a assinatura, fica marcado como usado.
  • Quando não há mais números disponíveis, a API devolve sncr_no_numbers (409).
Endereço dedicado do SNCR: o Manual API SNCR v5 da Anvisa (pp. 8-9) não exige cadastro prévio de URLs, mas pede domínio nacional .br, HTTPS e o mesmo Origin no início do login e na troca do session_id. Por isso todo connect_url novo aponta para https://sncr.prescri.com.br/sncr/conectar/… e o retorno gov.br é https://sncr.prescri.com.br/sncr/retorno, independentemente do endereço usado para chamar a API. O site e a API continuam em assinatura.online; links antigos são encaminhados ao endereço dedicado antes de qualquer pedido. A troca da sessão é feita no servidor com o Origin real do navegador, conferido contra esse endereço. O fluxo ainda depende de teste real com login gov.br antes de ser tratado como pronto para produção. Fonte: Manual API SNCR v5 — Anvisa.
POST /sncr/connect-url — link gov.br para o médico buscar a numeração
{
  "doctor_cpf": "52998224725",
  "crm": "123456",          // apenas números
  "crm_uf": "SP",
  "doctor_name": "Dra. Ana Exemplo",   // opcional
  "env": "prd",             // "hmg" para homologação da Anvisa (números sem validade real)
  "sncr_type": "RCE"        // opcional: NRA, NRB, NRB2, NRR, NRT, RCE ou RET (padrão RCE)
}

# 201 Created
{ "connect_url": "https://sncr.prescri.com.br/sncr/conectar/sncr_…?tipo=RCE", "expires_at": "…", "env": "prd", "sncr_type": "RCE" }
O link abre com o tipo pré-selecionado (o médico pode trocar). RCE/RET: bloco fixo de 1.000; notificações: 10 a 50. O login gov.br acontece no navegador do médico; a troca da sessão, a verificação da assinatura do token (emissor confiável, validade e CPF do médico do link) e o pedido à Anvisa são feitos no nosso servidor, com tentativa de uso único. Links antigos, sem sncr_type, continuam funcionando e abrem em NRB. POST /sncr/import e POST /sncr/ranges seguem desativados (410).
POST /sncr/reserve — reservar antes de montar o PDF (opcional)
{ "doctor_cpf": "52998224725", "sncr_type": "NRB", "ttl_minutes": 30 }

# 201 Created
{ "sncr_type": "NRB", "sncr_number": "2610.2-23.0165500",
  "reservation_token": "rsv_…", "expires_at": "…", "source": "anvisa" }

# Depois, em /signatures/create, envie "sncr_reservation_token": "rsv_…".
# Sem token, o número é reservado automaticamente na criação.
# O PDF recebe automaticamente o quadro da Notificação de Receita
# (tipo + número, topo da 1ª página). Para desligar: "sncr_stamp": false.

DELETE /sncr/reserve?reservation_token=rsv_…
# Desistiu antes de criar o documento? Devolve o número à carteira na hora.
# 200 { "released": true, "sncr_type": "NRB", "sncr_number": "2610.2-23.0165500" }
# 404 sncr_invalid_reservation — expirada, inexistente ou já vinculada a documento

GET /sncr/balance?doctor_cpf=52998224725
# → saldo da carteira do médico por tipo (available / reserved / used)
GET /doctors — listar médicos e vagas do plano
# 200 OK
{
  "doctors": [
    { "id": "…", "doctor_cpf": "52998224725", "crm": "123456", "crm_uf": "SP",
      "full_name": "Dra. Ana Exemplo", "can_sign": true, "signatures_count": 0 }
  ],
  "total": 1, "signers_limit": 10, "unlimited": false, "remaining": 9
}
signers_limit é o limite de pessoas assinantes do seu plano (nulo significa ilimitado) — vale para quem assina, seja médico, cliente ou colaborador. cycle_used conta os CPFs distintos usados no mês corrente (inclusive removidos), cycle_renews_at indica a renovação e remaining é quanto sobra no ciclo.
POST /doctors — cadastrar ou atualizar um médico
{
  "doctor_cpf": "52998224725",
  "crm": "123456",          // apenas números (opcional)
  "crm_uf": "SP",           // opcional
  "doctor_name": "Dra. Ana Exemplo"   // opcional
}

# 201 Created (novo) ou 200 (atualização)
{ "doctor": { "id": "…", "doctor_cpf": "52998224725", "crm": "123456", "crm_uf": "SP", … }, "created": true }

# 403 quando o limite contratado foi atingido
{ "error": "Limite de 10 pessoas assinantes do ciclo atingido…", "code": "signers_limit_reached" }
Pré-cadastro opcional: os médicos continuam sendo criados automaticamente pelo CPF na primeira assinatura ou no primeiro link gov.br. A vaga conta qualquer pessoa que assina com certificado (Bird ID, VIDaaS ou A1) pela sua conta — cada CPF diferente usado no mês (cadastro, assinatura qualificada ou sessão em nuvem) ocupa uma vaga do ciclo; acima do limite, novas pessoas são recusadas com signers_limit_reached. A assinatura simples não consome vagas: é cobrada por documento assinado.
POST /billing/upgrade — aumentar vagas com o cartão salvo
# Cotação (não cobra)
{ "signers": 15, "preview": true }

# Cobrança — exige o cabeçalho Idempotency-Key
Idempotency-Key: upg-2026-10-cliente-42
{ "signers": 15 }

# 200 OK
{ "id": "…", "status": "paid", "from_signers": 10, "to_signers": 15,
  "amount_cents": 7185, "days_remaining": 15, "cycle_end": "2026-11-05" }

# 402 card_required | payment_declined — plano anterior mantido
# 409 downgrade_not_allowed | subscription_not_active | no_change

GET /billing/upgrade  # → vagas atuais, próxima renovação e cartão (bandeira/final)
O cartão é cadastrado apenas na Área do Cliente; a API nunca recebe dados de cartão. A cobrança é proporcional aos dias restantes do ciclo e as novas vagas valem na hora. Redução imediata não é permitida: agende-a com /billing/downgrade. As vagas já usadas no mês continuam contando. Repetir a mesma Idempotency-Key não gera nova cobrança.
Reduzir vagas ou cancelar — sempre no fim do ciclo
# Agendar redução para a renovação
POST /billing/downgrade
{ "signers": 5 }
# 200 → { "status": "scheduled", "signers": 10, "scheduled_signers": 5, "effective_at": "2026-11-05" }

# Agendar cancelamento (próxima cobrança não é emitida)
POST /billing/schedule
{ "action": "cancel" }

# Consultar ou desfazer o agendamento
GET    /billing/schedule
DELETE /billing/schedule

# 409 subscription_not_active — fatura em aberto: regularize antes de alterar
# 409 not_a_downgrade | cancel_scheduled
Sem estorno do ciclo em curso: as vagas contratadas seguem ativas até a renovação. A próxima fatura já sai com as vagas reduzidas; no cancelamento, nenhuma nova cobrança é emitida e as vagas são encerradas na data de renovação. Um upgrade feito depois substitui a redução agendada. Com fatura em atraso, nenhuma alteração de vagas é aceita.
DELETE /doctors — remover um médico da sua conta
{ "doctor_cpf": "52998224725" }

# 200 OK
{ "ok": true, "removed_cpf": "52998224725", "note": "Os números SNCR já recebidos para este CPF permanecem na carteira da sua conta…" }

# 404 quando o CPF não pertence à sua conta
{ "error": "Médico não encontrado nesta conta.", "code": "doctor_not_found" }
Remover não libera a vaga no mesmo mês: ela volta a ficar livre só no próximo ciclo (horário de Brasília). Os números SNCR desse CPF permanecem na carteira da sua conta e voltam a ficar disponíveis se ele for cadastrado novamente.
POST /signatures/create — receita B
{
  "provider": "birdid",
  "document_name": "Notificação de Receita B",
  "signer": { "cpf": "52998224725", "full_name": "Dra. Ana Exemplo" },
  "medical_fields": { "crm": "123456", "crm_uf": "SP", "specialty": "Psiquiatria" },
  "prescription_type": "B",
  "document": { "title": "Notificação de Receita B", "items": [ … ] }
}

# 201 Created  →  "sncr_number" vem da carteira SNCR do médico (CPF do signatário)

O carimbo traz os dados do profissional e os metadados de assinatura no padrão ICP-Brasil. A numeração é solicitada ao SNCR no navegador do médico, após o login gov.br; a API guarda, reserva e consome esses números. O registro de uso pela farmácia continua sendo feito no SNCR. Incluir CRM não verifica o registro profissional. A conformidade final de cada documento de saúde com as normas do CFM, do ITI e da Anvisa é responsabilidade de quem emite.

A1 direto e sessão A1

Há dois jeitos de usar um certificado A1 pela API. Eles não são o mesmo fluxo da Prescri: lá o titular sempre confirma pela própria tela; aqui o A1 direto envia o arquivo no corpo e assina na hora, e a sessão A1 (opcional) usa uma confirmação do titular em authorize_url.

A1 direto (síncrono)

  • Formato original: certificate: { pfx_base64, password }. Também aceitos: a1_certificate_base64 ou a1_pfx_base64 + a1_password. Enviar os dois formatos (ou aliases diferentes) retorna 400 certificate_conflict.
  • Antes de gravar qualquer coisa verificamos base64, tamanho (até 100 KB), senha, se a chave privada corresponde ao certificado, validade e CPF. Falhas nessa fase não criam solicitação, PDF nem reserva SNCR: invalid_certificate, invalid_certificate_password, certificate_key_mismatch, certificate_expired, certificate_not_yet_valid, certificate_cpf_mismatch, certificate_too_large.
  • Se algo falhar depois de criada a solicitação, o erro traz signature_id e status: "failed"; a reserva SNCR volta ao saldo. Um documento já assinado nunca volta para pendente, mesmo que o webhook ou o envio falhem depois.
  • Lemos do certificado apenas nome, CPF e validade. A cadeia ICP-Brasil e a revogação (LCR/OCSP) não são verificadas pela API.
  • O arquivo e a senha ficam só em memória durante a chamada e não são gravados nem registrados em logs.
cURL — A1 direto
curl -X POST https://assinatura.online/api/public/v1/signatures/create \
  -H "X-API-Key: $API_KEY" -H "X-API-Secret: $API_SECRET" -H "Content-Type: application/json" \
  -H "Idempotency-Key: contrato-2431-v1" \
  -d '{
    "provider": "a1",
    "document_name": "Contrato de prestação de serviço",
    "signer": { "cpf": "52998224725" },
    "pdf_base64": "JVBERi0x...",
    "a1_pfx_base64": "MIIK...",
    "a1_password": "SENHA_DO_CERTIFICADO"
  }'

# 201 { "signature_id": "…", "status": "signed", "provider": "a1", "synchronous": true,
#       "signature_token": "…", "pdf_url": "…", "pdf_base64": "…" }

Sessão A1 (opcional, até 7 dias)

  • POST /sessions/create com provider: "a1" devolve authorize_url. O titular abre o link, envia o .pfx e a senha e confirma o consentimento. Conferimos CPF, validade e par de chaves.
  • Prazo: session_lifetime_seconds até 604800 (7 dias), limitado à validade do certificado.
  • O material fica cifrado no servidor (AES-256-GCM, chave dedicada fora do banco), preso ao cliente, ao CPF e à sessão. Nunca vai para metadata, logs ou para o navegador do parceiro.
  • Depois, POST /signatures/create com provider: "a1" e session_id assina na hora (via_session: true), sem enviar arquivo.
  • POST /sessions/revoke apaga o material na hora; ao expirar, ele é apagado pela rotina de manutenção. Sessões expiradas respondem 410 session_expired; revogadas, 410 session_revoked.
  • Se o servidor não tiver a chave de cifragem, só este recurso responde 503 a1_session_not_configured; o A1 direto, a página hospedada e as sessões Bird ID/VIDaaS continuam iguais.
Node.js — sessão A1
const h = { "X-API-Key": process.env.API_KEY, "X-API-Secret": process.env.API_SECRET, "Content-Type": "application/json" };
const s = await fetch("https://assinatura.online/api/public/v1/sessions/create", { method: "POST", headers: h,
  body: JSON.stringify({ provider: "a1", signer: { cpf: "52998224725" }, session_lifetime_seconds: 604800 }) }).then(r => r.json());
// envie s.authorize_url ao titular; depois de autorizada:
const r = await fetch("https://assinatura.online/api/public/v1/signatures/create", { method: "POST", headers: { ...h, "Idempotency-Key": "laudo-2431-sessao-01" },
  body: JSON.stringify({ provider: "a1", session_id: s.session_id, signer: { cpf: "52998224725" }, document_name: "Laudo técnico", pdf_url: "https://seusistema.com/rx.pdf" }) }).then(r => r.json());
Python — sessão A1
import os, requests
h = {"X-API-Key": os.environ["API_KEY"], "X-API-Secret": os.environ["API_SECRET"]}
s = requests.post("https://assinatura.online/api/public/v1/sessions/create", headers=h, json={"provider": "a1", "signer": {"cpf": "52998224725"}, "session_lifetime_seconds": 604800}).json()
# depois que o titular autorizar em s["authorize_url"]:
r = requests.post("https://assinatura.online/api/public/v1/signatures/create", headers={**h, "Idempotency-Key": "laudo-2431-sessao-01"},
    json={"provider": "a1", "session_id": s["session_id"], "signer": {"cpf": "52998224725"}, "document_name": "Laudo técnico", "pdf_url": "https://seusistema.com/rx.pdf"}).json()

Retentativas seguras (Idempotency-Key)

  • Opcional em POST /signatures/create (assinatura qualificada). Envie o cabeçalho Idempotency-Key (8 a 128 caracteres: letras, números, . _ : -).
  • Mesma chave + mesmo conteúdo: devolve a mesma resposta (cabeçalho Idempotent-Replayed: true), sem nova solicitação nem nova cobrança. Mesma chave + conteúdo diferente: 409 idempotency_key_reused. Ainda em andamento: 409 idempotency_in_progress com o signature_id quando já existir.
  • Depois de um timeout, repita com a mesma chave ou consulte GET /signatures/get?id= pelo signature_id. As chaves valem 24 horas e são separadas por cliente.
  • Certificados e senhas não entram na impressão digital nem no registro. O external_id continua livre e não passa a ser único.
Python — retentativa
import uuid, requests
key = f"rx-{uuid.uuid4()}"
for tentativa in range(3):
    try:
        r = requests.post(f"https://assinatura.online/api/public/v1/signatures/create", headers={"X-API-Key": API_KEY, "X-API-Secret": API_SECRET, "Idempotency-Key": key}, json=payload, timeout=30)
        if r.status_code != 409 or r.json().get("code") != "idempotency_in_progress":
            break
    except requests.Timeout:
        continue

Matriz de compatibilidade

RecursoContrato antigoNovo (aditivo)
prescription_typeA, B, B2, R, T, C, RCE, RET e NRA…NRTIguais; conflito com o modelo JSON → 422 prescription_type_conflict
Modelos JSONIDs anteriores mantidosprescription_rce, prescription_ret; catálogo com sncr_type/requires_sncr
"C" / prescription_special_controlSem numeraçãoDepreciado: ainda aceito, sem conversão silenciosa; não serve para RCE/RET eletrônica a partir de 30/10/2026 — migre para RCE/prescription_rce ou RET/prescription_ret
/sncr/connect-urldoctor_cpf, crm, crm_uf, envsncr_type opcional (padrão RCE); links antigos abrem em NRB
/sncr/balancebalance por tipo+ sncr_type, requires_sncr, required, can_issue, homologation
signer.cpfsigner.cpf, signer_cpf, doctor_cpfSem mudança
A1certificate { pfx_base64, password }, A1 hospedado+ a1_certificate_base64 / a1_pfx_base64 + a1_password; validação antes de gravar; sessão A1
Sessõesbirdid, vidaas+ a1 (opt-in)
Simples / híbridamode: "simple", qualified_signerSem mudança
Retentativa—Idempotency-Key opcional

SDKs de servidor: Node.js e Python

SDKs leves, sem dependências externas, para usar no seu servidor. Cobrem assinatura simples, híbrida e qualificada, consulta de status, download do PDF, cancelamento, reenvio, sessões e validação de webhook HMAC em tempo constante.

Nunca use a API Key e o Secret no navegador. Para abrir a assinatura dentro do seu app (iframe ou popup), use o SDK embed abaixo junto com o sign_url gerado no servidor.

SDK embed (navegador)

Para prontuários e ERPs que querem a assinatura dentro da própria tela. O SDK abre o fluxo do signatário e avisa sua página sobre cada etapa por postMessage. A aprovação no Bird ID/VIDaaS sempre abre numa janela própria, porque os provedores não permitem iframe.

index.html
<script src="https://assinatura.online/assinatura-sdk.js"></script>
<script>
  // sign_url vem da resposta de /signatures/create (chamada no seu servidor)
  AssinaturaOnline.open({
    signUrl: "https://assinatura.online/assinar/EXEMPLO",
    mode: "modal",            // "modal" (janela) ou "iframe"
    container: "#assinatura", // obrigatório no modo iframe
    onEvent(event) {
      // widget.opened | widget.ready | signature.completed | signature.failed | widget.closed
      if (event.type === "signature.completed") console.log(event.pdf_url);
    },
  });
</script>
Confirme sempre o resultado pelo webhook ou por /signatures/get — eventos do navegador servem só para a interface.

Sem SDK: iframe, popup ou nova aba

A tela de assinatura detecta sozinha quando está dentro de um iframe ou de um popup e mostra uma versão limpa, sem menu nem rodapé — só a marca, o progresso e o resultado. Você não precisa mudar nenhum parâmetro na chamada da API.

  • • iframe ou modal: use o sign_url direto no src. Modo limpo automático.
  • • Popup (window.open): modo limpo automático.
  • • Nova aba ou redirecionamento: acrescente ?embed=1 ao sign_url para forçar o modo limpo.
iframe.html
<!-- Modo limpo automático dentro do iframe -->
<iframe
  src="https://assinatura.online/assinar/EXEMPLO"
  style="width:100%;height:720px;border:0"
  allow="clipboard-write"
></iframe>

<!-- Nova aba: force o modo limpo -->
<a href="https://assinatura.online/assinar/EXEMPLO?embed=1" target="_blank" rel="noopener">
  Assinar documento
</a>
Na qualificada (Bird ID/VIDaaS), a aprovação abre numa janela própria do provedor; a tela embutida acompanha o andamento em tempo real e fecha o retorno sozinha.

Modo de testes (opcional, por envio)

Não existe ambiente separado: as mesmas credenciais assinam documentos reais desde o primeiro dia. Se quiser validar a integração sem certificado real, envie "test_mode": true em /signatures/create. A solicitação é concluída na hora, o webhook signature.completed chega com "test_mode": true e o PDF recebe a marca d’água “DOCUMENTO DE TESTE — SEM VALIDADE JURÍDICA”.

  • Não usa certificado ICP-Brasil e não conta no limite mensal.
  • provider é opcional nesse modo.
  • Nunca use documentos de teste como documentos válidos.
  • Não use test_mode: true com mode: "simple". Teste a simples com documento fictício e destinatário autorizado; a confirmação por código é real.

Erros

Erros vêm como { "error": "mensagem", "code": "codigo" }.

Códigos de erro
HTTPcodeSignificado
400invalid_document / invalid_pdf_sourceDocumento JSON ou PDF inválido
400invalid_signer_cpfCPF ausente ou inválido (opcional na simples)
400invalid_phoneWhatsApp ausente ou inválido; use país + DDD + número
400simple_test_not_supportedtest_mode: true não é aceito na simples
400invalid_otpCódigo incorreto, expirado ou solicitação indisponível na confirmação
429rate_limitedAguarde o intervalo de reenvio ou limite de códigos atingido
503whatsapp_not_configured / whatsapp_delivery_failed / email_delivery_failedCanal indisponível ou falha ao enviar código
401missing_credentials / invalid_credentialsRevise X-API-Key e X-API-Secret
402trial_expired / monthly_limit_reachedAvaliação encerrada ou limite do mês
403medical_fields_disabledMódulo de saúde não habilitado
404not_foundSolicitação não encontrada
409already_signed / not_signed / already_dispensedEstado não permite a operação
409sncr_no_numbersSem numeração SNCR na carteira do médico
410sncr_manual_import_disabledImportação manual de faixas desativada
410expiredPrazo de assinatura encerrado
413pdf_too_largePDF acima de 15 MB
500internal_errorTente novamente