Idempotência no Lote/Carnê
Novo No lote/carnê, o Token de Controle existe em dois níveis: o batch.tokenControleUsuario protege e recupera o lote inteiro, e o tokenControleUsuario de cada item de boletos[] protege cada boleto — o mesmo padrão de campo da Idempotência do boleto individual, um nível acima no JSON. Como em toda a API V1, reenviar o mesmo token retorna 409 (não é replay de 201/200); a recuperação do estado é sempre pela consulta.
Dois Níveis de Token de Controle
Seção intitulada “Dois Níveis de Token de Controle”O mesmo campo existe em dois níveis — não os confunda:
| Por boleto | Por lote/carnê | |
|---|---|---|
| Campo | boleto.tokenControleUsuario (individual) ou tokenControleUsuario em cada item de boletos[] |
batch.tokenControleUsuario (um por requisição, fora de boletos[]) |
| Protege contra | Boleto duplicado | Lote inteiro duplicado |
| Recuperação | Consulta individual por controle | GET .../controle/{tokenControleUsuario}/situacao (esta página) |
| Documentação | Idempotência | Esta página |
Os dois níveis podem (e devem) ser usados juntos: o token do lote fecha o cenário de timeout da requisição; os tokens por boleto continuam correlacionando cada parcela com o seu sistema.
Regras que valem para os dois níveis — por conta e case-insensitive, sempre ecoando a grafia persistida:
- O namespace por boleto é um só: um token usado num boleto individual colide com o mesmo token num boleto de batch ou carnê (e vice-versa);
- O namespace do lote é separado do de título, mas compartilhado entre batch e carnê;
- Precedência: o conflito do token do lote é verificado antes da geração dos boletos — no reenvio de um lote inteiro, você recebe o
409de lote, não o do primeiro título; - Criação atômica: conflito em qualquer título impede o lote inteiro — nunca há lote parcialmente criado. Por isso o
404da consulta por token de controle significa “pode reenviar”.
O Problema: Timeout na Criação do Lote
Seção intitulada “O Problema: Timeout na Criação do Lote”Um POST /batch/boletos ou POST /carnes pode criar até 300 boletos de uma vez. Se a conexão cai ou o timeout estoura depois que a requisição chegou, seu sistema fica sem saber se o lote existe — e as duas reações ingênuas são ruins:
- Reenviar às cegas: risco de criar o lote em duplicidade (cobranças duplicadas em massa);
- Não reenviar: risco de não cobrar ninguém.
A Solução: Token de Controle no Lote + Consulta
Seção intitulada “A Solução: Token de Controle no Lote + Consulta”POST /batch/boletos (com batch.tokenControleUsuario) ──▶ timeout / falha de rede │ ▼GET /batch/boletos/controle/{tokenControleUsuario}/situacao │ ├── 200 ──▶ o lote FOI criado: estado completo reconstruído │ (token do lote, tokens e situação de cada boleto) │ └── 404 ──▶ o lote NÃO foi criado: reenvie o POST com segurançaA criação do lote é atômica: ou o lote existe inteiro, ou não existe. Por isso o 404 da consulta por token de controle é um sinal confiável de “pode reenviar”.
Especificação do Campo
Seção intitulada “Especificação do Campo”| Atributo | Valor |
|---|---|
| Campo | batch.tokenControleUsuario (nível do lote, fora de boletos[]) |
| Endpoints | POST /api/v1/batch/boletos e POST /api/v1/carnes |
| Tipo | string |
| Tamanho máximo | 44 caracteres |
| Caracteres permitidos | Alfanuméricos e - _ = |
| Unicidade | Por conta, sem diferenciar maiúsculas de minúsculas |
| Obrigatoriedade | Opcional — sem o campo, o comportamento atual não muda |
Formato inválido → 400 com o código estável A7C4B21E.
Echo no 201
Seção intitulada “Echo no 201”Quando informado, o token de controle é ecoado na resposta de criação — sempre presente no envelope (null quando não usado):
{ "batch": { "token": "vSD9nA6iTFXhTComVKF-cQoY9Wq5RJkuJoTLPje8zVE=", "tokenControleUsuario": "pedido-2026-0117", "total": 2, "boletos": [ "token-do-boleto-1", "token-do-boleto-2" ], "titulos": [ { "token": "token-do-boleto-1", "numero": "12345678901", "documento": "PED-1", "codigoDeBarras": "23791...", "linhaDigitavel": "23790.50004 ...", "pixCodigoEMV": null, "tokenControleUsuario": "pedido-2026-0117-1" }, { "token": "token-do-boleto-2", "numero": "12345678902", "documento": "PED-2", "codigoDeBarras": "23792...", "linhaDigitavel": "23790.50004 ...", "pixCodigoEMV": null, "tokenControleUsuario": "pedido-2026-0117-2" } ] }}total é a quantidade de boletos criados (sempre igual ao tamanho de boletos[] e de titulos[]); boletos é a lista com o token (string) de cada boleto criado, alinhada por índice com titulos[], que resume cada boleto (inclusive o tokenControleUsuario por boleto, que é o outro nível). O restante do 201 segue o contrato das páginas de criação.
409 — Token de Controle do Lote Já Usado
Seção intitulada “409 — Token de Controle do Lote Já Usado”O reenvio de um token de controle já usado retorna 409 identificando o lote existente (nunca um boleto):
{ "erro": { "status": 409, "tipo": "conflito", "causas": [ { "codigo": "5C1D9F04", "mensagem": "Campo (batch.tokenControleUsuario) - Conflito de dados, já existe um lote de boletos com a mesma chave de controle (pedido-2026-0117), localizado em (/api/v1/batch/boletos/vSD9nA6iTFXhTComVKF-cQoY9Wq5RJkuJoTLPje8zVE=/situacao).", "suporte": "https://developers.boleto.cloud" }, { "codigo": "5C1DA015", "mensagem": "Campo(batch.token):Conteúdo(vSD9nA6iTFXhTComVKF-cQoY9Wq5RJkuJoTLPje8zVE=)", "suporte": "https://developers.boleto.cloud" }, { "codigo": "5C1DA015", "mensagem": "Campo(batch.tokenControleUsuario):Conteúdo(pedido-2026-0117)", "suporte": "https://developers.boleto.cloud" } ] }}Headers do 409
Seção intitulada “Headers do 409”| Header | Descrição |
|---|---|
Location |
Rota canônica de situação do lote existente, decidida pelo tipo dele: /api/v1/carnes/{token}/situacao se for carnê, /api/v1/batch/boletos/{token}/situacao se for batch |
X-BoletoCloud-Token |
Token do lote existente |
X-BoletoCloud-Token-Controle-Usuario |
O token de controle, na grafia persistida |
Não há header de nosso número: o conflito é de lote, que não tem um boleto único de referência.
409 por Boleto dentro do Lote
Seção intitulada “409 por Boleto dentro do Lote”Se o conflito for de um título (o tokenControleUsuario de um item de boletos[] já usado), o 409 identifica o boleto existente — mesmo molde do individual — e a criação do lote inteiro é abortada (atomicidade). Compare os dois conflitos:
| 409 de boleto | 409 de lote | |
|---|---|---|
| Identifica | O boleto existente | O lote existente |
| Headers | X-BoletoCloud-Token + X-BoletoCloud-NIB-Nosso-Numero + X-BoletoCloud-Token-Controle-Usuario |
X-BoletoCloud-Token + X-BoletoCloud-Token-Controle-Usuario (sem nosso número) |
Location |
O boleto individual existente | A situação canônica do lote existente (batch ou carnê, pelo tipo dele) |
| Recuperação | Situação individual | GET .../controle/{tokenControleUsuario}/situacao |
Consultar a Situação pelo Token de Controle
Seção intitulada “Consultar a Situação pelo Token de Controle”GET https://sandbox.boletocloud.com/api/v1/batch/boletos/controle/{tokenControleUsuario}/situacaoGET https://sandbox.boletocloud.com/api/v1/carnes/controle/{tokenControleUsuario}/situacao- Mesma resposta, semânticas e autenticação da consulta de situação por token — envelope
batchcomtoken,tokenControleUsuario,total,boletos[]etitulos[]; - Use a rota da família em que o lote foi criado (batch ou carnê) — a canônica;
- O valor na URL resolve em qualquer caixa; o envelope ecoa a grafia persistida;
404= o token de controle não existe para a sua conta → pode reenviar o POST com segurança.
Exemplo de 404 (token nunca usado):
{ "erro": { "status": 404, "tipo": "requisicao_para_recurso_nao_encontrado", "causas": [ { "codigo": "7B41C2E9", "mensagem": "Lote de boletos com token (pedido-2026-0117) não encontrado.", "suporte": "https://developers.boleto.cloud" } ] }}Na rota de carnê, o mesmo 404 usa o código 3F8A5D16 com a mensagem análoga (Carnê com token (...) não encontrado.) — a semântica “pode reenviar” é idêntica; se você trata códigos absolutos, trate o par.
Receita de Recuperação
Seção intitulada “Receita de Recuperação”# 1) Criar o lote com o token de controlecurl -v "https://sandbox.boletocloud.com/api/v1/batch/boletos" \ -H "Content-Type: application/json; charset=utf-8" \ -u "api-key_SUA-API-KEY:token" \ -d '{"batch": {"tokenControleUsuario": "pedido-2026-0117", "boletos": [ ... ]}}'
# 2) Em timeout/queda de rede, consultar pelo token de controle:curl -v "https://sandbox.boletocloud.com/api/v1/batch/boletos/controle/pedido-2026-0117/situacao" \ -H "Accept: application/json" \ -u "api-key_SUA-API-KEY:token"# 200 -> lote existe (token no corpo) | 404 -> reenviar o POSTResponse criacao = ClientBuilder.newClient() .target("https://sandbox.boletocloud.com/api/v1/batch/boletos") .register(HttpAuthenticationFeature.basic("api-key_SUA-API-KEY", "token")) .request(APPLICATION_JSON) .post(Entity.json(jsonComTokenControle)); // batch.tokenControleUsuario preenchido
if (criacao.getStatus() == 409) { // Lote já existe: siga a rota canônica informada String location = criacao.getHeaderString("Location"); String tokenLote = criacao.getHeaderString("X-BoletoCloud-Token"); System.out.println("Lote existente: " + tokenLote + " -> " + location);}
// Recuperação após timeout:Response consulta = ClientBuilder.newClient() .target("https://sandbox.boletocloud.com/api/v1/batch/boletos") .path("/controle/pedido-2026-0117/situacao") .register(HttpAuthenticationFeature.basic("api-key_SUA-API-KEY", "token")) .request(APPLICATION_JSON) .get();// 200 = lote criado; 404 = reenviar o POSTval consulta = Request.Builder() .url("https://sandbox.boletocloud.com/api/v1/batch/boletos/controle/pedido-2026-0117/situacao") .header("Authorization", Credentials.basic("api-key_SUA-API-KEY", "token")) .header("Accept", "application/json") .build()
client.newCall(consulta).execute().use { response -> when (response.code) { 200 -> println("Lote existe: ${response.body?.string()}") 404 -> println("Lote não criado — reenviar o POST") else -> println("Erro: ${response.code}") }}var response = await client.GetAsync( "https://sandbox.boletocloud.com/api/v1/batch/boletos/controle/pedido-2026-0117/situacao");
if ((int)response.StatusCode == 200) { Console.WriteLine("Lote existe — estado reconstruído");} else if ((int)response.StatusCode == 404) { Console.WriteLine("Lote não criado — reenviar o POST");}const https = require('https');
const tokenControle = 'pedido-2026-0117';const options = { hostname: 'sandbox.boletocloud.com', path: `/api/v1/batch/boletos/controle/${tokenControle}/situacao`, method: 'GET', auth: 'api-key_SUA-API-KEY:token', headers: {'Accept': 'application/json'}};
https.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { if (res.statusCode === 200) { const { batch } = JSON.parse(data); console.log(`Lote existe: ${batch.token} (${batch.total} títulos)`); } else if (res.statusCode === 404) { console.log('Lote não criado — reenviar o POST'); } });}).end();tokenControle := "pedido-2026-0117"req, _ := http.NewRequest("GET", "https://sandbox.boletocloud.com/api/v1/batch/boletos/controle/"+tokenControle+"/situacao", nil)req.SetBasicAuth("api-key_SUA-API-KEY", "token")req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)if err != nil { panic(err) }defer resp.Body.Close()
switch resp.StatusCode {case 200: fmt.Println("Lote existe — estado reconstruído")case 404: fmt.Println("Lote não criado — reenviar o POST")}<?php$tokenControle = 'pedido-2026-0117';$ch = curl_init();curl_setopt($ch, CURLOPT_URL, "https://sandbox.boletocloud.com/api/v1/batch/boletos/controle/$tokenControle/situacao");curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);curl_setopt($ch, CURLOPT_USERPWD, 'api-key_SUA-API-KEY:token');curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);curl_close($ch);
if ($http_code == 200) { $batch = json_decode($body, true)['batch']; echo "Lote existe: {$batch['token']} ({$batch['total']} títulos)\n";} elseif ($http_code == 404) { echo "Lote não criado — reenviar o POST\n";}?>import requestsfrom requests.auth import HTTPBasicAuth
token_controle = 'pedido-2026-0117'response = requests.get( f'https://sandbox.boletocloud.com/api/v1/batch/boletos/controle/{token_controle}/situacao', auth=HTTPBasicAuth('api-key_SUA-API-KEY', 'token'), headers={'Accept': 'application/json'})
if response.status_code == 200: batch = response.json()['batch'] print(f"Lote existe: {batch['token']} ({batch['total']} títulos)")elif response.status_code == 404: print('Lote não criado — reenviar o POST')require 'net/http'require 'uri'require 'json'
token_controle = 'pedido-2026-0117'uri = URI("https://sandbox.boletocloud.com/api/v1/batch/boletos/controle/#{token_controle}/situacao")
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| request = Net::HTTP::Get.new(uri) request.basic_auth('api-key_SUA-API-KEY', 'token') request['Accept'] = 'application/json'
response = http.request(request) case response.code when '200' batch = JSON.parse(response.body)['batch'] puts "Lote existe: #{batch['token']} (#{batch['total']} títulos)" when '404' puts 'Lote não criado — reenviar o POST' endend(require '[clj-http.client :as client])
(let [token-controle "pedido-2026-0117" response (client/get (str "https://sandbox.boletocloud.com/api/v1/batch/boletos/controle/" token-controle "/situacao") {:basic-auth ["api-key_SUA-API-KEY" "token"] :accept :json :as :json :throw-exceptions false})] (case (:status response) 200 (println "Lote existe:" (get-in response [:body :batch :token])) 404 (println "Lote não criado — reenviar o POST") (println "Erro:" (:status response))))Boas Práticas
Seção intitulada “Boas Práticas”| Prática | Motivo |
|---|---|
| Use um identificador do seu sistema (nº do pedido, contrato, fatura) | Rastreabilidade sem armazenar tokens antes da resposta |
| Um token de controle por lote, nunca reaproveitado | Reuso com conteúdo diferente resulta em 409 — é erro de integração |
| Grave o token de controle antes de enviar o POST | É ele que permite recuperar depois de um timeout |
Trate o 409 de lote como “o lote já existe” (siga o Location e reconcilie); o 409 de boleto significa que um boleto já existia e o lote novo não foi criado |
Discrimine pelo shape — não é falha |
| Combine com o token de controle por boleto | Cada nível protege contra uma duplicidade diferente |
Códigos de Resposta
Seção intitulada “Códigos de Resposta”| Código | Onde | Descrição |
|---|---|---|
201 Created |
POST | Lote criado; token de controle ecoado no envelope |
409 Conflict |
POST | Token de controle já usado nesta conta (5C1D9F04/5C1DA015) — lote existente identificado no corpo e headers |
400 Bad Request |
POST e GET | Token de controle fora do formato (A7C4B21E no POST) |
200 OK |
GET /controle |
Lote encontrado; mesma resposta da situação agregada |
404 Not Found |
GET /controle |
Token de controle não existe para a sua conta → pode reenviar o POST |
403 Forbidden |
GET /controle |
Conta sem o plano personalizado ativo |
