Assinatura embedded
Toda operacao criada pela API dispara o contrato sozinha assim que e aprovada, e o provedor de assinatura notifica os signatarios por e-mail. Voce nao precisa fazer nada para isso acontecer.
A assinatura embedded e o modo alternativo: em vez de deixar o e-mail automatico sair, voce recebe a URL de assinatura de cada signatario e a apresenta na sua propria interface — o cedente assina sem sair do seu produto.
O provedor de assinatura eletronica e a ZapSign. A sign_url aponta para o
dominio dela, e e la que o signatario assina — inclusive no modo embedded (voce
apresenta o link; a assinatura acontece na pagina do provedor). O provedor pode
mudar sem quebra de contrato: o que a API promete e o campo sign_url, nao o
dominio para onde ele aponta.
Em modo embedded nenhuma notificacao sai: nem o e-mail inicial, nem os
lembretes. Se a sign_url nunca for apresentada, ninguem assina — e nao ha
watchdog nem fallback que perceba isso por voce. A operacao fica parada em
CONTRACT_SENT indefinidamente. Use o modo embedded so onde voce controla a
tela em que o signatario vai clicar.
Fluxo completo
| Passo | O que voce faz | O que a API faz |
|---|---|---|
| 1 | Cria a operacao com embedded_signature: true | Registra a preferencia na operacao |
| 2 | Aguarda o webhook operation.approved | Ao aprovar, dispara o contrato em silencio |
| 2b | Aguarda o despacho (ver abaixo) | O despacho nao e instantaneo |
| 3 | GET /v1/operations/{id}/contract/signers | Devolve sign_url e status de cada signatario |
| 4 | Apresenta a sign_url ao signatario | — |
| 5 | Recebe o webhook operation.contract_signed | Avanca a operacao para o pagamento |
O gatilho do passo 2 e o webhook operation.approved. Nao existe hoje um
evento operation.contract_sent: nao espere por ele — a forma de saber que
o contrato saiu e o passo 3 responder 200.
Passo 2b: o despacho NAO e instantaneo
operation.approved diz que a operacao foi aprovada, nao que o contrato ja
existe no provedor. Entre os dois ha o disparo, que pode levar ate ~2 minutos
(operacoes auto-aprovadas sao despachadas por uma varredura periodica) mais o
tempo das chamadas ao provedor.
Nessa janela o passo 3 responde com estados TRANSITORIOS, que voce deve tratar como "ainda nao", e nao como erro:
| Resposta | Significa | O que fazer |
|---|---|---|
404 contract_not_found | o despacho ainda nao comecou | re-tentar |
409 contract_not_dispatched | comecou, o documento ainda nao existe | re-tentar |
200 com as sign_url | pronto | seguir para o passo 4 |
Faca poll com backoff — por exemplo a cada 5 s, dobrando ate 30 s, por ate 5 minutos — e so trate como falha se estourar esse teto. Nao faca poll apertado: as rotas contam no seu rate limit.
async function aguardarSignUrls(operationId: string, tetoMs = 5 * 60_000) {
let esperaMs = 5_000;
const limite = Date.now() + tetoMs;
while (Date.now() < limite) {
try {
return await zemo.operations.contractSigners(operationId);
} catch (err) {
// Classifique pelo CODE, nao pelo status: nesta rota `contract_not_found`
// e `contract_not_dispatched` sao TRANSITORIOS (o despacho esta em curso),
// mas 404/409 de outros codigos nao sao — re-tentar cegamente esconderia
// um erro real num loop de 5 minutos.
const transitorio =
(err instanceof NotFoundError && err.code === "contract_not_found") ||
(err instanceof ConflictError && err.code === "contract_not_dispatched");
if (!transitorio) throw err;
await new Promise((r) => setTimeout(r, esperaMs));
esperaMs = Math.min(esperaMs * 2, 30_000);
}
}
throw new Error("contrato nao foi despachado dentro do teto");
}
A flag vive na operacao, e nao na chamada de disparo, por um motivo pratico: o disparo acontece sozinho na aprovacao, sem intervencao sua — uma flag so no disparo chegaria tarde demais.
1. Crie a operacao pedindo o modo embedded
Nos dois caminhos de criacao:
curl -X POST https://receivables-api-sandbox.zemocapital.com/v1/operations/direct \
-H "Authorization: Bearer $JWT" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Empresa ABC Ltda",
"type": "J",
"cnpj": "12345678000195",
"email": "financeiro@abc.com.br",
"bank": { "use_document_pix": true },
"embedded_signature": true,
"receivables": [ ... ]
}'
# ... ou via estoque
curl -X POST .../v1/stock/request-anticipation \
-d '{ "stock_item_ids": ["..."], "requested_net_future_value": 10000.00,
"bank": { "use_document_pix": true }, "embedded_signature": true }'
Omitir o campo (ou mandar false) mantem o comportamento padrao: e-mail
automatico ao signatario.
2. Leia as URLs de assinatura
curl https://receivables-api-sandbox.zemocapital.com/v1/operations/$OPERATION_ID/contract/signers \
-H "Authorization: Bearer $JWT"
{
"operation_id": "0199a1b2-c3d4-7000-8000-0123456789ab",
"contract_id": "0199a1b2-c3d4-7000-8000-0123456789cd",
"doc_token": "d47e8a0c-...",
"document_status": "pending",
"signers": [
{
"name": "Maria Souza",
"status": "new",
"signed": false,
"sign_url": "https://app.zapsign.com.br/verificar/..."
},
{
"name": "Joao Lima",
"status": "signed",
"signed": true,
"sign_url": null
}
]
}
Este endpoint le ao vivo no provedor a cada chamada — e a fonte a usar sempre que voce precisar da URL e nao a tiver em maos.
status e document_status sao texto do provedorOs dois campos sao pass-through: a API os repassa como a ZapSign os devolve, sem traduzir nem normalizar. Podem ganhar valores novos ou mudar de grafia sem aviso e sem versao nova — nao construa logica em cima deles.
Para decidir qualquer coisa em codigo, use o booleano signed, que e nosso
e tem semantica estavel: true = este signatario ja assinou. status serve
para exibir ao operador, com um default para o que voce nao conhecer.
| Situacao | Resposta |
|---|---|
| Operacao sem contrato ativo (ou de outro originador) | 404 contract_not_found |
| Contrato registrado mas ainda nao despachado | 409 contract_not_dispatched |
| Provedor de assinatura indisponivel | 502 zapsign_unavailable |
Repare que nao existe um 200 com signers: []: lista vazia seria
indistinguivel de "documento sem signatarios", e voce ficaria esperando uma URL
que nunca viria.
3. Acompanhe a assinatura
Registre um webhook para operation.contract_signed — ele
chega quando todos os signatarios assinaram. O status por signatario do
GET .../contract/signers serve para mostrar o progresso na sua tela.
Disparo manual (opcional)
Se precisar disparar o contrato voce mesmo — para re-tentar apos uma falha, ou para ligar o modo embedded numa operacao criada sem a flag:
curl -X POST .../v1/operations/$OPERATION_ID/contract/send-for-signature \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{ "embedded": true }'
O corpo e opcional. embedded sobrepoe a preferencia da operacao apenas neste
disparo; omitido, vale o que a operacao carrega. Sucesso responde 201
(o despacho criou o contrato).
| Resposta | Significa | O que fazer |
|---|---|---|
201 | contrato despachado agora | usar as sign_url do corpo |
404 operation_not_found | a operacao nao existe ou e de outro originador | conferir o id |
409 contract_already_exists | ja despachado (em geral pelo envio automatico) | nao e falha — ler as URLs em GET .../contract/signers |
409 operation_not_in_signable_state | o estado da operacao nao permite disparar | ver abaixo |
502 zapsign_* | o provedor falhou; nenhum documento parcial ficou vivo | re-tentar com backoff |
Voce so dispara em PRE_APPROVED, APPROVED_DIRECT ou
CONTRACT_ERROR (o re-disparo apos falha). Em WAITING_APPROVAL a chamada
responde 409 operation_not_in_signable_state — de proposito: um contrato de
cessao assinado antes da decisao de credito seria um documento juridico de uma
operacao que ninguem aprovou. Espere o operation.approved.
Idempotency-Key e OPCIONAL aquiEsta rota nao exige o header — mas o honra se voce enviar, com a mesma
deduplicacao das rotas financeiras. Se usar, leia o aviso de replay no fim desta
pagina: a resposta guardada vem com sign_url: null.
A resposta traz os mesmos signatarios, ja com a sign_url do despacho:
{
"operation_id": "0199a1b2-...",
"contract_id": "0199a1b2-...",
"doc_token": "d47e8a0c-...",
"lifecycle_status": "CONTRACT_SENT",
"is_first_contract": true,
"titles_count": 3,
"signers_count": 2,
"signers_notified": null,
"embedded": true,
"signers": [
{ "name": "Maria Souza", "status": "new", "sign_url": "https://app.zapsign.com.br/..." }
]
}
signers_notified vem null no modo embedded — nao houve notificacao a
enviar. doc_token e contract_id sao identificadores opacos para
correlacao e suporte (cite-os ao abrir um chamado); nao ha rota publica que os
receba como parametro, e o doc_token nao substitui a sign_url — ele nao
abre a tela de assinatura. Se o contrato ja tiver sido despachado, a chamada responde
409 contract_already_exists: use o GET .../contract/signers para obter as
URLs do despacho vigente.
409 desta rota pedem acoes OPOSTAScontract_already_exists significa "ja esta feito" — siga para o
GET .../contract/signers. operation_not_in_signable_state significa "ainda
nao pode" — aguarde a aprovacao (webhook operation.approved). Tratar os
dois como o mesmo 409 leva a um dos dois erros: ou voce busca URLs que nao
existem, ou desiste de uma operacao que so precisava de tempo. Ramifique pelo
code, nunca pelo status.
sign_url e uma credencial
Quem tem a URL assina o documento. Trate-a como trata um segredo:
- nunca registre em log, nunca guarde em claro no seu banco;
- entregue so ao proprio signatario, por canal autenticado;
- ela e de uso corrente: se perder, nao tente reconstrui-la — peca de novo
em
GET .../contract/signers.
Idempotency-Key devolve sign_url: nullA resposta guardada do disparo tem a sign_url apagada de proposito — ela
nao pode ficar em repouso no cache de idempotencia. O reenvio da mesma chave
devolve o mesmo corpo com "sign_url": null (a chave continua la, o valor
nao). Isso nao e erro: leia as URLs em
GET /v1/operations/{id}/contract/signers.
Scopes
As duas rotas tem scopes diferentes — veja Scopes.
Nenhum dos dois vem junto de operation:create: uma credencial que cria
operacoes nao lida com contrato por tabela.
| Rota | Scope | Quem precisa |
|---|---|---|
GET .../contract/signers | contract:read | todo mundo que usa o modo embedded — e a rota do fluxo tipico |
POST .../contract/send-for-signature | contract:send | so quem cai nos casos de borda do disparo manual |
O fluxo tipico e embedded_signature: true na criacao + GET .../contract/signers para ler as URLs: o disparo e automatico na aprovacao, e
esse caminho inteiro pede apenas contract:read.
contract:send e um opt-in de borda: peca-o so na credencial que realmente
vai disparar contrato (re-disparo apos CONTRACT_ERROR, operacoes importadas do
fluxo antigo, sobrescrever o modo embedded da operacao, ou nao querer esperar
a varredura). Quem tem contract:read nao consegue disparar, e quem tem
contract:send nao herda a leitura.
Primeira cessao x cessoes seguintes (aditivo)
O modo embedded vale para as duas. A diferenca esta na exigencia do provedor, nao na sua integracao:
| Primeira cessao do cedente | Cessoes seguintes (aditivo) | |
|---|---|---|
| Documento | contrato de cessao completo | termo aditivo |
| Autenticacao do signatario | avancada: selfie + foto do documento de identidade | assinatura em tela |
| Sua integracao | identica | identica |
O campo is_first_contract na resposta do disparo diz em qual dos dois voce
esta. Na primeira cessao o signatario passa por mais etapas dentro da pagina
do provedor — reserve espaco na sua tela e nao trate a demora extra como
falha.