Idempotência - Evitar Duplicados
Idempotência na Criação de Boletos
Seção intitulada “Idempotência na Criação de Boletos”Para evitar a criação de boletos duplicados em cenários de retry, falhas de rede ou requisições concorrentes, a API Boleto Cloud suporta o padrão de idempotência através do campo boleto.tokenControleUsuario.
O Problema: Boletos Duplicados
Seção intitulada “O Problema: Boletos Duplicados”Em integrações via API, situações como timeout de rede, erros de conexão ou processamento lento podem fazer com que sua aplicação reenvie a mesma requisição. Sem um mecanismo de proteção, isso pode resultar em:
- Boletos duplicados para o mesmo pagador
- Cobranças indevidas
- Inconsistência entre seu sistema e o Boleto Cloud
A Solução: Token de Controle
Seção intitulada “A Solução: Token de Controle”O tokenControleUsuario é um identificador único definido por você que garante que cada boleto seja criado apenas uma vez.
Como Funciona
Seção intitulada “Como Funciona”┌─────────────────────────────────────────────────────────────────────────┐│ FLUXO DE IDEMPOTÊNCIA │├─────────────────────────────────────────────────────────────────────────┤│ ││ 1ª Requisição 2ª Requisição (retry) ││ tokenControleUsuario="pedido-123" tokenControleUsuario="pedido-123"│ │ │ ││ ▼ ▼ ││ ┌─────────────────┐ ┌─────────────────┐ ││ │ API verifica: │ │ API verifica: │ ││ │ Token existe? │ │ Token existe? │ ││ └────────┬────────┘ └────────┬────────┘ ││ │ NÃO │ SIM ││ ▼ ▼ ││ ┌─────────────────┐ ┌─────────────────┐ ││ │ Cria o boleto │ │ Retorna erro │ ││ │ Retorna 201 │ │ 409 Conflict │ ││ └─────────────────┘ └─────────────────┘ ││ │└─────────────────────────────────────────────────────────────────────────┘- Você envia uma requisição com um
tokenControleUsuarioúnico - A API verifica se esse token já foi utilizado
- Se não existir: cria o boleto e retorna
201 Created - Se já existir: retorna
409 Conflict, indicando que o boleto já foi criado
Especificação do Campo
Seção intitulada “Especificação do Campo”| Atributo | Valor |
|---|---|
| Campo | boleto.tokenControleUsuario |
| Tipo | string |
| Tamanho máximo | 44 caracteres |
| Caracteres permitidos | Alfanuméricos e \ - _ = |
| Unicidade | Obrigatória (único por boleto) |
| Obrigatoriedade | Opcional, mas altamente recomendado |
Exemplos de Uso
Seção intitulada “Exemplos de Uso”Requisição Individual (POST /boletos)
Seção intitulada “Requisição Individual (POST /boletos)”curl -v "https://sandbox.boletocloud.com/api/v1/boletos" \ -H "Content-Type: application/x-www-form-urlencoded; charset=utf-8" \ -X "POST" \ -u "api-key_SUA-API-KEY:token" \ -d boleto.conta.token="api-key_SEU-TOKEN-DA-CONTA" \ -d boleto.tokenControleUsuario="pedido-12345-abc" \ -d boleto.pagador.nome="Alberto Santos Dumont" \ ...Requisição em Lote (Batch/Carnês)
Seção intitulada “Requisição em Lote (Batch/Carnês)”{ "batch": { "boletos": [ { "tokenControleUsuario": "pedido-12345-parcela-1", "conta": { "token": "api-key_SEU-TOKEN-DA-CONTA" }, "pagador": { "nome": "Alberto Santos Dumont", ... }, "vencimento": "2024-02-15", "valor": 500.00 }, { "tokenControleUsuario": "pedido-12345-parcela-2", "conta": { "token": "api-key_SEU-TOKEN-DA-CONTA" }, "pagador": { "nome": "Alberto Santos Dumont", ... }, "vencimento": "2024-03-15", "valor": 500.00 } ] }}Boas Práticas
Seção intitulada “Boas Práticas”Use identificadores do seu sistema
Seção intitulada “Use identificadores do seu sistema”Exemplos de tokens recomendados:
| Cenário | Exemplo de Token |
|---|---|
| Pedido de e-commerce | pedido-12345 |
| Fatura mensal | fatura-2024-01-cliente-789 |
| Parcela de carnê | contrato-456-parcela-3 |
| UUID gerado | 550e8400-e29b-41d4-a716-446655440000 |
Trate o erro 409 Conflict
Seção intitulada “Trate o erro 409 Conflict”Quando receber 409 Conflict, significa que o boleto já foi criado. A API retorna nos headers todas as informações necessárias para identificar o boleto existente:
Headers Retornados no Erro 409
Seção intitulada “Headers Retornados no Erro 409”| Header | Descrição | Exemplo |
|---|---|---|
X-BoletoCloud-Token | Token do boleto existente | abc123def456... |
X-BoletoCloud-NIB-Nosso-Numero | Nosso Número do boleto | 00000012345-6 |
X-BoletoCloud-Token-Controle-Usuario | Token de controle enviado | pedido-12345-abc |
Location | Path para acessar o boleto | /api/v1/boletos/abc123def456 |
Exemplos de Tratamento
Seção intitulada “Exemplos de Tratamento”# Ao usar -v (verbose), os headers são exibidos na saída# Em caso de 409 Conflict, a resposta incluirá:## < HTTP/1.1 409 Conflict# < X-BoletoCloud-Token: abc123def456...# < X-BoletoCloud-NIB-Nosso-Numero: 00000012345-6# < X-BoletoCloud-Token-Controle-Usuario: pedido-12345-abc# < Location: /api/v1/boletos/abc123def456
curl -v "https://sandbox.boletocloud.com/api/v1/boletos" \ -H "Content-Type: application/x-www-form-urlencoded; charset=utf-8" \ -X "POST" \ -u "api-key_SUA-API-KEY:token" \ -d boleto.conta.token="api-key_SEU-TOKEN-DA-CONTA" \ -d boleto.tokenControleUsuario="pedido-12345-abc" \ -d boleto.pagador.nome="Alberto Santos Dumont" \ ...import javax.ws.rs.client.ClientBuilder;import javax.ws.rs.client.Entity;import javax.ws.rs.core.Form;import javax.ws.rs.core.Response;import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import static javax.ws.rs.core.Response.Status.CREATED;import static javax.ws.rs.core.Response.Status.CONFLICT;
public class CriarBoletoComIdempotencia { public static void main(String[] args) { Form form = new Form() .param("boleto.conta.token", "api-key_SEU-TOKEN-DA-CONTA") .param("boleto.tokenControleUsuario", "pedido-12345-abc") .param("boleto.pagador.nome", "Alberto Santos Dumont"); // ... demais campos
Response response = ClientBuilder.newClient() .target("https://sandbox.boletocloud.com/api/v1/boletos") .register(HttpAuthenticationFeature.basic( "api-key_SUA-API-KEY", "token")) .request() .post(Entity.form(form));
if (response.getStatus() == CREATED.getStatusCode()) { String token = response.getHeaderString("X-BoletoCloud-Token"); String nossoNumero = response.getHeaderString("X-BoletoCloud-NIB-Nosso-Numero"); System.out.println("Boleto criado! Token: " + token); System.out.println("Nosso Número: " + nossoNumero);
} else if (response.getStatus() == CONFLICT.getStatusCode()) { String tokenExistente = response.getHeaderString("X-BoletoCloud-Token"); String nossoNumero = response.getHeaderString("X-BoletoCloud-NIB-Nosso-Numero"); String tokenControle = response.getHeaderString("X-BoletoCloud-Token-Controle-Usuario"); String location = response.getHeaderString("Location");
System.out.println("Boleto já existe!"); System.out.println("Token: " + tokenExistente); System.out.println("Nosso Número: " + nossoNumero); System.out.println("Token de Controle: " + tokenControle); System.out.println("Location: " + location); } else { System.out.println("Erro: " + response.getStatus()); } }}import okhttp3.*
fun main() { val client = OkHttpClient() val credential = Credentials.basic("api-key_SUA-API-KEY", "token")
val body = FormBody.Builder() .add("boleto.conta.token", "api-key_SEU-TOKEN-DA-CONTA") .add("boleto.tokenControleUsuario", "pedido-12345-abc") .add("boleto.pagador.nome", "Alberto Santos Dumont") // ... demais campos .build()
val request = Request.Builder() .url("https://sandbox.boletocloud.com/api/v1/boletos") .header("Authorization", credential) .post(body) .build()
client.newCall(request).execute().use { response -> when (response.code) { 201 -> { val token = response.header("X-BoletoCloud-Token") val nossoNumero = response.header("X-BoletoCloud-NIB-Nosso-Numero") println("Boleto criado! Token: $token") println("Nosso Número: $nossoNumero") } 409 -> { val tokenExistente = response.header("X-BoletoCloud-Token") val nossoNumero = response.header("X-BoletoCloud-NIB-Nosso-Numero") val tokenControle = response.header("X-BoletoCloud-Token-Controle-Usuario") val location = response.header("Location")
println("Boleto já existe!") println("Token: $tokenExistente") println("Nosso Número: $nossoNumero") println("Token de Controle: $tokenControle") println("Location: $location") } else -> println("Erro: ${response.code}") } }}using System;using System.Net.Http;using System.Net.Http.Headers;using System.Text;using System.Threading.Tasks;
class Program { static async Task Main(string[] args) { using var client = new HttpClient(); var credentials = Convert.ToBase64String( Encoding.ASCII.GetBytes("api-key_SUA-API-KEY:token")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
var content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("boleto.conta.token", "api-key_SEU-TOKEN-DA-CONTA"), new KeyValuePair<string, string>("boleto.tokenControleUsuario", "pedido-12345-abc"), new KeyValuePair<string, string>("boleto.pagador.nome", "Alberto Santos Dumont") // ... demais campos });
var response = await client.PostAsync( "https://sandbox.boletocloud.com/api/v1/boletos", content);
if ((int)response.StatusCode == 201) { var token = response.Headers.GetValues("X-BoletoCloud-Token"); var nossoNumero = response.Headers.GetValues("X-BoletoCloud-NIB-Nosso-Numero"); Console.WriteLine($"Boleto criado! Token: {string.Join("", token)}"); Console.WriteLine($"Nosso Número: {string.Join("", nossoNumero)}");
} else if ((int)response.StatusCode == 409) { var token = response.Headers.GetValues("X-BoletoCloud-Token"); var nossoNumero = response.Headers.GetValues("X-BoletoCloud-NIB-Nosso-Numero"); var tokenControle = response.Headers.GetValues("X-BoletoCloud-Token-Controle-Usuario"); var location = response.Headers.Location;
Console.WriteLine("Boleto já existe!"); Console.WriteLine($"Token: {string.Join("", token)}"); Console.WriteLine($"Nosso Número: {string.Join("", nossoNumero)}"); Console.WriteLine($"Token de Controle: {string.Join("", tokenControle)}"); Console.WriteLine($"Location: {location}"); } else { Console.WriteLine($"Erro: {(int)response.StatusCode}"); } }}const https = require('https');const querystring = require('querystring');
const data = querystring.stringify({ 'boleto.conta.token': 'api-key_SEU-TOKEN-DA-CONTA', 'boleto.tokenControleUsuario': 'pedido-12345-abc', 'boleto.pagador.nome': 'Alberto Santos Dumont' // ... demais campos});
const options = { hostname: 'sandbox.boletocloud.com', path: '/api/v1/boletos', method: 'POST', auth: 'api-key_SUA-API-KEY:token', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(data) }};
const req = https.request(options, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { if (res.statusCode === 201) { console.log('Boleto criado!'); console.log('Token:', res.headers['x-boletocloud-token']); console.log('Nosso Número:', res.headers['x-boletocloud-nib-nosso-numero']); } else if (res.statusCode === 409) { console.log('Boleto já existe!'); console.log('Token:', res.headers['x-boletocloud-token']); console.log('Nosso Número:', res.headers['x-boletocloud-nib-nosso-numero']); console.log('Token de Controle:', res.headers['x-boletocloud-token-controle-usuario']); console.log('Location:', res.headers['location']); } else { console.log(`Erro: ${res.statusCode}`); } });});
req.write(data);req.end();package main
import ( "fmt" "net/http" "net/url" "strings")
func main() { data := url.Values{} data.Set("boleto.conta.token", "api-key_SEU-TOKEN-DA-CONTA") data.Set("boleto.tokenControleUsuario", "pedido-12345-abc") data.Set("boleto.pagador.nome", "Alberto Santos Dumont") // ... demais campos
req, _ := http.NewRequest("POST", "https://sandbox.boletocloud.com/api/v1/boletos", strings.NewReader(data.Encode())) req.SetBasicAuth("api-key_SUA-API-KEY", "token") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close()
switch resp.StatusCode { case 201: fmt.Println("Boleto criado!") fmt.Println("Token:", resp.Header.Get("X-BoletoCloud-Token")) fmt.Println("Nosso Número:", resp.Header.Get("X-BoletoCloud-NIB-Nosso-Numero")) case 409: fmt.Println("Boleto já existe!") fmt.Println("Token:", resp.Header.Get("X-BoletoCloud-Token")) fmt.Println("Nosso Número:", resp.Header.Get("X-BoletoCloud-NIB-Nosso-Numero")) fmt.Println("Token de Controle:", resp.Header.Get("X-BoletoCloud-Token-Controle-Usuario")) fmt.Println("Location:", resp.Header.Get("Location")) default: fmt.Printf("Erro: %d\n", resp.StatusCode) }}<?php$url = 'https://sandbox.boletocloud.com/api/v1/boletos';$api_key = 'api-key_SUA-API-KEY';
$data = http_build_query([ 'boleto.conta.token' => 'api-key_SEU-TOKEN-DA-CONTA', 'boleto.tokenControleUsuario' => 'pedido-12345-abc', 'boleto.pagador.nome' => 'Alberto Santos Dumont' // ... demais campos]);
$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_POST, true);curl_setopt($ch, CURLOPT_POSTFIELDS, $data);curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);curl_setopt($ch, CURLOPT_USERPWD, "$api_key:token");curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);$headers = substr($response, 0, $header_size);$body = substr($response, $header_size);curl_close($ch);
if ($http_code == 201) { preg_match('/X-BoletoCloud-Token:\s*(.+)/i', $headers, $matches); $token = trim($matches[1] ?? ''); echo "Boleto criado! Token: $token\n"; file_put_contents('boleto.pdf', $body);
} else if ($http_code == 409) { preg_match('/X-BoletoCloud-Token:\s*(.+)/i', $headers, $matchToken); preg_match('/X-BoletoCloud-NIB-Nosso-Numero:\s*(.+)/i', $headers, $matchNossoNumero); preg_match('/X-BoletoCloud-Token-Controle-Usuario:\s*(.+)/i', $headers, $matchTokenControle); preg_match('/Location:\s*(.+)/i', $headers, $matchLocation);
echo "Boleto já existe!\n"; echo "Token: " . trim($matchToken[1] ?? '') . "\n"; echo "Nosso Número: " . trim($matchNossoNumero[1] ?? '') . "\n"; echo "Token de Controle: " . trim($matchTokenControle[1] ?? '') . "\n"; echo "Location: " . trim($matchLocation[1] ?? '') . "\n";} else { echo "Erro ($http_code): $body\n";}?>import requestsfrom requests.auth import HTTPBasicAuth
data = { 'boleto.conta.token': 'api-key_SEU-TOKEN-DA-CONTA', 'boleto.tokenControleUsuario': 'pedido-12345-abc', 'boleto.pagador.nome': 'Alberto Santos Dumont' # ... demais campos}
response = requests.post( 'https://sandbox.boletocloud.com/api/v1/boletos', auth=HTTPBasicAuth('api-key_SUA-API-KEY', 'token'), data=data)
if response.status_code == 201: token = response.headers.get('X-BoletoCloud-Token') nosso_numero = response.headers.get('X-BoletoCloud-NIB-Nosso-Numero') print(f'Boleto criado! Token: {token}') print(f'Nosso Número: {nosso_numero}') with open('boleto.pdf', 'wb') as f: f.write(response.content)
elif response.status_code == 409: token = response.headers.get('X-BoletoCloud-Token') nosso_numero = response.headers.get('X-BoletoCloud-NIB-Nosso-Numero') token_controle = response.headers.get('X-BoletoCloud-Token-Controle-Usuario') location = response.headers.get('Location')
print('Boleto já existe!') print(f'Token: {token}') print(f'Nosso Número: {nosso_numero}') print(f'Token de Controle: {token_controle}') print(f'Location: {location}')else: print(f'Erro: {response.status_code}')require 'net/http'require 'uri'
uri = URI('https://sandbox.boletocloud.com/api/v1/boletos')
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| request = Net::HTTP::Post.new(uri) request.basic_auth('api-key_SUA-API-KEY', 'token') request.set_form_data( 'boleto.conta.token' => 'api-key_SEU-TOKEN-DA-CONTA', 'boleto.tokenControleUsuario' => 'pedido-12345-abc', 'boleto.pagador.nome' => 'Alberto Santos Dumont' # ... demais campos )
response = http.request(request) case response.code when '201' puts 'Boleto criado!' puts "Token: #{response['X-BoletoCloud-Token']}" puts "Nosso Número: #{response['X-BoletoCloud-NIB-Nosso-Numero']}" File.write('boleto.pdf', response.body) when '409' puts 'Boleto já existe!' puts "Token: #{response['X-BoletoCloud-Token']}" puts "Nosso Número: #{response['X-BoletoCloud-NIB-Nosso-Numero']}" puts "Token de Controle: #{response['X-BoletoCloud-Token-Controle-Usuario']}" puts "Location: #{response['Location']}" else puts "Erro: #{response.code}" endend(require '[clj-http.client :as client])
(let [response (client/post "https://sandbox.boletocloud.com/api/v1/boletos" {:basic-auth ["api-key_SUA-API-KEY" "token"] :form-params {:boleto.conta.token "api-key_SEU-TOKEN-DA-CONTA" :boleto.tokenControleUsuario "pedido-12345-abc" :boleto.pagador.nome "Alberto Santos Dumont"} :throw-exceptions false})] (case (:status response) 201 (do (println "Boleto criado!") (println "Token:" (get-in response [:headers "X-BoletoCloud-Token"])) (println "Nosso Número:" (get-in response [:headers "X-BoletoCloud-NIB-Nosso-Numero"])) (clojure.java.io/copy (:body response) (clojure.java.io/file "boleto.pdf"))) 409 (do (println "Boleto já existe!") (println "Token:" (get-in response [:headers "X-BoletoCloud-Token"])) (println "Nosso Número:" (get-in response [:headers "X-BoletoCloud-NIB-Nosso-Numero"])) (println "Token de Controle:" (get-in response [:headers "X-BoletoCloud-Token-Controle-Usuario"])) (println "Location:" (get-in response [:headers "Location"]))) (println "Erro:" (:status response))))Garanta unicidade
Seção intitulada “Garanta unicidade”Quando Usar
Seção intitulada “Quando Usar”| Cenário | Recomendação |
|---|---|
| Integrações via API | Sempre usar |
| E-commerce / Checkout | Sempre usar (evita cobranças duplicadas) |
| Geração em lote | Sempre usar (cada boleto com token único) |
| Sistemas com retry automático | Sempre usar |
| Ambientes com alta concorrência | Sempre usar |
Veja Também
Seção intitulada “Veja Também” Campos do Boleto Referência completa do campo tokenControleUsuario
Criar Boleto (POST) Criar boletos individuais com idempotência
Criar Carnês Criar carnês com tokens únicos por parcela
Criar Lote (Batch) Criar boletos em lote com idempotência