Pular para o conteúdo principal

Changelog

Historico de mudancas que afetam a integracao de API dos originadores.

Como ler esta pagina

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:

  1. o teto da credencial de API acaba — quem limita valor por operação passa a ser só a policy;
  2. 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 detail do 403 ganha o campo limit_origin.
O que te alcança agora, e o que depende de configuração

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_anchor fora do padrão both. 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.

O que muda para voce

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 sumiuO que ele recusava
LIMIT_MIN_TERM_DAYSrecebível com prazo menor que o mínimo da credencial
LIMIT_MAX_TERM_DAYSrecebí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​

O que muda para voce

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 enteO que vale
valor > 0o valor do vínculo, mesmo que seja menor que o default da policy
valor 0bloqueio daquele ente — mesmo que a policy tenha default folgado
vazio / nullherda 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 0trava: nada passa por aquele entesem limite por ali
teto > 0teto normalteto normal
limite próprio no entemanda sempremanda 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.

AntesAgora
403 LIMIT_ENTITY_CONFIG_MISSING, com detail.missing_entity_scopes listando os entes descobertos403 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: both incluí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 um 0 no 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_anchor fora de both — 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".

O que muda para voce

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.

As duas aprovações: A1 (do cliente) e A2 (da Zemo)

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.

O que muda para voce

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_credentials sempre 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_locked só quando a senha está correta e a conta está bloqueada (5 tentativas falhadas seguidas ⇒ bloqueio de 30 minutos).
Precisão sobre "indistinguíveis"

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​

O que muda para voce

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.

Se voce recebia LIMIT_MAX_OPERATION_VALUE numa policy so com a tese

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

Efeito pratico

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_value nas duas portas (o valor antecipado, em moeda de Net Future Value). A prosa dizia "bruto no fluxo de estoque". Vigente desde a 1.2.0; ver Valores do Recebivel.
  • pre_authorized do 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 um 409 stock_item_<id>_not_pre_authorized ao antecipar o resto — libere com PATCH /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​

O que muda para voce

Duas coisas, e a primeira vale mesmo que voce nao use nada novo:

  1. 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.
  2. 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.

Se voce disparava o contrato por conta propria

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​

EndpointPara que serve
POST /v1/operations/{id}/contract/send-for-signatureDispara o contrato (re-tentativa ou modo embedded) e devolve as sign_url
GET /v1/operations/{id}/contract/signersLe 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-Key devolve 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 em GET /v1/operations/{id}/contract/signers.
  • Sem watchdog no modo embedded: com embedded_signature: true nenhuma notificacao sai, nem lembretes. Se voce nunca apresentar a sign_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​

Nada quebra nesta versao

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_valuegross_face_value
net_future_valuenet_face_value
requested_net_future_valuerequested_advance_value
present_value_discountdiscounted_value · discount_brl
net_present_liquid_valueliquid_value
opr_gross_future_valueopr_gross_face_value
opr_net_future_valueopr_net_face_value
opr_present_value_discountopr_discounted_value
opr_net_present_liquid_valueopr_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_value menos net_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 => bruto 12000, deducoes 2000, NFV 10000. Numero gerencial: nao entra em nenhuma conta de desagio nem de pagamento. Aceito em POST /v1/stock, PATCH /v1/stock/{item_id}, POST /v1/simulate e POST /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; 100 devolve o NFV exato, sem arredondamento — mais seguro que o valor absoluto quando se quer 100% de um NFV com centavo impar. Existe so em POST /v1/simulate e POST /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​

CodigoQuando
VOCABULARY_CONFLICTO 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_GROSSgross_future_value usado sem gross_future_value_deductions
VALUE_DECOMPOSITION_MISMATCHVieram os tres valores e a conta nao fecha: net == gross − deducoes, sem tolerancia de centavo
REQUESTED_VALUE_AMBIGUOUSO 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 TypeScript strict: como a lista required do 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 na 2.0.
  • Python (zemo): zemo.Receivable, zemo.Stock.create, zemo.Stock.update, zemo.Stock.simulate_anticipation e zemo.Stock.request_anticipation aceitam 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​

