Pular para o conteúdo principal

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.

Quem assina o documento

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.

Voce assume a responsabilidade de levar o signatario ate a assinatura

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​

PassoO que voce fazO que a API faz
1Cria a operacao com embedded_signature: trueRegistra a preferencia na operacao
2Aguarda o webhook operation.approvedAo aprovar, dispara o contrato em silencio
2bAguarda o despacho (ver abaixo)O despacho nao e instantaneo
3GET /v1/operations/{id}/contract/signersDevolve sign_url e status de cada signatario
4Apresenta a sign_url ao signatario—
5Recebe o webhook operation.contract_signedAvanca 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:

RespostaSignificaO que fazer
404 contract_not_foundo despacho ainda nao comecoure-tentar
409 contract_not_dispatchedcomecou, o documento ainda nao existere-tentar
200 com as sign_urlprontoseguir 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 provedor

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

SituacaoResposta
Operacao sem contrato ativo (ou de outro originador)404 contract_not_found
Contrato registrado mas ainda nao despachado409 contract_not_dispatched
Provedor de assinatura indisponivel502 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).

RespostaSignificaO que fazer
201contrato despachado agorausar as sign_url do corpo
404 operation_not_founda operacao nao existe ou e de outro originadorconferir o id
409 contract_already_existsja despachado (em geral pelo envio automatico)nao e falha — ler as URLs em GET .../contract/signers
409 operation_not_in_signable_stateo estado da operacao nao permite dispararver abaixo
502 zapsign_*o provedor falhou; nenhum documento parcial ficou vivore-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 aqui

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

Os dois 409 desta rota pedem acoes OPOSTAS

contract_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.
Retry com a mesma Idempotency-Key devolve sign_url: null

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

RotaScopeQuem precisa
GET .../contract/signerscontract:readtodo mundo que usa o modo embedded — e a rota do fluxo tipico
POST .../contract/send-for-signaturecontract:sendso 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 cedenteCessoes seguintes (aditivo)
Documentocontrato de cessao completotermo aditivo
Autenticacao do signatarioavancada: selfie + foto do documento de identidadeassinatura em tela
Sua integracaoidenticaidentica

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.