Pular para o conteúdo principal

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.

Sandbox

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.

De onde vem o que esta aqui

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​

Qual simulate serve ao fluxo direto

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
}
Campos que chegam null nesta rota

created_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 rota

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

Qual campo de estado ler

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"
}
Guarde o hmac_secret agora

Ele 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:

  1. Verifique sobre os bytes crus. Body parseado e re-serializado produz assinatura diferente.
  2. Deduplique por event_id. A mesma entrega pode chegar mais de uma vez (retry).
  3. 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​

AssuntoOnde
Panorama, entidades e fluxo completoIntegration Overview
Fluxo por estoque (pre-cadastro)Getting Started
Todos os codigos de erroCodigos de Erro
Limites de requisicao e Retry-AfterRate Limits
O que pode mudar sem avisoVersionamento
Checklist antes de ir para ProducaoGo-live