Versao com contrato congelado

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, o 201 voltava, a gravacao falhava em silencio, e um retry com a mesma chave duplicava a operacao. Agora o 422 sai antes de qualquer efeito, junto do idempotency_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, responde 409 idempotency_key_in_flight. Este 409 nao 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: true na 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, use external_id nas 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_unavailable declarado nas rotas mutantes /v1/* que aceitam Idempotency-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 este 503 nao 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 o 503 desde 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 responder 409 idempotency_key_in_flight por 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_id opcional (1 a 80 caracteres) em POST /v1/discount-credits e POST /v1/assignor-payables/{payable_id}/payments. Sao duas rotas que mexem em dinheiro e nao estao na lista de rotas financeiras (o Idempotency-Key segue 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 com idempotent_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 por external_id responde 409 payable_already_settled em 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/simulate passa a enforçar o teto requested_advance_value <= net_face_value (BREAKING: request que retornava 200 pode passar a retornar 422) — o teto sempre existiu em POST /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 mesmo 422 com o mesmo detail (a string legada Receivable <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 — em POST /v1/operations/direct e POST /v1/simulate, recebível cujo deságio calculado alcance ou supere o seu requested_advance_value (líquido zero ou negativo) é recusado com 422 estruturado, nomeando cada ofensor em detail.receivables com external_id, requested_advance_value e discounted_value. Acontece tipicamente com fixed_discount_brl, que é nominal e não escala com o valor do recebível (R$150 sobre uma base de R$100), ou com fees.total_liquid_value_brl próximo de zero. Antes, o create respondia 500 (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/simulate passa a recusar fees.total_liquid_value_brl acima do face total (BREAKING: request que retornava 200 pode passar a retornar 422) — o create já recusava com 422 total_liquid_value_brl exceeds total face value; a simulação cotava, devolvendo opr_discounted_value negativo (alvo de 99.999,00 sobre um face de 10.000,00 produzia -89.999,00), como se a Zemo pagasse o deságio. Mesmo detail do create.
  • As rotas do Portal (/v1/portal/simulate e o create do Portal) herdam os três guards — elas usam o mesmo core das rotas de API, então o teto requested_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 respondendo 200. O wizard do PCP precisa tratar 422 na 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-anticipation e POST /v1/stock/simulate-anticipation, o deságio incidia sobre o gross_face_value do item e passa a incidir sobre o net_face_value (Net Face Value). A diferença gross − 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 com gross_face_value 12.000,00 e NFV 10.000,00 a 2% a.m./30d passa de deságio 240,00 e PIX 11.760,00 para deságio 200,00 e PIX 9.800,00 — o opr_liquid_value deixa de poder superar o lastro (era 117,6% do NFV, agora 98,0%). Item com gross_face_value == net_face_value nã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_brl també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_value continua 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_brl era comparado com o valor de face bruto da operação e passa a ser comparado com a soma do net_face_value solicitado na antecipação. No exemplo acima o gate deixa de medir 12.000,00 e passa a medir 10.000,00: sob um limite de 11.000,00, uma operação que ia para a fila do Backoffice passa a ser auto-aprovada (APPROVED_DIRECT); sob um limite de 9.900,00 ela 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 seu auto_approve_limit_brl foi dimensionado sobre o bruto, revise-o. Sem limite configurado nada muda: tudo continua indo para o Backoffice. POST /v1/operations/direct não tem gate de alçada — a criação direta nasce sempre WAITING_APPROVAL.
  • opr_net_face_value passa a agregar o NFV nas quatro portas (BREAKING de semântica) — em POST /v1/operations/direct e POST /v1/simulate o campo somava requested_advance_value (o valor antecipado) e passa a somar net_face_value (o lastro), que já era o comportamento das duas portas de estoque. Efeito: só em antecipação parcial pelo fluxo direto — NFV 10.000,00 com requested_advance_value 6.000,00 devolvia "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 somar requested_advance_value item a item em receivables[], que continua na resposta. Atenção a quem SOMA opr_net_face_value entre 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,00 na primeira operação + 4.000,00 na segunda = 14.000,00 para um recebível de 10.000,00. Para lastro consolidado, some net_face_value por título (GET /v1/titles/{id}) e não o agregado por operação. Nenhum outro opr_* muda.
  • requested_advance_value das rotas de estoque passa a ser validado — novo 422 STOCK_REQUESTED_VALUE_MISMATCH (BREAKING: request que retornava 201/200 pode passar a retornar 422) — 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 dos net_face_value dos itens em stock_item_ids; qualquer outro valor (para mais ou para menos) é recusado com 422, fail-closed (nada criado, nada consumido), com detail.code, detail.requested_advance_value e detail.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 use POST /v1/operations/direct. Ver Códigos de Erro.
  • net_face_value do estoque passa a exigir > 0 (BREAKING: request que retornava 201/200 pode passar a retornar 422) — em POST /v1/stock e PATCH /v1/stock/{item_id} o campo aceitava 0 (ge=0) enquanto POST /v1/operations/direct e POST /v1/simulate já 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 no PATCH — apertar só a criação deixaria o PATCH como bypass.
  • net_face_value maior que gross_face_value passa a ser recusado (BREAKING: request que retornava 201/200 pode passar a retornar 422) — 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 em POST /v1/stock e POST /v1/operations/direct e POST /v1/simulate (falha de schema, validation_error), em PATCH /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. Novo 422 STOCK_ITEM_DISCOUNT_EXCEEDS_BASE no mesmo escopo: item cujo deságio calculado fique maior ou igual ao seu net_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 422 de schema (BREAKING: request que retornava 201/200 pode passar a retornar 422) — requested_advance_value das duas rotas de antecipação do estoque ganhou a mesma restrição de escala que POST /v1/stock já tinha (decimal_places: 2). Somar valores em ponto flutuante no cliente e enviar o resultado como JSON number (10000.10 + 20000.20 chega como 30000.300000000003) agora falha na validação de schema, com detail.errors apontando o campo, em vez de virar um erro de regra de negócio. Envie o valor como string ou já arredondado a centavos.
  • 429 unificado 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 com code: "rate_limit_exceeded", scope ("token" ou "ip" — qual bucket negou), limit_rpm, retry_after_seconds e message. O limite por origem respondia detail como a string "rate_limit_exceeded", com retry_after_seconds ao lado do detail; esse formato foi removido. Quem tratava detail como string no 429 deve passar a ler detail.code — um único parse agora serve os dois limites. Os headers Retry-After e X-RateLimit-* não mudaram. Ver Rate Limits.
  • 422 de schema passa a usar o envelope aninhado — falhas de validação de schema agora retornam detail como objeto ({"code": "validation_error", "message": ..., "errors": [...]}), alinhado aos demais erros estruturados da API. O detalhe por campo (loc/msg/type) continua íntegro, agora em detail.errors. Integrações que liam a lista direto de detail devem passar a ler detail.errors; os SDKs oficiais aceitam os dois formatos e mantêm err.fieldErrors / err.errors. Ver Códigos de Erro.
  • Replay de criação de token (POST /v1/tokens) devolve 201 SEM o secret — o secret é de entrega única: só chega na primeira resposta. Um retry com o mesmo Idempotency-Key reproduz o corpo com a chave secret presente porém null (as demais chaves permanecem). Guarde o secret da primeira resposta; não conte com o replay para recuperá-lo.

Segurança​

  • 422 das rotas de auth não ecoa mais o valor enviado — em /v1/auth/token e /v1/auth/login, o campo input de cada item de detail.errors passa a vir como "[REDACTED]". Antes, um erro de campo ausente devolvia o corpo inteiro em input, incluindo client_secret / password. O item continua trazendo loc, msg, type e o ctx de restrição — muda só o valor, e só nessas rotas; nas demais o input segue ecoando para depuração. Nenhum campo foi removido: quem lê input continua recebendo uma string. Ver Códigos de Erro.

Correção de contrato​

  • floating_days passa a somar no prazo cobrado — integrações que reproduzem o cálculo localmente devem usar valor-base × (taxa mensal / 100) × (dias até o vencimento + floating_days) / 30. O valor-base é net_face_value no estoque e requested_advance_value na 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_schedule agora tem a taxa mensal interpolada pelo Prazo Medio Ponderado da operacao. POST /v1/simulate retorna weighted_avg_days e o objeto policy_used (rate_from_schedule, schedule_day_used).
  • product_id e policy_id no /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 codigos LIMIT_MAX_OPERATION_VALUE e LIMIT_MIN_OPERATION_VALUE (com source indicando a camada).
  • Validacao de CET — taxa efetiva fora do intervalo [min, max] da policy retorna 422 CET_OUT_OF_BOUNDS.

Documentacao​

  • Schemas OpenAPI enriquecidos com examples e 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):

AntesAgora
valuerequested_advance_value
total_asset_backingnet_face_value
expected_datedue_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_):

AntesAgora
total_face_value_brlopr_gross_face_value
liquid_value_brlopr_liquid_value
total_discount_brlopr_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​

  • bank agora e opcional em POST /v1/operations/direct — omitir = PIX automatico via CPF/CNPJ do cedente — ERRATA (2026-07-27): esta entrada nunca refletiu o comportamento entregue. O campo bank sempre foi e continua sendo obrigatorio em POST /v1/operations/direct; omiti-lo retorna 422 validation_error. O que a v0.5.0 realmente entregou foi o conteudo de bank poder 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 (campo taxes aceito como alias para backward compat)
  • Dedup por CNPJ em POST /v1/assignors — se documento ja existe para o originador, retorna 200 com _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 com 403
  • Renomeacao — advance_value renomeado para liquid_value em toda a API (requests e responses). Campo antigo descontinuado
  • external_ref idempotente 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_document para 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 estoque
  • POST /v1/simulate — simular antecipacao sem persistir
  • POST /v1/operations/direct — criar operacao em uma unica chamada
  • GET /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_id em todas as queries