Changelog
Historico de mudancas que afetam a integracao de API dos originadores.
As entradas estao em ordem de versao (decrescente), nao de data. As datas das
releases anteriores a v1.0.0 nao formam uma linha do tempo consistente com
essa ordem (v0.7.0 e v0.6.0 aparecem datadas 2026-06-10, depois da v1.0.0
de 2026-06-03; v0.2.0 e v0.1.0 aparecem datadas depois da v0.3.0).
Nenhuma entrada foi apagada nem redatada: para efeito de compatibilidade vale a
ordem de versao — as datas pre-1.0.0 sao indicativas.
v1.6.0 (2026-08-20) — Limites: o teto da CREDENCIAL acaba, o override do ente passa a mandar, e a ancoragem de risco ganha semântica
Esta versão tem duas partes, as duas no motor de limites de crédito:
- o teto da credencial de API acaba — quem limita valor por operação passa a ser só a policy;
- o motor muda como os limites se compõem: o limite próprio do ente passa a
mandar sobre o default da policy nas duas direções, a ancoragem de risco passa
a dar significado à ausência de teto em cada lado, e o
detaildo403ganha o campolimit_origin.
As mudanças da parte 2 não chegam todas ao mesmo público:
- o limite próprio do ente (item 1) alcança TODAS as policies, inclusive as que estão no padrão. É a regra de composição entre o default da policy e o limite do vínculo, e ela não depende de ancoragem nenhuma;
- a semântica por lado do risco (item 2) só vale onde alguém configurar
risk_anchorfora do padrãoboth. Com o padrão, a herança de limites — o que vale quando o ente não tem limite próprio — é byte-a-byte a de sempre.
Nada muda para você se as suas policies estão no padrão e nenhum ente tem
limite próprio menor que o default da policy (o 0 incluído). Se algum tem,
leia o item 1: antes valia o maior dos dois, agora vale o do ente.
Parte 1 — o teto por operação da CREDENCIAL acaba
A credencial de API tinha um teto próprio (limits), separado dos limites da
policy: um valor máximo por operação e uma janela de prazo mínimo/máximo dos
recebíveis. Esse teto deixa de existir. O limite de valor por operação passa
a ter uma fonte só — a policy da entidade, configurada no Backoffice.
1. Duas recusas somem da criação de operação. POST /v1/stock/request-anticipation
e POST /v1/operations/direct não devolvem mais os códigos 403:
| Código que sumiu | O que ele recusava |
|---|---|
LIMIT_MIN_TERM_DAYS | recebível com prazo menor que o mínimo da credencial |
LIMIT_MAX_TERM_DAYS | recebível com prazo maior que o máximo da credencial |
Nenhuma camada recusa mais por PRAZO — não há substituto: prazo de recebível
não é limite de crédito. Uma operação que antes tomava 403 por essa razão
agora segue para as demais validações (preço, CET, limites de crédito), e
pode ser criada.
LIMIT_MAX_OPERATION_VALUE continua existindo, nas duas portas. O que muda
é de onde ele vem: só do teto por operação da policy (detail.source com
policy_per_operation), nunca mais do teto da credencial. Se o seu originador
usava o teto do token para segurar valor, esse número precisa passar a viver na
policy — peça ao seu contato Zemo.
2. limits sai do corpo de criação de token (Portal e Backoffice). Mandar o
campo agora devolve 422 limits_removido_teto_vive_na_policy, em vez de
ser aceito e gravado. É deliberado: aceitá-lo em silêncio entregaria uma
credencial que o chamador acredita ter teto e que não tem nenhum. O que
fazer: remova o campo do payload de criação. Os tokens já emitidos não são
tocados — a coluna continua no banco como dado histórico, e o GET de tokens
segue devolvendo o que está gravado; ele apenas não decide mais nada.
Por que (parte 1)
Um censo somente-leitura de produção não encontrou nenhuma credencial de API viva com teto configurado: as únicas que existiram foram revogadas e nenhuma delas tinha o campo preenchido. O gate cobrava um pedágio no caminho do dinheiro — uma consulta e uma decisão por operação criada — para proteger um conjunto vazio, ao lado de um motor de limites de crédito que já decide valor por entidade, por recorte e por exposição em aberto. Duas fontes para "quanto cabe nesta operação" só se justificam se as duas protegerem algo.
Parte 2 — o motor de limites: override absoluto e semântica por lado do risco
1. O limite próprio do ente passa a MANDAR — nas duas direções.
Quando um cedente ou sacado tem limite próprio configurado no vínculo, é ele que vale, maior ou menor que o default da policy. Antes, o motor ficava com o maior dos dois — e, por causa disso, era impossível APERTAR um ente específico: um limite individual menor que o default era simplesmente ignorado.
| No vínculo do ente | O que vale |
|---|---|
valor > 0 | o valor do vínculo, mesmo que seja menor que o default da policy |
valor 0 | bloqueio daquele ente — mesmo que a policy tenha default folgado |
vazio / null | herda o default do lado (é o "não defini nada aqui") |
⚠️ 0 e vazio deixaram de ser a mesma coisa. Se você usava 0 esperando "sem
limite configurado", ele agora bloqueia aquele ente. Quem quer herdar o
default manda o campo vazio.
2. A ancoragem de risco passa a dar significado à AUSÊNCIA de teto.
Numa policy com risk_anchor em assignor ou payer, a ausência de limite passa
a significar coisas opostas em cada lado:
| lado ANCORADO (onde o risco está) | lado NÃO ancorado | |
|---|---|---|
sem teto / teto 0 | trava: nada passa por aquele ente | sem limite por ali |
teto > 0 | teto normal | teto normal |
| limite próprio no ente | manda sempre | manda sempre |
E, junto com isso, a ancoragem deixou de desligar o lado não ancorado: um limite configurado ali volta a travar normalmente. Antes ele era ignorado — o que significava que um número que alguém digitou no Backoffice não valia nada.
É assim que se escreve uma allowlist: default do lado ancorado em 0 (ou
ausente) e limite próprio apenas nos entes que devem operar.
3. A recusa do caso "lado ancorado sem teto" muda de código.
| Antes | Agora |
|---|---|
403 LIMIT_ENTITY_CONFIG_MISSING, com detail.missing_entity_scopes listando os entes descobertos | 403 LIMIT_AGGREGATE_EXPOSURE, com limit_brl: "0" e detail.limit_origin |
A operação continua sendo recusada — o que muda é a explicação: deixou de ser
"falta configuração" e passou a ser "este ente está travado", que é uma recusa de
limite. O campo missing_entity_scopes não existe mais.
LIMIT_ENTITY_CONFIG_MISSING continua existindo no caso que sempre foi dele:
a policy tem limite agregado mas nenhum limite por ente valendo para a
operação — basta o do cedente ou o de um sacado.
4. Campo novo no detail: limit_origin.
Todo 403 LIMIT_AGGREGATE_EXPOSURE passa a trazer limit_origin, que diz de
onde veio o teto que travou: configured (default da policy),
entity_override (limite próprio do ente), anchored_side_closed (lado ancorado
sem default definido) ou side_default_zero (default do lado configurado como
0). É o campo que distingue "você estourou um teto" de "este ente está
bloqueado" — situações com remediações opostas. Tabela completa em Códigos de
Erro.
5. Novo limite: default por SACADO na policy (payer_default_limit_brl).
O lado do cedente já tinha dois degraus (default da policy + limite do vínculo); o do sacado só tinha o do vínculo. Agora tem os dois, simétricos. Configuração de Backoffice: nada muda no seu payload, e a ausência do campo num evento de policy preserva o valor gravado.
6. Ordem de avaliação: o teto por operação vem ANTES das travas de ente.
Uma operação que estoura o teto por operação e esbarra num ente travado
reporta LIMIT_MAX_OPERATION_VALUE. É deliberado: "peça menos" é acionável, "seu
lado está travado" não.
Por que (parte 2)
Três lacunas de produto no desenho anterior, todas decididas pelo owner:
- compor default e limite do ente pelo maior tornava impossível apertar um ente específico — a exceção só podia ser para mais, nunca para menos;
- a ancoragem desligava o lado oposto, o que apagava configuração explícita: um teto digitado no Backoffice deixava de valer por causa de um atributo de outro lado da policy;
- a ausência de limite não tinha significado definido por lado, então "não configurei" e "quero bloquear" eram indistinguíveis — e a allowlist, o caso de produto mais natural do modelo, não era expressável.
Por que MINOR, e não uma versão de URL nova
Parte 1. Os dois códigos removidos são recusas: pela política de
versionamento, um cliente já é obrigado a tratar
detail.code desconhecido como "403 negado". Nenhum endpoint, tipo ou status HTTP
mudou nas rotas públicas; o que muda é o desfecho de chamadas que antes eram
negadas e agora prosseguem — no sentido de afrouxar. A rejeição 422 do item
2 é restritiva, mas atinge só as rotas de criação de token
(Portal/Backoffice), que não fazem parte do spec público.
Parte 2. limit_origin é campo acrescentado a um detail que já existia
— aditivo, e um cliente que ignore campos desconhecidos não percebe. A troca de
LIMIT_ENTITY_CONFIG_MISSING por LIMIT_AGGREGATE_EXPOSURE naquele cenário é
troca entre recusas (403 nos dois casos, e o contrato já obriga a tratar
detail.code desconhecido). Nenhum campo saiu de resposta de sucesso.
⚠️ Diferente da parte 1, aqui há mudanças no sentido RESTRITIVO, e elas têm alcances diferentes:
- o limite próprio do ente passar a mandar (item 1) alcança TODAS as policies,
o padrão
risk_anchor: bothincluído. Uma operação que hoje passa porque o default da policy é folgado passa a ser recusada quando o ente tem limite próprio menor — e um0no vínculo, que antes era descartado pelo default maior, agora bloqueia o ente; - a semântica por lado do risco (item 2) alcança apenas as policies que
configurem
risk_anchorfora deboth— nelas, um limite no lado não ancorado volta a travar.
A versão segue MINOR porque nenhuma dessas mudanças altera endpoint, tipo, status
HTTP ou campo de resposta de sucesso: o que muda é o desfecho de chamadas, e o
contrato já obriga o cliente a tratar 403 com detail.code desconhecido. Mas
elas não são todas invisíveis para quem está no padrão — leia o item 1 se
algum dos seus entes tem limite próprio menor que o default da policy, 0
incluído.
Esta versão foi congelada como
openapi-v1.6.0.json e passa a ser o piso de
compatibilidade comparado a cada mudança. Os snapshots anteriores continuam
publicados e intactos.
v1.5.1 (2026-08-18) — Alçada na porta direta; desbloqueio automático do login vira real
Duas correções de comportamento em fluxos que já existiam. Nenhum endpoint novo, nenhum campo novo obrigatório, nenhum código HTTP alterado — mas a primeira delas muda o desfecho de chamadas que você já faz, então leia o "O que fazer".
1. POST /v1/operations/direct — e o espelho do Portal POST /v1/portal/operations —
passa a aplicar o limite de auto-aprovação da policy. A operação nasce
APPROVED_DIRECT quando o total solicitado (a soma dos
requested_advance_value do payload) cabe no auto_approve_limit_brl da policy
regente, e WAITING_APPROVAL quando o ultrapassa. É o mesmo gate, a mesma
base e o mesmo fail-closed que POST /v1/stock/request-anticipation já
aplicava: as duas portas agora leem uma regra só. Limite não configurado
(ausente ou zero) ⇒ WAITING_APPROVAL sempre — "sem teto" nunca significa
"aprova tudo".
needs_backoffice_approval passa a vir preenchido nesta porta. O campo já
estava publicado no schema da resposta, mas só o fluxo de estoque o preenchia;
pela porta direta ele chegava sempre null. Agora ele carrega o veredito do
gate: true = a operação entrou na fila de aprovação do Backoffice
(WAITING_APPROVAL); false = nasceu aprovada (APPROVED_DIRECT), sem espera
humana.
2. POST /v1/auth/login — o desbloqueio automático dos 30 minutos passa a
funcionar de verdade. O contador de falhas só voltava a zero num login bem
sucedido: passados os 30 minutos do bloqueio, a primeira senha errada da
janela seguinte já batia no limiar de 5 tentativas e re-travava a conta por mais
30 minutos — indefinidamente, e a conta nunca alcançava na prática o
"desbloqueio automático" que esta documentação promete. Agora, com o bloqueio
vencido, a janela recomeça: essa primeira falha conta como tentativa 1 de 5,
e só cinco falhas seguidas voltam a travar a conta. Nada muda no contrato — nem
status, nem corpo, nem campo.
Toda operação tem duas aprovações. A A1 é a aprovação da parte — o "liberado para antecipar" — e não muda nada nesta versão. A A2 é a aprovação da Zemo, e é dela que o item 1 trata: a A2 só é automática se estiver dentro do teto da policy ESPECIFICAMENTE no limite Auto-Approve. Fora desse teto — ou sem teto configurado — a A2 é manual, na mesa do Backoffice.
O que fazer: pare de assumir que a criação direta nasce
WAITING_APPROVAL. Leia o estado com a precedência lifecycle_status ?? status
(nesta porta o preenchido é o alias legado status), ou simplesmente leia o
booleano needs_backoffice_approval — os dois dizem a mesma coisa, e o booleano
poupa você de conhecer os literais do lifecycle. Quem escuta webhooks: a
operação auto-aprovada por esta porta emite operation.approved (mesmo payload
do fluxo de estoque) e deixa de emitir operation.approval_requested — que,
para uma operação já aprovada, seria um item fantasma na fila do operador. Os
valores das operações não mudam: o gate decide roteamento, não preço.
Substitui uma afirmação da entrada v1.1.0
A entrada da v1.1.0, nesta mesma página, afirma no item sobre a base do gate
de auto-aprovação: "POST /v1/operations/direct não tem gate de alçada — a
criação direta nasce sempre WAITING_APPROVAL". Essa afirmação deixa de valer
a partir da 1.5.1 — é exatamente o que esta entrada corrige. A frase antiga
permanece onde está, como registro histórico do que valia na 1.1.0; o estado
vigente é o descrito aqui.
Por que
O auto_approve_limit_brl da policy é o teto de alçada do originador, e ele já
decidia o roteamento no fluxo de estoque. Na porta direta o mesmo número
viajava apenas como dado no evento da fila do Backoffice: nunca decidia
nada. O efeito prático era um originador com teto configurado ver toda
operação criada pela porta direta parar na fila do Backoffice, mesmo quando o
valor cabia folgadamente na alçada — o limite existia no papel e não no
caminho. Duas portas para o mesmo produto, com o mesmo teto, davam desfechos
diferentes.
Por que PATCH
Nada foi removido, nenhum campo virou obrigatório, nenhum código HTTP mudou e
nenhum valor foi reprecificado — os dois itens são correção: o gate passa a
aplicar um limite que já era do contrato de produto, e o login passa a cumprir o
desbloqueio automático que a documentação já prometia. O que é observável
segue a lista de mudanças retrocompatíveis da política de
versionamento: um valor a mais no alias status (APPROVED_DIRECT,
que o fluxo de estoque já emitia) e um campo que deixa de chegar null.
Ainda assim, quem tratava status desta rota como a constante
WAITING_APPROVAL precisa mudar — daí o destaque no "O que fazer".
v1.5.0 (2026-08-17) — Login: 423 só com credencial correta
BREAKING CHANGE — altera o código HTTP de uma resposta já existente, categoria classificada como quebra pela política de versionamento.
POST /v1/auth/login para de responder 423 para quem erra a senha numa
conta bloqueada. Antes, uma conta bloqueada respondia 423 user_locked
independentemente da senha enviada estar certa — o que permitia a quem não
tem a senha descobrir, por tentativa e erro, se um e-mail existe na base
(bastava provocar 5 falhas e observar se a resposta virava 423). Agora:
401 invalid_credentialssempre que a senha não é provada — e-mail inexistente, senha errada (conta bloqueada ou não) devolvem todos o mesmo status e o mesmo corpo.423 user_lockedsó quando a senha está correta e a conta está bloqueada (5 tentativas falhadas seguidas ⇒ bloqueio de 30 minutos).
O que esta versão garante é a igualdade de status e corpo da resposta. O tempo de resposta é tratado à parte, como mitigação de canal lateral: o login executa a mesma verificação de senha (mesmo custo de KDF) mesmo quando o e-mail não existe, para que a latência não denuncie a existência da conta. Como toda contramedida temporal, ela reduz o sinal — não prova igualdade perfeita de tempo. Ver Proteção contra brute force.
O que fazer: trate 401 como "credenciais inválidas" (não assuma nada
sobre a existência da conta) e 423 como "credencial válida, conta
temporariamente bloqueada". Nenhum campo e nenhum endpoint mudou — só o
código devolvido num cenário que já existia (senha errada em conta
bloqueada).
Por que
O 423 é um sinal só para quem prova a senha: é a própria credencial
correta que autoriza a informação "esta conta está bloqueada". Devolvê-lo
para senha errada transformava o bloqueio num oráculo de existência de
e-mail — 5 tentativas com senha qualquer bastavam para distinguir "conta
existe e está bloqueada" (423) de "conta não existe ou senha errada"
(401). A partir da 1.5.0 a rota não revela mais essa diferença: quem não
prova a senha recebe sempre 401, ponto.
Por que MINOR apesar de BREAKING
A política de versionamento classifica "alterar
códigos HTTP de respostas existentes" como quebra de compatibilidade — e
esta mudança entra no changelog marcada BREAKING CHANGE por isso. Ainda
assim ela sai como MINOR (1.4.0 → 1.5.0), não como uma nova versão de
URL (/v2): segue o mesmo precedente da 1.4.0 (mudança de desfecho de
chamadas dentro do fluxo de auth, sem novo endpoint nem novo campo) — decisão
de produto, não uma nova superfície de contrato. Nenhum campo saiu, nenhum
ficou obrigatório, nenhum endpoint novo apareceu; o único efeito observável é
o código HTTP de um cenário específico (senha errada numa conta bloqueada).
v1.4.0 (2026-08-10) — Limite POR ENTE obrigatorio para aprovar operacao
Uma operacao so e aprovada se existir teto configurado do CEDENTE ou do SACADO.
O teto da tese inteira (o limite agregado da policy) continua valendo, mas deixou
de ser suficiente sozinho: quem tem apenas a tese configurada passa a receber
403 com LIMIT_ENTITY_CONFIG_MISSING.
Se as suas policies ja tem limite individual de cedente ou de sacado — ou o default por cedente da policy — nada muda para voce.
Por que
O limite agregado da tese controla a exposicao da carteira inteira; ele nao
controla ente nenhum. Uma policy so com a tese aprovava operacao de qualquer
cedente e de qualquer sacado ate o teto da carteira, sem nenhum limite individual
no caminho. A partir da 1.4.0 a plataforma exige que exista pelo menos um teto
por ente valendo para a operacao. E uma recusa de configuracao, nao de valor:
o numero da operacao nao muda nada: ate o teto ser configurado, a resposta e a
mesma.
O 403 novo
{
"detail": {
"code": "LIMIT_ENTITY_CONFIG_MISSING",
"configured_levels": ["policy_total"],
"entity_levels_required": ["assignor", "payer", "policy_entity_default"],
"message": "Nenhum limite POR ENTE (cedente ou sacado) vale para esta operacao: o teto da tese inteira (policy-total) e o teto por operacao controlam o agregado, mas nao substituem o limite do ente. Configure o limite individual do cedente ou do sacado (ou o default por cedente da policy) no Backoffice — e, sob ancoragem de risco, no lado em que o risco esta ancorado."
}
}
configured_levels diz quais recortes de limite chegaram a esta operacao e
entity_levels_required, quais deles satisfazem a regra. Remediacao: peca ao
Backoffice o limite individual do cedente ou do sacado (ou o default por cedente
da policy). Re-tentar sem configurar da o mesmo 403. Lista completa em
Codigos de Erro.
LIMIT_MAX_OPERATION_VALUE numa policy so com a teseO codigo do detail de um cenario que ja era recusado pode mudar. Quando a
policy nao tem teto por ente e a operacao estoura o teto por operacao, a
resposta era LIMIT_MAX_OPERATION_VALUE e agora e
LIMIT_ENTITY_CONFIG_MISSING. O 403 e o mesmo, a chamada continua recusada — o
que mudou e qual dos dois defeitos e reportado primeiro.
A precedencia e deliberada: o defeito de configuracao vem antes do de
valor, porque ele nao depende do numero da operacao. Baixar o valor nao
resolveria, e sem esta ordem o integrador ficaria ajustando o valor de uma
operacao que ia ser recusada de qualquer jeito. Se voce ramifica logica por
detail.code, trate os dois codigos como "recusa que exige acao humana".
Limite do cedente no canal direto (correcao de comportamento)
No canal direto (cedente sem parceiro, operado pela propria plataforma) o limite
individual do cedente nao estava sendo aplicado: uma regra de preco —
naquele canal vale a taxa padrao da policy, e nao a negociada no vinculo —
acabava escondendo tambem o limite do vinculo do motor de exposicao. Com a
1.4.0 os dois assuntos ficam separados: o preco segue como era, e o teto do
cedente volta a ser aplicado.
Se voce opera no canal direto e o cedente tem limite individual configurado,
operacoes que passavam por cima desse teto passam a receber 403
LIMIT_AGGREGATE_EXPOSURE com "source": "assignor". O teto sempre esteve
configurado; o que mudou e que ele agora vale.
Ancoragem de risco: todos os entes do lado ancorado
Para as policies que usam ancoragem de risco (o risco fica no cedente ou no
sacado, e so os recortes daquele lado travam a operacao), a exigencia e mais
forte: todos os entes do lado ancorado precisam de teto proprio. Numa
antecipacao de estoque com varios sacados e a ancoragem no sacado, um unico
sacado sem teto recusa o lote inteiro — o detail traz
missing_entity_scopes com a lista de quem esta descoberto.
{
"detail": {
"code": "LIMIT_ENTITY_CONFIG_MISSING",
"configured_levels": ["policy_total", "payer"],
"entity_levels_required": ["assignor", "payer", "policy_entity_default"],
"missing_entity_scopes": ["payer:3f2a...", "payer:9c81..."],
"message": "Sob ancoragem de risco, TODOS os entes do lado ancorado precisam de limite proprio configurado, e estes nao tem: payer:3f2a..., payer:9c81.... Configure o limite individual de cada um no Backoffice (ou o default por cedente da policy, quando a ancoragem for no cedente)."
}
}
Sem ancoragem (o default de toda policy hoje) a regra e a da secao anterior: um teto de qualquer das duas pontas basta.
Correcoes de documentacao publicadas junto (comportamento nao mudou agora)
Duas paginas descreviam comportamento que ja nao era o vigente — a correcao entra neste trem porque a doc tem de descrever o que o codigo faz:
- medida do limite do sacado: o recorte do sacado mede
requested_net_future_valuenas duas portas (o valor antecipado, em moeda de Net Future Value). A prosa dizia "bruto no fluxo de estoque". Vigente desde a1.2.0; ver Valores do Recebivel. pre_authorizeddo item-resto: o resto de uma antecipacao parcial nasce sempre bloqueado, mesmo quando o item de origem estava liberado. A prosa dizia que ele herdava a liberacao. Se voce contava com a heranca, o sintoma e um409 stock_item_<id>_not_pre_authorizedao antecipar o resto — libere comPATCH /v1/stock/{item_id}.
Nada quebra no contrato
Nenhum campo saiu, nenhum virou obrigatorio, nenhum status mudou e nenhum
endpoint novo apareceu. O 403 ja fazia parte do vocabulario das rotas de
criacao; o que entra e um codigo novo dentro dele
(LIMIT_ENTITY_CONFIG_MISSING) — aditivo pela
politica de versionamento. A 1.4.0 e MINOR porque
muda o desfecho de chamadas identicas em duas classes de configuracao, e
essa e a mesma razao pela qual a 1.3.0 foi MINOR.
v1.3.0 (2026-08-04) — Assinatura embedded e envio automatico do contrato para operacoes de API
Duas coisas, e a primeira vale mesmo que voce nao use nada novo:
- Operacao criada pela API agora dispara o contrato sozinha ao ser aprovada — como ja acontecia com as operacoes do portal. Antes, quem criava pela API precisava chamar o disparo por conta propria.
- Modo embedded (opt-in): voce recebe a URL de assinatura de cada signatario e a apresenta na sua interface, em vez de deixar o e-mail automatico sair. Veja Assinatura embedded.
Envio automatico do contrato (mudanca de comportamento)
Ate a 1.2.0, o envio automatico do contrato pos-aprovacao existia apenas
para operacoes originadas no portal. Operacao criada pela API ficava aprovada e
parada, esperando um disparo explicito — apesar de esta documentacao afirmar que
o envio era automatico. A 1.3.0 alinha o comportamento a promessa: toda
operacao aprovada dispara o contrato, qualquer que seja a origem.
A rota interna de disparo que alguns integradores usavam continua existindo e
respondendo, mas agora ela encontra o contrato ja despachado e responde
409 contract_already_exists. Isso e esperado, e nao um erro do seu lado: o
contrato foi enviado. Trate esse 409 como sucesso, ou pare de chamar o
disparo. Para obter as URLs de assinatura do despacho vigente, use
GET /v1/operations/{operation_id}/contract/signers.
Se voce chamava aquela rota com Idempotency-Key, note tambem que o
replay dela passa a devolver sign_url: null dentro de zapsign_response.
E a mesma redacao descrita em Cuidados com a sign_url
e vale para qualquer rota que carregue a URL: a credencial nao pode ficar em
repouso no cache de idempotencia. A primeira resposta continua trazendo a URL.
Endpoints novos
| Endpoint | Para que serve |
|---|---|
POST /v1/operations/{id}/contract/send-for-signature | Dispara o contrato (re-tentativa ou modo embedded) e devolve as sign_url |
GET /v1/operations/{id}/contract/signers | Le ao vivo os signatarios, com status e sign_url |
Cada uma exige um scope novo e diferente (ver Scopes):
GET .../contract/signers pede contract:read e
POST .../contract/send-for-signature pede contract:send. Nenhum dos dois
vem junto de operation:create — credenciais existentes precisam receber o que
usarem.
O recorte tipico do modo embedded e so contract:read: o disparo acontece
automaticamente na aprovacao e a integracao apenas le as URLs. contract:send e
um opt-in de borda (re-disparo apos CONTRACT_ERROR, operacoes importadas
do fluxo antigo, sobrescrever o modo embedded, disparar sem esperar a
varredura) — peca-o so na credencial que de fato dispara contrato.
Campo novo nos creates
embedded_signature (booleano, opcional, default false) em
POST /v1/operations/direct e POST /v1/stock/request-anticipation. Omitir
mantem exatamente o comportamento de hoje. A flag fica na operacao porque o
disparo e automatico: declara-la so no momento do disparo chegaria tarde.
Status 502 no contrato publico
As rotas de contrato dependem de um provedor de assinatura externo e podem
responder 502 quando ele falha (zapsign_create_failed,
zapsign_unavailable, ...). O 502 passa a fazer parte do vocabulario de erro
publicado — nenhuma rota existente mudou de status; o codigo esta declarado para
que o seu cliente ja o trate.
Cuidados com a sign_url
Quem tem a URL assina o documento — e credencial. Nao registre em log, nao guarde em claro. Dois pontos que costumam surpreender:
- Replay de idempotencia: reenviar o disparo com a MESMA
Idempotency-Keydevolve o mesmo corpo com"sign_url": null. A URL e apagada de proposito da resposta guardada (ela nao pode ficar em repouso no cache). Nao e erro — leia as URLs emGET /v1/operations/{id}/contract/signers. - Sem watchdog no modo embedded: com
embedded_signature: truenenhuma notificacao sai, nem lembretes. Se voce nunca apresentar asign_url, ninguem assina e a operacao fica parada indefinidamente.
Nada quebra no contrato
Endpoints novos, campo de request opcional, codigo de erro novo: tudo aditivo. Nenhum campo saiu, nenhum virou obrigatorio, nenhum status de operacao existente mudou. A mudanca de comportamento do envio automatico esta descrita acima porque afeta quem chamava o disparo manualmente — nao porque o contrato tenha sido quebrado.
v1.2.0 (2026-07-30) — Vocabulario canonico de valores: *_future_value / *_present_value
Esta versao e 100% aditiva. Todo campo que existia continua existindo, aceito nos requests e devolvido nas responses, com o mesmo numero e a mesma semantica. Nenhuma conta mudou. Se a sua integracao roda hoje, ela continua rodando sem nenhuma alteracao.
O que muda e o que voce deve passar a usar: os nomes antigos entram em
depreciacao aqui e serao removidos na 2.0 (veja o aviso no fim desta
entrada).
Esta release nao tem spec congelado proprio: o alvo imutavel continua sendo
openapi-v1.1.0.json, o piso de
compatibilidade da serie 1.x — e e contra ele que o gate de CI mede tudo o
que a 1.2.0 acrescentou. Ver Versionamento.
O vocabulario novo
Cada valor monetario da API passa a ter um nome canonico ao lado do nome
historico. O nome longo ficou preciso: *_future_value e o que vale no
vencimento, *_present_value / net_present_* e o que vale hoje.
| Nome canonico (use este) | Nome agora deprecado |
|---|---|
gross_future_value | gross_face_value |
net_future_value | net_face_value |
requested_net_future_value | requested_advance_value |
present_value_discount | discounted_value · discount_brl |
net_present_liquid_value | liquid_value |
opr_gross_future_value | opr_gross_face_value |
opr_net_future_value | opr_net_face_value |
opr_present_value_discount | opr_discounted_value |
opr_net_present_liquid_value | opr_liquid_value |
A sigla NFV nao mudou de numero: era Net Face Value, agora se le Net Future Value, e o valor e o mesmo de sempre. De-para completo, com as duas pegadinhas, em Valores do Recebivel.
Campos NOVOS (sem equivalente anterior)
gross_future_value_deductions— os descontos ja aplicados ao lastro, agora DECLARADOS em vez de inferidos:gross_future_valuemenosnet_future_value. Ex.: NF-e de R$12.000 com R$1.500 de ISS e IRRF retidos na fonte e R$500 de glosa do sacado => bruto12000, deducoes2000, NFV10000. Numero gerencial: nao entra em nenhuma conta de desagio nem de pagamento. Aceito emPOST /v1/stock,PATCH /v1/stock/{item_id},POST /v1/simulateePOST /v1/operations/direct.requested_net_future_value_percent— o pedido em percentual do NFV (0 < p <= 100), alternativa ao valor absoluto em BRL. Resolucao:percent / 100 x net_future_value, arredondada a centavos sempre para baixo;100devolve o NFV exato, sem arredondamento — mais seguro que o valor absoluto quando se quer 100% de um NFV com centavo impar. Existe so emPOST /v1/simulateePOST /v1/operations/direct; as portas de/v1/stock/*antecipam sempre 100% dos itens e por isso nao o aceitam.
Uma troca de nome que NAO e 1:1
Usar gross_future_value exige gross_future_value_deductions no mesmo
payload (envie 0 quando nao houver deducoes), senao 422
DEDUCTIONS_REQUIRED_WITH_GROSS. O deprecado gross_face_value continua
aceito sozinho.
E deliberado: sob o nome canonico, a diferenca entre o bruto e o NFV passa a ser
declarada por voce, nunca inferida pela API. Se voce so quer renomear campos sem
mudar payload, troque net_face_value e requested_advance_value primeiro e
deixe o bruto para depois. O net_future_value segue sempre obrigatorio: a
API nunca deriva o NFV do bruto menos as deducoes.
Novos codigos 422 — a convivencia nunca e resolvida em silencio
| Codigo | Quando |
|---|---|
VOCABULARY_CONFLICT | O nome canonico e o deprecado do MESMO valor vieram com numeros diferentes. O pedido inteiro e recusado, sem efeito colateral — nada e escolhido por voce. Enviar so um dos nomes, ou os dois com o mesmo numero, nunca gera este erro |
DEDUCTIONS_REQUIRED_WITH_GROSS | gross_future_value usado sem gross_future_value_deductions |
VALUE_DECOMPOSITION_MISMATCH | Vieram os tres valores e a conta nao fecha: net == gross − deducoes, sem tolerancia de centavo |
REQUESTED_VALUE_AMBIGUOUS | O percentual veio junto do valor absoluto solicitado. Os dois sao mutuamente exclusivos |
Detalhe de cada um, com exemplo de detail, em
Codigos de Erro.
Webhooks
Os data dos eventos passam a trazer os dois nomes, lado a lado, com o mesmo
numero. O espelho e aplicado na emissao, entao um evento gerado ANTES desta
versao e reentregue na janela de re-tentativa (ate ~128 min) chega so com o
nome deprecado — o payload entregue e o que foi registrado na emissao. Por isso
o nome canonico nao e declarado obrigatorio no schema do webhook: trate a
ausencia dele caindo no nome deprecado. Ver Eventos.
Dentro do detail de um erro, os nomes seguem os DEPRECADOS
O espelhamento vale para os requests e para as respostas de sucesso. As
chaves e as mensagens dos detail de erro continuam citando net_face_value,
gross_face_value, requested_advance_value, discount_brl e
total_net_face_value — inclusive quando voce enviou o payload no vocabulario
canonico. Vale tambem para o loc de validation_error: omitir
requested_net_future_value devolve loc: ["requested_advance_value"]. Ao ler
um erro, procure o nome deprecado. A excecao sao os quatro codigos novos acima,
que nomeiam os campos canonicos em detail.fields.
SDKs
- Node/TypeScript (
@zemocapital/sdk): os tipos gerados do OpenAPI expoem os nomes canonicos em camelCase (grossFutureValue,netFutureValue,requestedNetFutureValue,oprNetPresentLiquidValue, ...) ao lado dos deprecados. Atencao no TypeScriptstrict: como a listarequireddo contrato ainda aponta os nomes ANTIGOS, um payload so-canonico nao compila — envie o par com o mesmo numero (os snippets do Quickstart ja fazem isso). Do lado da API os dois sao intercambiaveis: a obrigatoriedade e do tipo, nao do servidor. Isso se resolve sozinho na2.0. - Python (
zemo):zemo.Receivable,zemo.Stock.create,zemo.Stock.update,zemo.Stock.simulate_anticipationezemo.Stock.request_anticipationaceitam o vocabulario canonico. Os nomes antigos seguem aceitos.
Documentacao de campo mais explicita no POST /v1/operations/direct
Todo campo nao-deprecado de receivables[] passa a declarar, alem da definicao
formal, quando usar e um exemplo pratico de negocio; e a ordem de leitura na
Referencia Tecnica agora e a racional (identificacao -> vencimento
-> valores -> preco), com os nomes deprecados no fim. Ver
Operacao Direta.
Estes nomes serao removidos na 2.0
Os nomes da coluna direita da tabela acima estao deprecados a partir desta
versao. Neste produto, deprecar significa uma coisa so: o campo vale em toda
a serie 1.x e sai na proxima versao MAIOR. Nao ha remocao dentro da 1.x, e
nao ha data de corte publicada — a garantia e de versao, nao de calendario.
Coloque a migracao no seu backlog agora e conclua antes de adotar a 2.0. Ver
Versionamento.
v1.1.0 (2026-07-28) — Idempotencia real nas rotas de dinheiro, envelope 422 e floating_days
O spec desta versao esta congelado e versionado:
openapi-v1.1.0.json. Ele nao muda mais —
dentro da v1.x a API so cresce (adicoes), e um gate de CI recusa qualquer
remocao ou estreitamento. Ver Versionamento. O spec
sempre-vigente continua em openapi.json.
Idempotencia — novos codigos de erro e header
Endurecimento do middleware de idempotencia. Detalhe completo em Idempotencia.
422 idempotency_key_invalid— chave fora de 16 a 80 caracteres e recusada NA ENTRADA. O limite ja existia, mas so era aplicado na gravacao do cache, depois de a requisicao ter rodado: numa rota financeira a operacao era criada, o201voltava, a gravacao falhava em silencio, e um retry com a mesma chave duplicava a operacao. Agora o422sai antes de qualquer efeito, junto doidempotency_key_required. Gere sempre um UUID — nunca contadores, hashes truncados ou strings curtas.409 idempotency_key_in_flight— retry concorrente nunca executa duas vezes. A chave passa a ser reservada antes da execucao. Se a primeira chamada ainda estiver em andamento, a segunda espera por ela e devolve a mesma resposta; se a espera estourar, responde409 idempotency_key_in_flight. Este409nao e falha: re-tente com a mesma chave em instantes para receber a resposta original.409 idempotency_key_reused_on_different_route— a chave e por operacao, nao por sessao. Reutiliza-la em outra rota passa a ser recusado explicitamente.- Header
Idempotency-Replayed: truena resposta replayada — toda resposta servida do cache (sem reprocessar) passa a carrega-lo. Distinguir "executou agora" de "veio do cache" deixa de depender de heuristica sobre o corpo. - Cache isolado por
(chave, originador)tambem na ESCRITA. A leitura ja era isolada; a gravacao usava a chave sozinha, entao o segundo originador a usar a mesma string processava normalmente mas ficava sem protecao contra duplicidade. Isso acabou — e o replay nunca cruza originadores. - Abortar a conexao antes da resposta LIBERA a chave. Cancelamento do cliente (timeout curto,
Ctrl-C, conexao caida) solta a reserva: o retry com a MESMA chave executa de novo, porque nada foi concluido para replayar. E deliberado — o contrario travaria a chave sem ter resposta a devolver. Se seu cliente cancela requisicoes agressivamente, useexternal_idnas rotas que o aceitam.
Declaracoes antecipadas (nada muda no comportamento de hoje)
Duas coisas entram no contrato antes do congelamento, de proposito: declarar agora custa zero e faz a mudanca futura ser compativel; declarar depois, numa versao ja publicada, seria quebra de contrato.
503 idempotency_unavailabledeclarado nas rotas mutantes/v1/*que aceitamIdempotency-Key. Significa: o controle de idempotencia esta indisponivel e a requisicao NAO foi executada — re-tente com a MESMA chave, com backoff, sem gerar chave nova. Hoje este503nao e emitido: o modo fail-closed esta desligado, e uma indisponibilidade faz a requisicao seguir sem a protecao de deduplicacao (comportamento historico). A declaracao existe para que liga-lo no futuro nao seja uma mudanca incompativel. Trate o503desde ja e a mudanca nao te atinge. Ver Idempotencia.- Janela de uma chave em voo declarada como TETO de 600 segundos. No caso normal a chave e liberada assim que a execucao termina. Se o processo que a detem morrer sem libera-la (OOM,
kill -9, queda do container), ela pode responder409 idempotency_key_in_flightpor ate 600 segundos, ate a reserva expirar sozinha — depois disso o retry seguinte volta a executar, com seguranca. O numero e TETO, nao duracao garantida: pode ser encurtado sem aviso (encurtar e compativel); alargar exigiria versao nova. Sua conduta nao muda: continue re-tentando com a MESMA chave.
Novo campo — external_id (dedup sem depender do header)
external_idopcional (1 a 80 caracteres) emPOST /v1/discount-creditsePOST /v1/assignor-payables/{payable_id}/payments. Sao duas rotas que mexem em dinheiro e nao estao na lista de rotas financeiras (oIdempotency-Keysegue opcional nelas); o campo da a mesma garantia pelo corpo. Grao da unicidade:(originador, external_id)nos creditos de desconto,(payable, external_id)nos pagamentos. Um retry com o mesmo valor devolve o registro original comidempotent_replay: true— sem segundo debito, sem segundo credito. O valor e comparado literalmente, sem normalizacao de caixa ou de espacos ("NF-001","nf-001"e" NF-001"sao tres chaves diferentes): envie o mesmo valor byte a byte em todos os retries. Campo aditivo e opcional — omitindo-o nada muda: dois POSTs iguais criam dois registros, que e o correto para, por exemplo, dois pagamentos parciais legitimos de mesmo valor no mesmo dia.- Ressalva do write-off (so em pagamentos, e so no mecanismo do
external_id): depois de um write-off, o dedup porexternal_idresponde409 payable_already_settledem vez de replayar — o write-off resolve a divida por decisao contabil, nao por pagamento, e nao ha saldo honesto a devolver na resposta. Payable liquidado por pagamento replaya normalmente, inclusive o retry da propria chamada que o liquidou.
Mudança de contrato
POST /v1/simulatepassa a enforçar o tetorequested_advance_value <= net_face_value(BREAKING: request que retornava200pode passar a retornar422) — o teto sempre existiu emPOST /v1/operations/direct; a simulação cotava o payload acima do lastro e a recusa só aparecia na hora de criar. As duas portas agora leem a mesma validação e devolvem o mesmo422com o mesmodetail(a string legadaReceivable <external_id>: requested_advance_value (...) cannot exceed net_face_value (...)), reportando o primeiro recebível ofensor na ordem do payload. Antecipação parcial (requested < NFV) e de 100% (requested == NFV) não mudam: o guard é>. Nenhum valor é reprecificado — o que a simulação aceitava e o create também aceitava continua idêntico.- Deságio maior ou igual à base do recebível passa a ser recusado no fluxo direto — novo
422 RECEIVABLE_DISCOUNT_EXCEEDS_BASE— emPOST /v1/operations/directePOST /v1/simulate, recebível cujo deságio calculado alcance ou supere o seurequested_advance_value(líquido zero ou negativo) é recusado com422estruturado, nomeando cada ofensor emdetail.receivablescomexternal_id,requested_advance_valueediscounted_value. Acontece tipicamente comfixed_discount_brl, que é nominal e não escala com o valor do recebível (R$150 sobre uma base de R$100), ou comfees.total_liquid_value_brlpróximo de zero. Antes, o create respondia500(o título violava a checagem de valor no banco) e a simulação publicava líquido negativo como cotação válida. É o mesmo guard que as rotas de estoque já aplicam; a avaliação ocorre depois da redistribuição do alvo líquido, sobre o deságio final de cada recebível. Ver Códigos de Erro. POST /v1/simulatepassa a recusarfees.total_liquid_value_brlacima do face total (BREAKING: request que retornava200pode passar a retornar422) — o create já recusava com422 total_liquid_value_brl exceeds total face value; a simulação cotava, devolvendoopr_discounted_valuenegativo (alvo de99.999,00sobre um face de10.000,00produzia-89.999,00), como se a Zemo pagasse o deságio. Mesmodetaildo create.- As rotas do Portal (
/v1/portal/simulatee o create do Portal) herdam os três guards — elas usam o mesmo core das rotas de API, então o tetorequested_advance_value <= net_face_value, o alvo líquido acima do face e o deságio maior ou igual à base valem lá também. O veredito de faixa de taxa (policy_verdict) continua consultivo no Portal: essa parte segue respondendo200. O wizard do PCP precisa tratar422na chamada de simulação. - Base do deságio no fluxo de estoque passa a ser o NFV (BREAKING — muda VALORES) — em
POST /v1/stock/request-anticipationePOST /v1/stock/simulate-anticipation, o deságio incidia sobre ogross_face_valuedo item e passa a incidir sobre onet_face_value(Net Face Value). A diferençagross − NFVé informação gerencial (impostos na fonte e outros descontos já aplicados ao lastro) e deixa de entrar no cálculo. Efeito no extrato: item comgross_face_value12.000,00e NFV10.000,00a 2% a.m./30d passa de deságio240,00e PIX11.760,00para deságio200,00e PIX9.800,00— oopr_liquid_valuedeixa de poder superar o lastro (era 117,6% do NFV, agora 98,0%). Item comgross_face_value == net_face_valuenão muda em valor nenhum, e o dimensionamento feito antes do lançamento não encontrou operações de estoque vivas sujeitas à base antiga.fees.total_liquid_value_brltambém passa a ser rateado sobre o NFV, e um alvo líquido acima da soma dos NFV — que antes cabia no bruto — agora é422.opr_gross_face_valuecontinua reportando o bruto. Ver Valores do Recebível (NFV) e Taxas. - Base do gate de auto-aprovação passa a ser o NFV solicitado (BREAKING de comportamento) — em
POST /v1/stock/request-anticipation,auto_approve_limit_brlera comparado com o valor de face bruto da operação e passa a ser comparado com a soma donet_face_valuesolicitado na antecipação. No exemplo acima o gate deixa de medir12.000,00e passa a medir10.000,00: sob um limite de11.000,00, uma operação que ia para a fila do Backoffice passa a ser auto-aprovada (APPROVED_DIRECT); sob um limite de9.900,00ela continua escalando. A base não é o valor a desembolsar — se fosse, a mesma operação cruzaria ou não o teto de alçada conforme a taxa aplicada, e o limite deixaria de medir exposição. Se o seuauto_approve_limit_brlfoi dimensionado sobre o bruto, revise-o. Sem limite configurado nada muda: tudo continua indo para o Backoffice.POST /v1/operations/directnão tem gate de alçada — a criação direta nasce sempreWAITING_APPROVAL. opr_net_face_valuepassa a agregar o NFV nas quatro portas (BREAKING de semântica) — emPOST /v1/operations/directePOST /v1/simulateo campo somavarequested_advance_value(o valor antecipado) e passa a somarnet_face_value(o lastro), que já era o comportamento das duas portas de estoque. Efeito: só em antecipação parcial pelo fluxo direto — NFV10.000,00comrequested_advance_value6.000,00devolvia"6000.00"e passa a devolver"10000.00". Em antecipação de 100% (caso dominante) nada muda. Migração: quem usava o agregado como base do deságio deve somarrequested_advance_valueitem a item emreceivables[], que continua na resposta. Atenção a quem SOMAopr_net_face_valueentre operações: o campo passa a reportar o lastro INTEIRO, então uma antecipação parcial e a antecipação posterior do item-resto (_REMAINDER_) somam mais que a face do recebível de origem —10.000,00na primeira operação +4.000,00na segunda =14.000,00para um recebível de10.000,00. Para lastro consolidado, somenet_face_valuepor título (GET /v1/titles/{id}) e não o agregado por operação. Nenhum outroopr_*muda.requested_advance_valuedas rotas de estoque passa a ser validado — novo422 STOCK_REQUESTED_VALUE_MISMATCH(BREAKING: request que retornava201/200pode passar a retornar422) — o campo era obrigatório e nunca lido: pedir uma fração do lastro pelo estoque criava uma operação de 100% em silêncio. Agora ele tem que ser exatamente igual à soma dosnet_face_valuedos itens emstock_item_ids; qualquer outro valor (para mais ou para menos) é recusado com422, fail-closed (nada criado, nada consumido), comdetail.code,detail.requested_advance_valueedetail.total_net_face_value. Vale nas duas portas — o que a simulação recusa, o create recusa. Migração: envie a soma dos NFV dos itens selecionados; para antecipação parcial usePOST /v1/operations/direct. Ver Códigos de Erro.net_face_valuedo estoque passa a exigir> 0(BREAKING: request que retornava201/200pode passar a retornar422) — emPOST /v1/stockePATCH /v1/stock/{item_id}o campo aceitava0(ge=0) enquantoPOST /v1/operations/directePOST /v1/simulatejá exigiam> 0. Agora as quatro portas exigem> 0: um NFV zerado significa "nada antecipável" e, com a base do deságio no NFV, produziria antecipação de valor zero. O aperto vale também noPATCH— apertar só a criação deixaria oPATCHcomo bypass.net_face_valuemaior quegross_face_valuepassa a ser recusado (BREAKING: request que retornava201/200pode passar a retornar422) — o NFV é o valor de face bruto menos os descontos já aplicados ao lastro, então nunca o supera; até agora nenhuma porta cruzava os dois. Agora o invariante vale emPOST /v1/stockePOST /v1/operations/directePOST /v1/simulate(falha de schema,validation_error), emPATCH /v1/stock/{item_id}(422 STOCK_ITEM_NFV_ABOVE_GROSS, avaliado contra os valores resultantes do item) e nas duas rotas de antecipação do estoque, que também recusam item já cadastrado nesse estado.gross_face_valueé opcional no fluxo direto e, quando omitido, continua assumindo o próprio NFV. Novo422 STOCK_ITEM_DISCOUNT_EXCEEDS_BASEno mesmo escopo: item cujo deságio calculado fique maior ou igual ao seunet_face_value(líquido zero ou negativo) é recusado nas duas portas de estoque, em vez de a simulação publicar um líquido negativo. Ver Códigos de Erro.- Valores monetários com mais de 2 casas decimais passam a ser
422de schema (BREAKING: request que retornava201/200pode passar a retornar422) —requested_advance_valuedas duas rotas de antecipação do estoque ganhou a mesma restrição de escala quePOST /v1/stockjá tinha (decimal_places: 2). Somar valores em ponto flutuante no cliente e enviar o resultado como JSON number (10000.10 + 20000.20chega como30000.300000000003) agora falha na validação de schema, comdetail.errorsapontando o campo, em vez de virar um erro de regra de negócio. Envie o valor como string ou já arredondado a centavos. 429unificado num único shape (BREAKING para quem parseava o shape string) — os dois limites (por token e por origem) passam a responder com o mesmo corpo:detailé sempre um objeto comcode: "rate_limit_exceeded",scope("token"ou"ip"— qual bucket negou),limit_rpm,retry_after_secondsemessage. O limite por origem respondiadetailcomo a string"rate_limit_exceeded", comretry_after_secondsao lado dodetail; esse formato foi removido. Quem tratavadetailcomo string no429deve passar a lerdetail.code— um único parse agora serve os dois limites. Os headersRetry-AftereX-RateLimit-*não mudaram. Ver Rate Limits.422de schema passa a usar o envelope aninhado — falhas de validação de schema agora retornamdetailcomo objeto ({"code": "validation_error", "message": ..., "errors": [...]}), alinhado aos demais erros estruturados da API. O detalhe por campo (loc/msg/type) continua íntegro, agora emdetail.errors. Integrações que liam a lista direto dedetaildevem passar a lerdetail.errors; os SDKs oficiais aceitam os dois formatos e mantêmerr.fieldErrors/err.errors. Ver Códigos de Erro.- Replay de criação de token (
POST /v1/tokens) devolve201SEM osecret— osecreté de entrega única: só chega na primeira resposta. Um retry com o mesmoIdempotency-Keyreproduz o corpo com a chavesecretpresente porémnull(as demais chaves permanecem). Guarde osecretda primeira resposta; não conte com o replay para recuperá-lo.
Segurança
422das rotas de auth não ecoa mais o valor enviado — em/v1/auth/tokene/v1/auth/login, o campoinputde cada item dedetail.errorspassa a vir como"[REDACTED]". Antes, um erro de campo ausente devolvia o corpo inteiro eminput, incluindoclient_secret/password. O item continua trazendoloc,msg,typee octxde restrição — muda só o valor, e só nessas rotas; nas demais oinputsegue ecoando para depuração. Nenhum campo foi removido: quem lêinputcontinua recebendo uma string. Ver Códigos de Erro.
Correção de contrato
floating_dayspassa a somar no prazo cobrado — integrações que reproduzem o cálculo localmente devem usarvalor-base × (taxa mensal / 100) × (dias até o vencimento + floating_days) / 30. O valor-base énet_face_valueno estoque erequested_advance_valuena simulação/operação direta (ver a entrada sobre a base do deságio acima). A documentação estática anterior descrevia incorretamente esses dias como descontados do prazo.
v1.0.0 (2026-06-03) — GA: Motor de Taxas (PMP) e Limites de Valor
Melhorias
- Taxa variavel por prazo (PMP) — policies com
rate_scheduleagora tem a taxa mensal interpolada pelo Prazo Medio Ponderado da operacao.POST /v1/simulateretornaweighted_avg_dayse o objetopolicy_used(rate_from_schedule,schedule_day_used). product_idepolicy_idno/v1/simulate— filtre as policies por produto financeiro (product_id) ou force uma policy especifica do originador (policy_id).- Limites de valor por entidade — operacoes podem ser barradas (
403) se o valor total ficar fora da janela permitida por cedente / sacado / originador (regra most-restrictive-wins): novos codigosLIMIT_MAX_OPERATION_VALUEeLIMIT_MIN_OPERATION_VALUE(comsourceindicando a camada). - Validacao de CET — taxa efetiva fora do intervalo
[min, max]da policy retorna422 CET_OUT_OF_BOUNDS.
Documentacao
- Schemas OpenAPI enriquecidos com
examplese descricoes em todos os campos de request (/simulate,/operations/direct,/stock). - Pagina de Simulacao e Operacoes atualizadas com a hierarquia de taxas, PMP e limites.
v0.7.0 (2026-06-10) — Nomenclatura Consistente (BREAKING CHANGE)
Renomeacao de campos (breaking change)
Campos de request (recebiveis):
| Antes | Agora |
|---|---|
value | requested_advance_value |
total_asset_backing | net_face_value |
expected_date | due_date |
face_value (estoque) | gross_face_value |
expected_advance_value (estoque) | requested_advance_value |
gross_value_brl (estoque) | removido (usar gross_face_value) |
net_value_brl (estoque) | net_face_value |
expected_due_date (estoque) | due_date |
Novo campo: gross_face_value (opcional) — valor bruto do lastro. Se omitido, assume net_face_value.
Campos de response (operacao — prefixo opr_):
| Antes | Agora |
|---|---|
total_face_value_brl | opr_gross_face_value |
liquid_value_brl | opr_liquid_value |
total_discount_brl | opr_discounted_value |
original_value (por item) | requested_advance_value |
Novo campo: opr_net_face_value — soma dos net_face_value da operacao.
Novo endpoint
POST /v1/stock/simulate-anticipation— simular antecipacao a partir de itens do estoque (read-only, sem criar operacao)
v0.6.0 (2026-06-10) — Resiliencia e DX
Melhorias
- JWKS com retry e backoff — validacao de JWT agora resiliente a falhas de rede transientes (3 tentativas, timeout 10s)
- Dockerfile otimizado — base image migrada para ECR Public Gallery (builds mais rapidos e confiaveis)
- Pipeline CI/CD — SSM atualizado automaticamente apos migrations (go-live.yml)
Documentacao
- Response examples adicionados em todos os endpoints publicos
- Postman Collection disponivel para download
v0.5.0 (2026-05-25) — Cedentes e Dados Bancarios
Novos endpoints
POST /v1/assignors— cadastrar cedente explicitamente (dedup automatico por documento)PATCH /v1/assignors/{id}— atualizar dados do cedente
Melhorias
— ERRATA (2026-07-27): esta entrada nunca refletiu o comportamento entregue. O campobankagora e opcional emPOST /v1/operations/direct— omitir = PIX automatico via CPF/CNPJ do cedentebanksempre foi e continua sendo obrigatorio emPOST /v1/operations/direct; omiti-lo retorna422 validation_error. O que a v0.5.0 realmente entregou foi o conteudo debankpoder ser minimo: envie{"use_document_pix": true}(ou ate{}) para cair no PIX automatico via CPF/CNPJ do cedente. Ver Operacao Direta.bank.use_document_pix— forcar PIX via documento do cedente explicitamente- Nomenclatura padronizada: campo
fees(campotaxesaceito como alias para backward compat) - Dedup por CNPJ em
POST /v1/assignors— se documento ja existe para o originador, retorna200com_matched_by
v0.4.0 (2026-02-20) — Token Limits e Nomenclatura
Melhorias
- Token limits — tokens de API podem ter limites:
max_operation_value_brl,min_term_days,max_term_days. Operacoes que excedem o limite sao rejeitadas com403 - Renomeacao —
advance_valuerenomeado paraliquid_valueem toda a API (requests e responses). Campo antigo descontinuado external_refidempotente em originators e assignors
v0.3.0 (2026-02-07) — Hierarquia de Taxas
Melhorias
- Hierarquia de taxas 5 niveis: recebivel > operacao > cedente > sacado > policy do originador
- 3 modalidades cumulativas:
monthly_rate_pct(% mensal pro-rata),discount_pct(% desagio fixo),fixed_discount_brl(R$ nominal) - Simulacao agora aceita
assignor_documentpara aplicar defaults do cedente - Response models tipados em todos os endpoints publicos (melhora a documentacao OpenAPI/Swagger)
v0.2.0 (2026-06-02) — Fluxo Completo
Novos endpoints
POST /v1/stock/request-anticipation— solicitar antecipacao a partir do estoquePOST /v1/simulate— simular antecipacao sem persistirPOST /v1/operations/direct— criar operacao em uma unica chamadaGET /v1/originators/me— consultar dados do originador
Melhorias
- Contratos ZapSign com 223 variaveis parametrizadas
- Accrual diario de juros e multa no saldo devedor
v0.1.0 (2026-06-02) — Release Inicial
Endpoints
- Auth JWT + Client Credentials (X-Client-Id / X-Client-Secret) com protecao brute force
- CRUDs: sacados, cedentes, contas bancarias, estoque, operacoes, titulos
- Webhooks outbound HMAC-SHA256 com retry 7x
- Lifecycle completo de operacao: simulacao > criacao > aprovacao > contrato > pagamento PIX > conciliacao
Infraestrutura
- PostgreSQL 16, FastAPI, ECS Fargate
- Idempotencia obrigatoria em POSTs financeiros (
Idempotency-Key) - Tenant isolation por
originator_idem todas as queries