Para IAs & LLMs
Página machine-first: escrita para ser colada no contexto de um agente que vai escrever código de integração com a API V2 de Antecipação de Recebíveis da Zemo Capital.
Tudo aqui é derivado das páginas canônicas — Autenticação,
Valores do Recebível, Idempotência,
Códigos de Erro, Webhooks,
Getting Started e Integração em 6 curls — e do
openapi.json, que é a fonte da verdade do contrato. Onde esta
página e o contrato divergirem, o contrato vence.
O llms.txt é o índice (a ordem de leitura das páginas).
Esta página é o system-prompt sugerido — o bloco abaixo é autossuficiente para os casos
comuns. Use os dois juntos: o bloco no system-prompt, o llms.txt como mapa para aprofundar.
1. TL;DR (bloco único, copiável)
SISTEMA: integracao com a API Zemo Capital V2 (antecipacao de recebiveis).
CONTRATO (fonte da verdade): https://docs.zemocapital.com/openapi.json — release 1.3.0, prefixo /v1
BASE URL
Sandbox: https://receivables-api-sandbox.zemocapital.com
Producao: https://receivables-api.zemocapital.com
O ambiente e o da credencial: zk_sbx_* (ou zk_test_*, legado) = Sandbox,
zk_live_* = Producao.
AUTH — Client Credentials (OAuth2), em 2 linhas
1) POST /v1/auth/token Content-Type: application/json
body {"client_id":"zk_test_...","client_secret":"sk_test_..."}
-> {"access_token":"eyJ...","token_type":"bearer","expires_in":900,"scopes":[...]}
2) Todas as demais chamadas: header Authorization: Bearer <access_token> (TTL 15 min — renove)
GOLDEN PATH — 3 endpoints (registro de estoque -> operacao)
1. POST /v1/auth/token -> access_token
2. POST /v1/stock -> registra o recebivel. EXIGE Idempotency-Key
3. POST /v1/stock/request-anticipation -> cria a operacao. EXIGE Idempotency-Key
Depois: GET /v1/operations/{id} (estado autoritativo em lifecycle_status)
POST /v1/webhooks (eventos, em vez de polling)
Atalho quando NAO ha estoque a registrar: POST /v1/operations/direct (tudo numa chamada).
VOCABULARIO CANONICO — use SO estes nomes de valor
gross_future_value · gross_future_value_deductions · net_future_value ·
requested_net_future_value · requested_net_future_value_percent ·
present_value_discount · net_present_liquid_value ·
opr_gross_future_value · opr_net_future_value · opr_present_value_discount ·
opr_net_present_liquid_value
Nunca invente um campo que nao esteja no contrato.
REGRAS DURAS
- Idempotency-Key em TODO POST de dinheiro. Retry = MESMA chave. Nunca gere uma chave nova no retry.
- Dinheiro: BRL, string decimal com 2 casas ("10000.00"). IDs em UUIDv7.
- Usar gross_future_value OBRIGA enviar gross_future_value_deductions no mesmo payload (0 se nao houver).
- Classifique erro por detail.code (ou pelo prefixo ate o ":"), NUNCA pelo texto da mensagem.
- Webhook: valide o HMAC-SHA256 do body CRU no header X-Zemo-Signature; deduplique por event_id.
- Nunca escreva client_id/client_secret no codigo gerado — leia de variavel de ambiente/cofre.
2. Regras de ouro da integração
R1 — Autenticação de API: Client Credentials (OAuth2)
Um único fluxo servidor-a-servidor: troque client_id + client_secret por um JWT RS256 de
curta duração em POST /v1/auth/token, e use esse token como Authorization: Bearer.
curl -sS -X POST "$BASE/v1/auth/token" \
-H "Content-Type: application/json" \
-d '{"client_id": "zk_test_...", "client_secret": "sk_test_..."}'
# -> {"access_token": "eyJ...", "token_type": "bearer", "expires_in": 900, "scopes": [...]}
| Regra | Detalhe |
|---|---|
| Nunca em código gerado | client_id / client_secret saem de variável de ambiente ou cofre de segredos. O client_secret é exibido uma única vez na emissão. Nunca em log, nunca em repositório. |
| TTL | 15 minutos (expires_in: 900). Renove antes de expirar; os SDKs fazem cache e refresh sozinhos. |
| Escopo mínimo | O token carrega scopes granulares. Peça só os que a integração usa — faltando, a rota responde 403 scope_insufficient. |
| Rotação | Não existe endpoint de rotação. Crie uma credencial nova, migre o cliente, e só então revogue a antiga (teto de 10 credenciais ativas por originador). Recomendação: trocar o secret a cada 90 dias. |
| Revogação é imediata | Revogar derruba os access_token já emitidos: a credencial é relida a cada request e responde 401 invalid_client_credentials na hora. |
POST /v1/auth/token recebe um corpo JSON com client_id e client_secret. Não há
grant_type, não há application/x-www-form-urlencoded e não há HTTP Basic. Não use o formulário
genérico "Authorize" de ferramentas OAuth — monte a chamada como está acima.
Detalhes, tipos de chave e ciclo de vida da credencial em Autenticação.
R2 — Idempotência
| Regra | Detalhe |
|---|---|
| Obrigatória | Idempotency-Key é exigida em POST /v1/stock, POST /v1/stock/request-anticipation e POST /v1/operations/direct. Sem o header: 422 idempotency_key_required, antes de qualquer efeito. |
| Formato | 16 a 80 caracteres, validado na entrada (422 idempotency_key_invalid). Use UUID (uuidgen, uuid.uuid4(), crypto.randomUUID()). Nunca contador, hash truncado ou string curta. |
| Retry seguro = MESMA chave | Reenviar a mesma chave com o mesmo corpo devolve a resposta guardada, com Idempotency-Replayed: true, sem executar de novo. TTL de 24 h. |
| Nunca re-gerar no retry | Gerar uma chave nova num retry duplica a operação — é exatamente o acidente que o header existe para impedir. Uma chave por operação de negócio, persistida antes do primeiro envio. |
| Mesma chave, corpo diferente | 409 idempotency_key_reused_with_different_body. Corrija o corpo e use uma chave nova: é outra operação. |
| Chave em voo | 409 idempotency_key_in_flight não é falha: a primeira chamada ainda está rodando. Re-tente com a mesma chave. |
| Opcional, mas honrada | Qualquer POST/PUT/PATCH/DELETE sob /v1/* participa do cache se o header vier. Ignorada em POST /v1/auth/login e POST /v1/auth/token. |
| Segredo não volta no replay | Valores de uso único (hmac_secret, access_token, signer_token) só vêm na primeira resposta; no replay a chave existe com valor null. |
Regras completas, incluindo 503 idempotency_unavailable e o dedup por external_id, em
Idempotência.
R3 — Vocabulário canônico
Estes são os nomes de valor da API. Gere código só com eles:
| Nome | Nível | O que é |
|---|---|---|
gross_future_value | recebível | Valor de face bruto do lastro, antes dos descontos aplicados a ele. |
gross_future_value_deductions | recebível | Os descontos já aplicados ao lastro, declarados por você. |
net_future_value | recebível | NFV (Net Future Value) — o face futuro líquido. É o teto do antecipável e a base do deságio no fluxo de estoque. |
requested_net_future_value | recebível | Quanto do NFV está sendo antecipado. |
requested_net_future_value_percent | recebível | O mesmo, em percentual do NFV (0 < p <= 100). Só em POST /v1/simulate e POST /v1/operations/direct. |
present_value_discount | recebível | Deságio do recebível. |
net_present_liquid_value | recebível | Valor presente líquido do recebível. |
opr_gross_future_value | operação | Soma dos gross_future_value. Não é base de cálculo em nenhuma porta. |
opr_net_future_value | operação | Soma dos net_future_value — o lastro agregado, não o valor antecipado. |
opr_present_value_discount | operação | Deságio total. |
opr_net_present_liquid_value | operação | Valor efetivamente pago por PIX ao cedente. É este que concilia pagamento. |
opr_net_liquid_value | operação | Projeção informativa de opr_net_present_liquid_value − other_debt_discounts. Não é persistida e não concilia nada. |
other_debt_discounts | operação | Saldo devedor dos títulos vencidos do mesmo cedente. Informativo — não é abatido do pagamento. |
- Nunca invente campo. Se o nome não está no
openapi.json, ele não existe — a API não o infere. - Enviar o mesmo valor duas vezes com números diferentes é
422 VOCABULARY_CONFLICT. Nada é escolhido em silêncio. - Semântica do vocabulário:
*_future_valueé o que vale no vencimento;*_present_value/net_present_*é o que vale hoje. O prefixoopr_marca o nível operação (agregado); o nome nu é o nível recebível/título.
A API ainda aceita e ainda devolve um conjunto de nomes de campo antigos, herdados da série
anterior — inclusive dentro do detail de alguns erros e no loc de um validation_error. Eles
serão removidos: não os use em código novo e normalize tudo o que chegar para os nomes desta
página. O inventário histórico, para quem precisa migrar código legado, está em
Valores do Recebível.
R4 — Dinheiro
| Regra | Detalhe |
|---|---|
| Formato | BRL, string decimal com 2 casas nas respostas ("9160.00"). IDs em UUIDv7. |
| Bruto exige deduções | Usar gross_future_value obriga gross_future_value_deductions no mesmo payload — envie 0 se não houver. Sem ele: 422 DEDUCTIONS_REQUIRED_WITH_GROSS. |
| A decomposição tem que fechar | net_future_value == gross_future_value − gross_future_value_deductions, sem tolerância de centavo — senão 422 VALUE_DECOMPOSITION_MISMATCH. |
| Percentual ou valor, nunca os dois | requested_net_future_value_percent é mutuamente exclusivo com o valor absoluto: os dois juntos são 422 REQUESTED_VALUE_AMBIGUOUS. |
| O NFV é teto | requested_net_future_value não pode exceder o net_future_value do recebível — 422. |
| O estoque antecipa sempre 100% | Em POST /v1/stock/request-anticipation e POST /v1/stock/simulate-anticipation, requested_net_future_value tem que ser exatamente a soma dos net_future_value dos itens; qualquer outro valor é 422 STOCK_REQUESTED_VALUE_MISMATCH. Antecipação parcial existe só em POST /v1/operations/direct. |
422 = corrigir, não repetir | Todo 422 é determinístico: o mesmo payload dá o mesmo erro para sempre. Corrija o corpo e mande uma requisição nova — com Idempotency-Key nova, porque é outra operação. |
A cadeia de valor completa, e por que o deságio incide sobre o NFV, em Valores do Recebível.
R5 — Erros: classifique por code, nunca por mensagem
O envelope é sempre detail, em três formas:
| Forma | Quando | Como identificar |
|---|---|---|
Objeto com code: "validation_error" + errors[] | Falha de schema | detail.code === "validation_error" |
Objeto com code + message, sem errors | Regra de negócio estruturada | detail.code presente e detail.errors ausente |
| String | Regra de negócio legada | typeof detail === "string" |
Códigos de regra podem trazer um sufixo : <detalhe> variável (ex.:
stock_item_<id>_not_available): classifique pelo prefixo até o :, nunca por igualdade da
string inteira — e nunca pelo texto de message, que não é contrato e muda sem aviso.
Retry seguro (com a MESMA Idempotency-Key)
| Status | code | O que o agente faz |
|---|---|---|
429 | rate_limit_exceeded | Espera o Retry-After (segundos) e re-tenta. detail.scope diz qual bucket negou (token ou ip). |
409 | idempotency_key_in_flight | A primeira chamada ainda está rodando. Re-tenta em instantes, mesma chave. |
503 | idempotency_unavailable | A requisição não foi executada. Re-tenta com backoff, mesma chave. |
503 | (dependência externa) | Re-tenta com backoff, mesma chave. |
401 | token_expired | Renova o token em POST /v1/auth/token e repete a chamada. |
Ação humana (re-tentar não resolve)
| Status | code | Causa |
|---|---|---|
401 | invalid_client_credentials | Credencial inválida, revogada ou expirada — inclusive fora do token exchange. Emita uma credencial nova. |
403 | scope_insufficient · scope_not_allowed | Falta scope no token / scope pedido fora do permitido. detail traz required e granted (ou allowed_scopes). |
403 | LIMIT_MAX_OPERATION_VALUE · LIMIT_MIN_OPERATION_VALUE · LIMIT_AGGREGATE_EXPOSURE | Limite de policy. detail.source indica o recorte que travou; em LIMIT_AGGREGATE_EXPOSURE, detail.limit_origin indica de onde veio o teto (ver linha abaixo). |
403 | LIMIT_AGGREGATE_EXPOSURE com limit_brl: "0" | Ente TRAVADO, não "sem limite". Quando a policy ancora o risco num lado (cedente ou sacado), a ausência de teto naquele lado significa bloqueio: o ente entra com limite 0 e nada passa por ele. detail.limit_origin distingue anchored_side_closed (ninguém definiu o default do lado) de side_default_zero (o default foi configurado como 0, bloqueio deliberado). No lado não ancorado a ausência libera, mas o que estiver configurado lá vale. É assim que se escreve uma allowlist: default do lado ancorado 0/ausente + limite próprio só nos entes que devem operar. |
403 | LIMIT_CONFIG_MISSING · LIMIT_ENTITY_CONFIG_MISSING | Configuração de limites incompleta na policy — não é excesso de valor. LIMIT_CONFIG_MISSING: nenhum limite agregado configurado. LIMIT_ENTITY_CONFIG_MISSING: falta teto por ente valendo para esta operação — o teto da tese inteira e o teto por operação controlam o agregado e não substituem o limite do ente. Basta o teto do cedente ou de um sacado. detail.configured_levels diz quais recortes chegaram; detail.entity_levels_required, quais satisfazem. Peça ao Backoffice o limite individual do cedente ou do sacado (ou o default por cedente da policy). |
422 | validation_error | Falha de schema. detail.errors[] traz loc, msg e type por campo. |
422 | VOCABULARY_CONFLICT · DEDUCTIONS_REQUIRED_WITH_GROSS · VALUE_DECOMPOSITION_MISMATCH · REQUESTED_VALUE_AMBIGUOUS | Vocabulário ou decomposição de valores. Corrija o payload. |
422 | STOCK_REQUESTED_VALUE_MISMATCH · STOCK_ITEM_NFV_NOT_POSITIVE · STOCK_ITEM_NFV_ABOVE_GROSS · RECEIVABLE_DISCOUNT_EXCEEDS_BASE · CET_OUT_OF_BOUNDS | Regra de negócio de valor/taxa. |
422 | idempotency_key_required · idempotency_key_invalid | Header ausente ou fora de 16–80 caracteres. |
409 | idempotency_key_reused_with_different_body · idempotency_key_reused_on_different_route | Chave reaproveitada indevidamente. |
409 | stock_item_<id>_not_pre_authorized · stock_item_not_in_stock | Item travado (pre_authorized: false) ou fora de IN_STOCK. Libere com PATCH /v1/stock/{item_id}. |
404 | operation_not_found · stock_item_not_found · title_not_found | Recurso inexistente ou de outro originador (a API é multi-tenant). |
423 | user_locked | Senha correta e conta bloqueada por 30 min após 5 falhas de login. Senha errada em conta bloqueada devolve 401 invalid_credentials (a API não é oráculo de existência de e-mail). Passados os 30 min o desbloqueio é automático e o contador de falhas zera — uma nova trava exige 5 novas falhas. |
Não existe 400 nem 502 em rota pública: erro de corpo sai como 422, falha de dependência
externa sai como 503. Catálogo completo em Códigos de Erro — e a lista
de códigos por operação está na própria descrição de cada resposta no
openapi.json.
R6 — Webhooks
| Regra | Detalhe |
|---|---|
| Valide a assinatura | X-Zemo-Signature é o HMAC-SHA256 do body cru (os bytes exatos recebidos, nunca o JSON re-serializado), em hexadecimal, com o hmac_secret como chave. Compare em tempo constante. |
Guarde o hmac_secret | Ele aparece apenas na resposta de POST /v1/webhooks, e não volta num replay de idempotência. |
Idempotência por event_id | A entrega é at-least-once: o mesmo event_id pode chegar mais de uma vez. Deduplique por ele (também disponível no header X-Zemo-Event-Id). |
| Consuma o par completo | Hoje os valores monetários do data ainda viajam com dois nomes lado a lado, com o mesmo número. Leia o canônico — e, num evento antigo reentregue na janela de re-tentativa (~128 min), aceite o nome antigo como fallback, porque o payload entregue é o que foi gravado na emissão. Ver Eventos. |
| Responda 2xx rápido | O timeout de entrega é de 10 s; responda abaixo disso (idealmente em menos de 5 s) e processe de forma assíncrona. |
| HTTPS obrigatório | A URL passa por guarda anti-SSRF: HTTPS e host público resolvível, senão 422 com código da família url_* / ip_*. |
Envelope comum a todo evento — event_type, event_id, occurred_at, data, os quatro sempre
presentes:
{
"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"
}
}
Catálogo de eventos em Eventos; headers e verificação em Segurança de Webhooks.
3. Golden path executável
O caminho principal é o de estoque: registrar o recebível e, depois, criar a operação a partir dele. Versão narrada em Getting Started.
Credenciais zk_sbx_* (ou zk_test_*, legado), sem transações reais. Use zk_live_* só depois do
go-live.
Os valores dos exemplos fecham entre si: NFV 10.000,00 · 3,5% a.m. · 70 dias + 2 de floating
= 72 dias ⇒ deságio 840,00, líquido 9.160,00. Os id que você receber serão outros.
Preparação
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
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
{
"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" }.
Este fluxo também cadastra sacado e cedente, então a credencial precisa ainda de payer:write e
assignor:write (tabela de scopes).
2. Cadastrar sacado e cedente
O dedup por documento torna estes dois comandos seguros para repetir.
PAYER_ID=$(curl -sS -X POST "$BASE/v1/payers" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"cnpj":"98765432000198","legal_name":"Sacado Sandbox SA"}' | jq -r .id)
ASSIGNOR_ID=$(curl -sS -X POST "$BASE/v1/assignors" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"document":"12345678000195","legal_name":"Cedente Sandbox SA","type":"J"}' | jq -r .id)
export PAYER_ID ASSIGNOR_ID
3. Registrar o recebível no estoque
Idempotency-Key é obrigatória aqui.
STOCK_ITEM_ID=$(curl -sS -X POST "$BASE/v1/stock" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d "{
\"assignor_id\": \"$ASSIGNOR_ID\",
\"payer_id\": \"$PAYER_ID\",
\"external_id\": \"NF-2026-001\",
\"backing_type\": \"NFE\",
\"gross_future_value\": \"10000.00\",
\"gross_future_value_deductions\": \"0.00\",
\"net_future_value\": \"10000.00\",
\"due_date\": \"2026-10-07\",
\"pre_authorized\": true
}" | jq -r .id)
export STOCK_ITEM_ID
Resposta (201) — quatro campos, só isso:
{
"id": "01994c1e-c000-7000-9000-0000000000a7",
"external_id": "NF-2026-001",
"status": "IN_STOCK",
"created_at": "2026-07-29T14:30:00Z"
}
gross_future_value e net_future_value precisam ser maiores que zero, e usar o bruto obriga
declarar gross_future_value_deductions (aqui 0.00, porque bruto e NFV coincidem).
pre_authorized: true libera o item para antecipação — item com false é recusado com 409
tanto na simulação quanto na solicitação. Para reler o item inteiro, use
GET /v1/stock/{item_id}.
4. Simular (não cria nada)
curl -sS -X POST "$BASE/v1/stock/simulate-anticipation" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"stock_item_ids\": [\"$STOCK_ITEM_ID\"],
\"requested_net_future_value\": 10000.00,
\"fees\": { \"monthly_rate_pct\": 3.5, \"floating_days\": 2 }
}"
{
"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,
"floating_days": 2,
"receivables": [
{
"stock_item_id": "01994c1e-c000-7000-9000-0000000000a7",
"external_id": "NF-2026-001",
"gross_future_value": "10000.00",
"net_future_value": "10000.00",
"requested_net_future_value": "10000.00",
"due_date": "2026-10-07",
"days_advanced": 72,
"present_value_discount": "840.00",
"net_present_liquid_value": "9160.00",
"monthly_rate_pct": "3.5",
"fee_source": "operation"
}
]
}
Omitir fees faz a API aplicar a hierarquia de taxas configurada para o seu originador.
requested_net_future_value tem que ser exatamente a soma dos net_future_value dos itens —
o estoque antecipa sempre 100%.
5. Solicitar a antecipação
Idempotency-Key é obrigatória aqui.
OPERATION_ID=$(curl -sS -X POST "$BASE/v1/stock/request-anticipation" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d "{
\"stock_item_ids\": [\"$STOCK_ITEM_ID\"],
\"requested_net_future_value\": 10000.00,
\"bank\": { \"use_document_pix\": true },
\"fees\": { \"monthly_rate_pct\": 3.5, \"floating_days\": 2 }
}" | jq -r .id)
export OPERATION_ID
{
"id": "01994c1e-9b20-7000-9000-0000000000b2",
"display_number": "OP-A1B2C3D4-E5F6G7H8",
"lifecycle_status": "WAITING_APPROVAL",
"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",
"other_debt_discounts": "0.00",
"opr_net_liquid_value": "9160.00"
}
opr_net_present_liquid_value e opr_net_liquid_value são campos diferentesopr_net_present_liquid_value (9160.00) é o PIX ao cedente — é por ele que se concilia.
opr_net_liquid_value é uma projeção informativa (opr_net_present_liquid_value menos
other_debt_discounts), não é persistida e não concilia nada. Aqui os dois coincidem porque
other_debt_discounts é 0.00.
6. Consultar o estado
curl -sS "$BASE/v1/operations/$OPERATION_ID" \
-H "Authorization: Bearer $TOKEN"
{
"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"
}
lifecycle_status é o campo autoritativo. Polling serve para depurar; o caminho de produção
é o webhook do passo 7. Regra completa em
Lifecycle.
7. 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"
}'
{
"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 só aparece nesta resposta e é o segredo que valida toda entrega. Grave-o no seu cofre de segredos antes de seguir.
8. Receber: verificar o HMAC
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 menos de 5 s
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);
});
Caminho secundário: operação direta
Atalho para quando não há estoque a registrar: POST /v1/operations/direct envia cedente,
recebíveis e dados bancários numa única chamada, sem passar pelo estoque. Mesmas regras de
Idempotency-Key e de vocabulário.
Duas diferenças que mudam código:
- É a única rota que faz antecipação parcial do NFV (
requested_net_future_valuemenor quenet_future_value, ourequested_net_future_value_percent); o restante volta ao estoque como um item novo, que nasce travado (pre_authorized: false) e precisa ser liberado comPATCH /v1/stock/{item_id}. - A resposta traz o alias legado
statuse deixalifecycle_statusemnull— o oposto do fluxo de estoque. Cliente que atende os dois lêlifecycle_status ?? status.
O simulate desta porta é POST /v1/simulate (recebe receivables[], não stock_item_ids).
Payloads completos em Integração em 6 curls e
Operação direta.
4. Recursos machine-readable
| Recurso | URL | O que é |
|---|---|---|
| Índice para agentes | /llms.txt · /.well-known/llms.txt | O índice: a ordem de leitura das páginas, pronto para colar no contexto. |
| Especificação viva | /openapi.json | Fonte da verdade do contrato (release 1.3.0): endpoints, schemas, códigos de erro por operação e exemplos. |
| Especificação congelada | /openapi-v1.1.0.json | Snapshot imutável de 1.1.0 — o piso de compatibilidade da série 1.x. Fixe-o em geradores de SDK, mocks e testes de contrato quando quiser um alvo que não muda. |
| Collection Postman | /postman_collection.json | Gerada da especificação; importa direto no Postman/Insomnia. |
| Referência interativa | /reference | A mesma especificação renderizada (Scalar), com exemplos de código por endpoint. |
| Changelog | Changelog | Histórico de mudanças que afetam a integração, em ordem de versão. |
llms.txt é o índice; esta página é o system-prompt sugeridoO llms.txt diz o que ler e em que ordem. Esta página diz como escrever a integração sem
errar. Um agente bem calibrado carrega o bloco da seção 1 no system-prompt e resolve o resto
consultando o openapi.json.
5. Anti-padrões
1. Gerar código com nomes de campo legados. A API ainda os aceita hoje e vai removê-los. Código novo nasce com os nomes da seção R3 — e normaliza para eles tudo o que chegar.
2. Pular a idempotência, ou gerar chave nova no retry. As duas falhas produzem o mesmo desfecho: operação duplicada, dinheiro pago duas vezes. Uma chave por operação de negócio, persistida antes do primeiro envio, reutilizada em todos os retries.
3. Parsear a mensagem de erro. detail.message não é contrato: o texto muda sem aviso.
Classifique por detail.code — e, nos códigos com sufixo variável, pelo prefixo até o :.
4. Confundir os dois "liquid value" da operação. Colisão de homônimos real:
opr_net_present_liquid_value é o PIX ao cedente e concilia pagamento;
opr_net_liquid_value é uma projeção informativa que desconta débitos vencidos e não
concilia nada. Os nomes são parecidos e o significado não é — a nota completa está em
Valores do Recebível. No mesmo espírito, opr_net_future_value agrega
lastro, não valor antecipado: somá-lo entre operações conta o item-resto duas vezes.
5. Tratar 422 como falha transitória. 422 é determinístico: re-tentar o mesmo payload
devolve o mesmo erro para sempre. Corrija o corpo. Transitório é 429 (respeite Retry-After) e
503 (backoff, mesma Idempotency-Key).
Outros dois que aparecem com frequência: assumir que o simulate espelha o create (fora de três validações compartilhadas, as portas divergem por design — ver Códigos de Erro) e inventar campo que "deveria existir" — a API não infere nada que você não declarou.
6. Versão, política e suporte
| Item | Valor |
|---|---|
| Release vigente | 1.3.0 — toda resposta traz o header X-Zemo-API-Version. |
| Versão na URL | /v1. Só muda em quebra de compatibilidade, e aí a anterior entra em descontinuação anunciada. |
| Retrocompatível (pode chegar sem aviso) | Endpoint novo, campo opcional novo em request, campo novo em response, valor de enum novo, código de erro novo, evento de webhook novo. Programe defensivamente: ignore campo desconhecido e trate enum aberto com default. |
| Breaking | Remover/renomear campo ou endpoint, tornar opcional em obrigatório, mudar tipo/semântica, remover valor de enum. Só por versão nova de URL, anunciada no Changelog. |
| Nomes de campo legados | Continuam aceitos na série 1.x e são removidos na 2.0. Não os use — a garantia é de versão, não de calendário. |
| Antes de produzir | Checklist de Go-Live. |
| Suporte | Guarde o header X-Request-Id de cada resposta e informe-o ao seu contato técnico na Zemo — é com ele que a requisição é localizada. Disponibilidade em status.zemocapital.com. |
Política completa em Versionamento.