Integracao em 6 curls
O caminho mais curto entre "recebi minhas credenciais" e "meu servidor processa um webhook verificado": 6 comandos, na ordem, copiaveis. Cada um traz a resposta que a API devolve.
Este guia usa a operacao direta (POST /v1/operations/direct) — tudo numa chamada, sem
cadastrar o recebivel antes. Se voce prefere manter um estoque de recebiveis pre-cadastrados e
antecipar depois, o caminho e o Getting Started.
Todos os comandos apontam para o ambiente de homologacao, com credenciais de Sandbox
(zk_sbx_* ou zk_test_*, legado) e sem
transacoes reais. Use zk_live_* somente apos o go-live.
Endpoints, campos, headers e corpos de resposta desta pagina sao os do
openapi.json, a fonte canonica do contrato. Os valores sao ilustrativos e
fecham a conta entre si (NFV 10.000,00 · 3,5% a.m. · 70 dias + 2 de floating = 72 dias de prazo
=> desagio 840,00, liquido 9.160,00). Os id que voce receber serao outros.
Vocabulario: os exemplos desta pagina usam os nomes canonicos (*_future_value /
*_present_value). Cada valor tem tambem um nome deprecado
(net_face_value, requested_advance_value, opr_liquid_value, ...) que segue
aceito nos requests e devolvido em toda resposta, com o mesmo numero, em toda a
serie 1.x — e sai na 2.0 (politica). Os blocos de resposta
abaixo mostram so o canonico, para caber; nenhum campo saiu do contrato. Os examples
renderizados na Referencia Tecnica ainda estao no nome deprecado — mesmos
numeros, mesmos campos, o outro nome do par. De-para completo em
Valores do Recebivel.
Preparacao
export BASE="https://receivables-api-sandbox.zemocapital.com"
export CLIENT_ID="zk_test_..."
read -rsp "Client secret de Sandbox: " CLIENT_SECRET; echo
export CLIENT_SECRET
1. Autenticar
Troque as credenciais de API por um JWT RS256 de curta duracao (TTL 15 minutos), pelo fluxo Client Credentials (OAuth2).
TOKEN=$(jq -n '{client_id: env.CLIENT_ID, client_secret: env.CLIENT_SECRET}' \
| curl -sS -X POST "$BASE/v1/auth/token" \
-H "Content-Type: application/json" --data-binary @- \
| jq -r .access_token)
unset CLIENT_SECRET
export TOKEN
Resposta:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.<payload>.<assinatura>",
"token_type": "bearer",
"expires_in": 900,
"scopes": ["stock:write", "simulation:create", "operation:create", "operation:read", "webhook:write"]
}
Credencial errada devolve 401 com detail string:
{ "detail": "invalid_client_credentials" }
Os scopes concedidos limitam o que o token pode fazer — a lista canonica esta em Scopes. Detalhes do fluxo em Autenticacao.
2. Simular
POST /v1/operations/direct nao tem um simulate proprio. O simulate deste fluxo e
POST /v1/simulate: ele recebe a mesma lista receivables[] (com payer_name /
payer_document) que o create, e nao cria nada. O outro simulate da API,
POST /v1/stock/simulate-anticipation, e do fluxo de estoque e recebe stock_item_ids —
nao serve aqui.
curl -sS -X POST "$BASE/v1/simulate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"assignor_document": "12345678000195",
"receivables": [
{
"external_id": "NF-2026-001",
"payer_name": "Sacado Sandbox SA",
"payer_document": "98765432000198",
"gross_future_value": 10000.00,
"gross_future_value_deductions": 0.00,
"net_future_value": 10000.00,
"requested_net_future_value": 10000.00,
"due_date": "2026-10-07"
}
],
"fees": { "monthly_rate_pct": 3.5, "floating_days": 2 }
}'
Resposta (nada e criado):
{
"id": "simulation",
"status": "SIMULATED",
"opr_gross_future_value": "10000.00",
"opr_net_future_value": "10000.00",
"opr_present_value_discount": "840.00",
"opr_net_present_liquid_value": "9160.00",
"average_days_in_advance": 70,
"weighted_avg_days": 70,
"fee_hierarchy": "receivable > operation > assignor > payer > policy",
"assignor_defaults_used": false,
"payer_defaults_used": false,
"receivables": [
{
"external_id": "NF-2026-001",
"payer_name": "Sacado Sandbox SA",
"payer_document": "98765432000198",
"requested_net_future_value": "10000.00",
"net_future_value": "10000.00",
"gross_future_value": "10000.00",
"due_date": "2026-10-07",
"days_advanced": 72,
"monthly_rate_pct": "3.5",
"floating_days": 2,
"present_value_discount": "840.00",
"net_present_liquid_value": "9160.00",
"fee_source": "operation"
}
]
}
net_future_value e o NFV (Net Future Value) — o valor de face futuro ja liquido dos descontos
aplicados ao lastro; o desagio incide sobre ele. Usar gross_future_value exige
gross_future_value_deductions no mesmo payload (aqui 0, porque bruto e NFV coincidem): a
diferenca entre os dois tem que ser declarada, nunca inferida — sem ela, 422
DEDUCTIONS_REQUIRED_WITH_GROSS. Omitir fees faz a API aplicar a hierarquia de taxas
configurada para o seu originador. Veja Valores do Recebivel e
Taxas.
3. Criar a operacao (com Idempotency-Key)
IDEM_KEY=$(uuidgen)
OPERATION_ID=$(curl -sS -X POST "$BASE/v1/operations/direct" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM_KEY" \
-d '{
"name": "Cedente Sandbox SA",
"type": "J",
"cnpj": "12345678000195",
"email": "financeiro@cedente.com.br",
"bank": { "use_document_pix": true },
"receivables": [
{
"external_id": "NF-2026-001",
"payer_name": "Sacado Sandbox SA",
"payer_document": "98765432000198",
"gross_future_value": 10000.00,
"gross_future_value_deductions": 0.00,
"net_future_value": 10000.00,
"requested_net_future_value": 10000.00,
"due_date": "2026-10-07",
"backing_type": "NFE"
}
],
"fees": { "monthly_rate_pct": 3.5, "floating_days": 2 }
}' | jq -r .id)
export OPERATION_ID
Resposta (201):
{
"id": "01994c1e-9b20-7000-9000-0000000000b2",
"display_number": "OP-A1B2C3D4-E5F6G7H8",
"status": "WAITING_APPROVAL",
"lifecycle_status": null,
"opr_gross_future_value": "10000.00",
"opr_net_future_value": "10000.00",
"opr_present_value_discount": "840.00",
"opr_net_present_liquid_value": "9160.00",
"opr_net_liquid_value": "9160.00",
"other_debt_discounts": "0.00",
"average_days_in_advance": 70,
"floating_days": 2,
"product_id": null,
"needs_backoffice_approval": true,
"created_at": null,
"titles": [
{
"id": "01994c1e-c000-7000-9000-0000000000a7",
"external_id": "NF-2026-001",
"backing_type": "NFE",
"status": "WAITING_PAYMENT",
"requested_net_future_value": "10000.00",
"net_present_liquid_value": "9160.00",
"present_value_discount": "840.00",
"due_date": "2026-10-07",
"qty_days_advanced": 72,
"fee_source": "operation",
"monthly_rate_pct": "3.5",
"discount_pct": "0",
"fixed_discount_brl": "0",
"is_partial": false
}
],
"fee_hierarchy": "receivable > operation > assignor > payer > policy",
"has_partial_anticipations": false,
"contract_details": { "type": "FULL_CONTRACT", "signature_date": null, "signer_token": null },
"signers_count": 1
}
null nesta rotacreated_at e lifecycle_status fazem parte do schema da resposta mas esta rota nao os
preenche — chegam sempre como null. Para a data de criacao e o estado autoritativo, leia
GET /v1/operations/{id} (passo 4). O fluxo de estoque
(POST /v1/stock/request-anticipation) e o oposto — la created_at e lifecycle_status vem
preenchidos e status vem null.
Para saber se a operacao foi para a fila do Backoffice, use needs_backoffice_approval (as duas
rotas o preenchem) ou o estado (lifecycle_status ?? status) — os dois dizem a mesma coisa. Nao
assuma WAITING_APPROVAL: quando o total solicitado cabe no limite de auto-aprovacao da policy, a
operacao ja nasce APPROVED_DIRECT com needs_backoffice_approval: false — sem passar pelo
Backoffice e sem esperar aprovacao humana.
Idempotency-Key e obrigatoria nesta rotaSem o header, a requisicao e recusada antes de qualquer efeito:
{
"detail": {
"code": "idempotency_key_required",
"message": "Idempotency-Key header is required for financial operations"
}
}
A chave deve ter 16 a 80 caracteres (UUID serve). Reenviar a mesma chave com o mesmo corpo
devolve a resposta guardada da primeira execucao, com o header Idempotency-Replayed: true, sem
executar de novo — e assim que se faz retry seguro em fluxo de dinheiro. A mesma chave com
corpo diferente responde 409 idempotency_key_reused_with_different_body. Regras completas em
Idempotencia.
Esta rota devolve o alias legado status; POST /v1/stock/request-anticipation e
GET /v1/operations/{id} devolvem lifecycle_status, o campo autoritativo. Cliente que atende
os dois fluxos deve ler lifecycle_status ?? status. Ver
Lifecycle.
Para antecipar apenas uma fracao do NFV (com o restante voltando ao estoque), veja antecipacao parcial — e um recurso exclusivo desta rota.
4. Consultar a operacao
curl -sS "$BASE/v1/operations/$OPERATION_ID" \
-H "Authorization: Bearer $TOKEN"
Resposta:
{
"id": "01994c1e-9b20-7000-9000-0000000000b2",
"display_number": "OP-A1B2C3D4-E5F6G7H8",
"lifecycle_status": "WAITING_APPROVAL",
"opr_gross_future_value": "10000.00",
"opr_present_value_discount": "840.00",
"opr_net_present_liquid_value": "9160.00",
"created_at": "2026-07-29T14:30:00Z",
"updated_at": "2026-07-29T14:30:00Z"
}
Operacao inexistente (ou de outro originador) devolve 404 operation_not_found — a API e
multi-tenant e filtra tudo pelo originator_id do token. Polling e util para depurar, mas o
caminho de producao e o webhook do passo 5.
5. Registrar o webhook
curl -sS -X POST "$BASE/v1/webhooks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://api.seu-dominio.com.br/zemo/webhooks",
"events": ["operation.approved", "operation.paid", "title.paid"],
"description": "Integracao principal"
}'
Resposta (201):
{
"id": "01994c1e-3c00-7000-9000-000000000003",
"url": "https://api.seu-dominio.com.br/zemo/webhooks",
"event_types": ["operation.approved", "operation.paid", "title.paid"],
"hmac_secret": "whsec_<guarde-este-valor-ele-nao-e-exibido-de-novo>",
"created_at": "2026-07-29T14:30:00Z"
}
hmac_secret agoraEle so aparece nesta resposta e e o segredo que valida toda entrega. Grave-o no seu cofre de segredos antes de seguir.
A URL passa por uma guarda anti-SSRF: precisa ser HTTPS, host publico resolvivel. URL recusada
volta 422 com um codigo da familia url_* ou ip_*:
{
"detail": {
"code": "url_scheme_not_https",
"message": "webhook url rejected: url_scheme_not_https"
}
}
A lista canonica de eventos esta em Eventos.
6. Conferir as entregas
WEBHOOK_ID="01994c1e-3c00-7000-9000-000000000003"
curl -sS "$BASE/v1/webhooks/$WEBHOOK_ID/deliveries?limit=10" \
-H "Authorization: Bearer $TOKEN"
Resposta:
{
"items": [
{
"id": "01994c1f-0000-7000-9000-0000000000c3",
"event_type": "operation.approved",
"url": "https://api.seu-dominio.com.br/zemo/webhooks",
"response_status_code": 200,
"duration_ms": 142,
"attempt_number": 1,
"delivered_successfully": true,
"next_retry_at": null,
"created_at": "2026-07-29T14:31:05Z"
}
]
}
E aqui que voce depura o handler: response_status_code, attempt_number e next_retry_at
mostram exatamente o que o seu endpoint respondeu.
Receber o webhook: verificar o HMAC
O corpo entregue e sempre o envelope:
{
"event_type": "operation.approved",
"event_id": "01994c1f-1111-7000-9000-0000000000d4",
"occurred_at": "2026-07-29T14:31:00Z",
"data": {
"operation_id": "01994c1e-9b20-7000-9000-0000000000b2",
"display_number": "OP-A1B2C3D4-E5F6G7H8",
"lifecycle_status": "APPROVED_DIRECT",
"approved_at": "2026-07-29T14:31:00Z"
}
}
O header X-Zemo-Signature traz o HMAC-SHA256 do body cru (os bytes exatos recebidos —
nunca o JSON re-serializado), em hexadecimal, com o hmac_secret do passo 5 como chave.
Python
import hashlib
import hmac
def verify(raw_body: bytes, secret: str, signature: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# FastAPI
@app.post("/zemo/webhooks")
async def receive(request: Request):
raw = await request.body() # bytes CRUS, antes de qualquer parse
if not verify(raw, WEBHOOK_SECRET, request.headers.get("X-Zemo-Signature", "")):
return Response(status_code=401)
event = json.loads(raw)
enqueue(event) # processe fora do request
return Response(status_code=200) # responda em < 5 s
Node.js
const crypto = require('crypto');
function verify(rawBody, secret, signature) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(signature || '', 'utf8');
// timingSafeEqual exige o mesmo tamanho — compare o comprimento antes.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: express.raw() preserva os bytes crus.
app.post('/zemo/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body, WEBHOOK_SECRET, req.get('X-Zemo-Signature'))) {
return res.sendStatus(401);
}
enqueue(JSON.parse(req.body.toString('utf8')));
res.sendStatus(200);
});
Tres regras que decidem se a sua integracao aguenta producao:
- Verifique sobre os bytes crus. Body parseado e re-serializado produz assinatura diferente.
- Deduplique por
event_id. A mesma entrega pode chegar mais de uma vez (retry). - Responda 2xx rapido — o timeout de entrega e de 10 s; processe de forma assincrona.
Headers, boas praticas e a tabela completa em Seguranca de Webhooks.
Proximos passos
| Assunto | Onde |
|---|---|
| Panorama, entidades e fluxo completo | Integration Overview |
| Fluxo por estoque (pre-cadastro) | Getting Started |
| Todos os codigos de erro | Codigos de Erro |
Limites de requisicao e Retry-After | Rate Limits |
| O que pode mudar sem aviso | Versionamento |
| Checklist antes de ir para Producao | Go-live |