Primeros pasos
Este recorrido emite una boleta en Pruebas (test), sin valor fiscal. Usa HTTP con curl, sirve para cualquier lenguaje y no necesita un SDK; al final está el mismo paso en cURL, PHP, Python, C#, Java y TypeScript. El contrato completo es la referencia de /v1, que también se descarga sin API key en https://api.boletura.com/openapi/v1.json.
Necesitas una cuenta de Boletura con un plan que incluya la API: cómo pedirla.
1. Preparar cada empresa
Sección titulada «1. Preparar cada empresa»Antes de llamar a la API, un Propietario (owner) o Administrador (admin) de la cuenta prepara cada empresa en el back office:
- En Empresas, registra RUC, razón social y domicilio fiscal con ubigeo. El alta crea el establecimiento principal
0000. Agrega los establecimientos anexos que correspondan. Para ensayar no hace falta un RUC real: entestsolo se comprueban su formato y su dígito verificador, y20000000010los cumple. - En la pestaña Series de la empresa, crea una serie de ambiente Pruebas por tipo y establecimiento que usarás: por ejemplo
B001para boletas yF001para facturas. La primera de cada tipo queda por defecto. Para notas de crédito y de débito, prepara series con la letra del documento afectado, por ejemploBC01yFC01para las de crédito yBD01yFD01para las de débito.
En test no registras certificado ni credenciales SOL: la plataforma firma con su certificado de prueba y envía a SUNAT beta con las credenciales públicas de beta. Conectar con SUNAT es solo para live.
El canal actual es SUNAT directo: facturas individuales y boletas por resumen diario. Antes de live, Boletura comprueba que ese canal sirve para cada RUC; no se admite una empresa obligada a OSE. Dar de alta una empresa en la plataforma no la inscribe ni autoriza por sí solo ante SUNAT.
2. Obtener y guardar una clave test
Sección titulada «2. Obtener y guardar una clave test»En Configuración → API keys, con rol owner o admin, crea una clave de ambiente Pruebas con estos scopes, que ya vienen marcados:
| Scope | Para qué |
|---|---|
companies:read |
Resolver empresas y series |
documents:write |
Emitir, crear notas y solicitar bajas |
documents:read |
Consultar y descargar |
Si vas a emitir para varios RUC, elige Sin empresa por defecto y manda siempre company_id. Una empresa por defecto solo permite omitirlo; no restringe la clave a esa empresa. La clave accede a las empresas de su cuenta según sus scopes.
Copia la clave completa bol_test_…: solo se muestra una vez. Guárdala en el gestor de secretos de tu servidor, nunca en código de navegador, una app móvil, Git ni logs. La API usa Authorization: Bearer; la sesión del back office no sirve para llamarla.
La clave determina la cuenta y el ambiente. No existe un parámetro que convierta una clave test en live, ni sirve cambiarle el prefijo. Al rotar, API keys → Rotar entrega otra clave con los mismos permisos y una gracia de 24 horas por defecto: despliega la nueva, comprueba que funciona y revoca la anterior. Las claves de idempotencia siguen valiendo al rotar dentro de la misma cuenta y el mismo ambiente.
3. Preparar la terminal
Sección titulada «3. Preparar la terminal»Los bloques se ejecutan en la misma terminal Bash (Git Bash en Windows), en una carpeta de trabajo privada fuera de Git. Necesitas curl y Python 3, accesible como python; si tu sistema lo llama python3, ejecuta antes alias python=python3. En Windows, python y python3 pueden ser solo el atajo de la Microsoft Store, y el alias no lo resuelve: si python --version no imprime una versión, instala Python 3 y, si el atajo sigue respondiendo en una terminal nueva, antepón la carpeta de Python al PATH. Python solo ayuda a generar UUID y a leer JSON; el protocolo es HTTP plano.
La URL base es https://api.boletura.com, sin /v1 ni / final, y es la misma en pruebas y en producción: la clave elige el ambiente. Pruebas ahí mismo con tu clave test, que envía a SUNAT beta y no tiene valor fiscal. Usa solo una clave test durante este recorrido.
python --versionexport BOLETURA_BASE_URL='https://api.boletura.com'read -r -s -p 'API key test: ' BOLETURA_API_KEYprintf '\n'campo() { python -c 'import json, sysprint(json.load(open(sys.argv[1], encoding="utf-8")).get(sys.argv[2]) or "")' "$1" "$2"}campo archivo.json nombre imprime un campo de primer nivel de una respuesta guardada. Si cierras la terminal, repite este bloque: las variables y la función no se conservan.
Los cuerpos se crean como archivos UTF-8 sin BOM y se envían con --data-binary @archivo; así se conservan los acentos y los bytes de los reintentos, también con curl.exe en Git Bash. python -m json.tool muestra los acentos como é: es el mismo texto. Los archivos de solicitud y respuesta contienen datos del cliente: consérvalos con acceso restringido.
4. Resolver company_id y las series
Sección titulada «4. Resolver company_id y las series»curl -sS -D empresas.headers -o empresas.json -w '%{http_code}\n' \ -H "Authorization: Bearer $BOLETURA_API_KEY" \ "$BOLETURA_BASE_URL/v1/companies?limit=200"python -m json.tool empresas.jsonEspera 200. Busca el RUC emisor y copia su id, no el de la cuenta ni el del establecimiento:
export COMPANY_ID='UUID de la empresa elegida'Cada entrada trae ruc, legal_name, status, environments_ready y series. Con esta clave, series contiene solo las activas de test: comprueba boleta B…, factura F… e is_default. Si llega vacía, créalas en el paso 1 con el ambiente Pruebas. Una empresa en draft puede emitir en test; ready es su habilitación para live. environments_ready.test: true solo indica que la empresa no está suspendida.
Para emitir desde una sucursal basta enviar su series: la API deduce el establecimiento. Este listado trae el establishment_id de cada serie, no su código ni su nombre, así que en tu sistema guarda qué serie corresponde a cada sucursal.
Si next_cursor no es nulo, pide las páginas siguientes conservando los filtros:
CURSOR=$(campo empresas.json next_cursor)curl -sS -G -o empresas.json -w '%{http_code}\n' \ -H "Authorization: Bearer $BOLETURA_API_KEY" \ --data-urlencode 'limit=200' --data-urlencode "cursor=$CURSOR" \ "$BOLETURA_BASE_URL/v1/companies"Ejecuta ese bloque solo si hay cursor; reemplaza empresas.json por la página siguiente. Léela con python -m json.tool empresas.json y repite el bloque mientras su next_cursor no sea nulo. En tu sistema guarda qué company_id corresponde a cada RUC. Al emitir para otro RUC, cambia explícitamente esa relación, aunque uses la misma clave. /v1/companies es solo lectura: el alta se hace desde el back office o la hace Boletura.
5. Emitir la primera boleta de S/ 12
Sección titulada «5. Emitir la primera boleta de S/ 12»Genera una identidad por operación de negocio, antes del primer envío. En el ensayo usamos UUID nuevos; en tu sistema external_id debe derivar de la venta o del pedido y ser estable. Guárdalo junto con la clave de idempotencia y el cuerpo exacto en tu trabajo pendiente persistente.
IDEMPOTENCY_KEY=$(python -c 'import uuid; print(uuid.uuid4())')EXTERNAL_ID="guia-boleta-$(python -c 'import uuid; print(uuid.uuid4())')"cat > boleta.json <<JSON{ "type": "boleta", "company_id": "$COMPANY_ID", "customer": { "document_type": "dni", "document_number": "45678912", "name": "Juan Pérez" }, "items": [{ "description": "Estacionamiento 2 horas · Placa ABC-123", "quantity": 1, "unit": "ZZ", "unit_price": 12.00 }], "external_id": "$EXTERNAL_ID", "metadata": {"placa": "ABC-123", "pedido": "$EXTERNAL_ID"}}JSONprintf '%s\n' "$IDEMPOTENCY_KEY" > boleta-idempotencia.txtcurl -sS -D boleta.headers -o boleta-respuesta.json -w '%{http_code}\n' \ -H "Authorization: Bearer $BOLETURA_API_KEY" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H 'Content-Type: application/json' \ --data-binary @boleta.json "$BOLETURA_BASE_URL/v1/documents"python -m json.tool boleta-respuesta.jsonEspera 201. Si hay un error, léelo antes de seguir. Con éxito, guarda el UUID:
DOCUMENT_ID=$(campo boleta-respuesta.json id)echo "$DOCUMENT_ID"Comprueba environment: "test", company_id, full_number, un totals.total de 12 y los tres estados. Normalmente verás status: "issued", sunat_status: "pending" y delivery_status: "none". 201 confirma la emisión; no confirma la aceptación fiscal: el resultado de SUNAT llega después, en sunat_status y por webhook. Guarda también external_id, updated_at, sunat.deadline_at y X-Request-Id.
Se usa el establecimiento principal y la serie por defecto. Para otra sucursal agrega su series al cuerpo antes del primer intento. La API asigna el número; no lo envíes. Sin issue_date usa hoy en Lima y sin currency, PEN. unit_price incluye IGV; unit_value lo excluye: envía solo uno. ZZ identifica servicios. La plataforma calcula los totales; los importes son números JSON, nunca cadenas.
El cliente de arriba es ficticio para test. En una boleta de hasta S/ 700 puede omitirse customer; sobre ese importe se exige identificar al cliente. No traslades datos ficticios ni supuestos sobre detracciones o cortesías a ventas reales.
El mismo paso en tu lenguaje
Sección titulada «El mismo paso en tu lenguaje»Cada programa emite una boleta de S/ 12 en test y repite la misma solicitud con la misma clave, como lo haría tras perder la respuesta: la segunda vez llega el mismo 201 con Idempotent-Replayed: true, sin emitir otro comprobante. Solo usan la biblioteca estándar de su lenguaje y leen BOLETURA_API_KEY, BOLETURA_COMPANY_ID y, si no es producción, BOLETURA_BASE_URL.
#!/bin/sh# Emite una boleta de S/ 12 en test y repite la misma solicitud, como lo haría tras perder la respuesta.# Requiere curl y openssl. Variables: BOLETURA_API_KEY (bol_test_…), BOLETURA_COMPANY_ID y, si no es producción,# BOLETURA_BASE_URL.set -eu
: "${BOLETURA_API_KEY:?Falta BOLETURA_API_KEY}": "${BOLETURA_COMPANY_ID:?Falta BOLETURA_COMPANY_ID}"base_url="${BOLETURA_BASE_URL:-https://api.boletura.com}"
# La clave y el cuerpo se guardan antes del primer envío y se reutilizan tal cual en cada reintento. Cualquier valor# único de hasta 128 caracteres sirve como clave; aquí, 32 caracteres hexadecimales al azar.idempotency_key="$(openssl rand -hex 16)"external_id="venta-$(openssl rand -hex 8)"cat > boleta.json <<JSON{ "type": "boleta", "company_id": "$BOLETURA_COMPANY_ID", "external_id": "$external_id", "customer": { "document_type": "dni", "document_number": "45678912", "name": "Juan Pérez" }, "items": [ { "description": "Estacionamiento 2 horas", "quantity": 1, "unit": "ZZ", "unit_price": 12.00 } ]}JSON
issue() { curl -sS -o "$1.json" -D "$1.headers" -w '%{http_code}' \ -H "Authorization: Bearer $BOLETURA_API_KEY" \ -H "Idempotency-Key: $idempotency_key" \ -H 'Content-Type: application/json' \ --data-binary @boleta.json "$base_url/v1/documents"}
field() { sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p" "$1.json"; }
status="$(issue respuesta)"if [ "$status" != 201 ]; then echo "Error $status: $(cat respuesta.json)" >&2 exit 1fiecho "Emitido: $status $(field respuesta full_number) (sunat_status: $(field respuesta sunat_status))"
status="$(issue reintento)"replayed="$(sed -n 's/^[Ii]dempotent-[Rr]eplayed: *\([a-z]*\).*/\1/p' reintento.headers)"echo "Reintento con la misma clave: $status, Idempotent-Replayed: ${replayed:-false}"<?php// Emite una boleta de S/ 12 en test y repite la misma solicitud, como lo haría tras perder la respuesta.// PHP 8.1 o superior con la extensión curl. Variables: BOLETURA_API_KEY (bol_test_…), BOLETURA_COMPANY_ID y, si no es// producción, BOLETURA_BASE_URL.
$apiKey = getenv('BOLETURA_API_KEY') ?: exit("Falta BOLETURA_API_KEY\n");$companyId = getenv('BOLETURA_COMPANY_ID') ?: exit("Falta BOLETURA_COMPANY_ID\n");$baseUrl = getenv('BOLETURA_BASE_URL') ?: 'https://api.boletura.com';
function newUuid(): string{ $bytes = random_bytes(16); $bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40); $bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));}
// La clave y el cuerpo se guardan antes del primer envío y se reutilizan tal cual en cada reintento.$idempotencyKey = newUuid();$body = json_encode([ 'type' => 'boleta', 'company_id' => $companyId, 'external_id' => 'venta-' . newUuid(), 'customer' => ['document_type' => 'dni', 'document_number' => '45678912', 'name' => 'Juan Pérez'], 'items' => [ ['description' => 'Estacionamiento 2 horas', 'quantity' => 1, 'unit' => 'ZZ', 'unit_price' => 12.00], ],], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
function issue(string $url, string $apiKey, string $idempotencyKey, string $body): array{ $headers = []; $curl = curl_init($url); curl_setopt_array($curl, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => [ "Authorization: Bearer $apiKey", "Idempotency-Key: $idempotencyKey", 'Content-Type: application/json', ], CURLOPT_HEADERFUNCTION => function ($curl, string $line) use (&$headers): int { $parts = explode(':', $line, 2); if (count($parts) === 2) { $headers[strtolower(trim($parts[0]))] = trim($parts[1]); } return strlen($line); }, ]); $response = curl_exec($curl); if ($response === false) { throw new RuntimeException(curl_error($curl)); } return [curl_getinfo($curl, CURLINFO_RESPONSE_CODE), $headers, json_decode($response, true)];}
[$status, , $document] = issue("$baseUrl/v1/documents", $apiKey, $idempotencyKey, $body);if ($status !== 201) { fwrite(STDERR, "Error $status: " . json_encode($document, JSON_UNESCAPED_UNICODE) . "\n"); exit(1);}echo "Emitido: $status {$document['full_number']} (sunat_status: {$document['sunat_status']})\n";
[$status, $headers] = issue("$baseUrl/v1/documents", $apiKey, $idempotencyKey, $body);$replayed = $headers['idempotent-replayed'] ?? 'false';echo "Reintento con la misma clave: $status, Idempotent-Replayed: $replayed\n";"""Emite una boleta de S/ 12 en test y repite la misma solicitud, como lo haría tras perder la respuesta.
Python 3.9 o superior. Variables: BOLETURA_API_KEY (bol_test_…), BOLETURA_COMPANY_ID y, si no es producción,BOLETURA_BASE_URL."""
import jsonimport osimport sysimport urllib.errorimport urllib.requestimport uuid
api_key = os.environ["BOLETURA_API_KEY"]company_id = os.environ["BOLETURA_COMPANY_ID"]base_url = os.environ.get("BOLETURA_BASE_URL", "https://api.boletura.com")
# La clave y el cuerpo se guardan antes del primer envío y se reutilizan tal cual en cada reintento.idempotency_key = str(uuid.uuid4())body = json.dumps( { "type": "boleta", "company_id": company_id, "external_id": f"venta-{uuid.uuid4()}", "customer": {"document_type": "dni", "document_number": "45678912", "name": "Juan Pérez"}, "items": [{"description": "Estacionamiento 2 horas", "quantity": 1, "unit": "ZZ", "unit_price": 12.00}], }, ensure_ascii=False,).encode("utf-8")
def issue(): request = urllib.request.Request( f"{base_url}/v1/documents", data=body, method="POST", headers={ "Authorization": f"Bearer {api_key}", "Idempotency-Key": idempotency_key, "Content-Type": "application/json", }, ) try: with urllib.request.urlopen(request, timeout=30) as response: return response.status, response.headers, json.load(response) except urllib.error.HTTPError as error: return error.code, error.headers, json.load(error)
status, _, document = issue()if status != 201: sys.exit(f"Error {status}: {json.dumps(document, ensure_ascii=False)}")print(f"Emitido: {status} {document['full_number']} (sunat_status: {document['sunat_status']})")
status, headers, _ = issue()print(f"Reintento con la misma clave: {status}, Idempotent-Replayed: {headers.get('Idempotent-Replayed', 'false')}")// Emite una boleta de S/ 12 en test y repite la misma solicitud, como lo haría tras perder la respuesta.// .NET 10: dotnet run issue-document.cs. Variables: BOLETURA_API_KEY (bol_test_…), BOLETURA_COMPANY_ID y, si no es// producción, BOLETURA_BASE_URL.using System.Net.Http.Headers;using System.Text.Json;
string apiKey = Environment.GetEnvironmentVariable("BOLETURA_API_KEY") ?? throw new InvalidOperationException("Falta BOLETURA_API_KEY");string companyId = Environment.GetEnvironmentVariable("BOLETURA_COMPANY_ID") ?? throw new InvalidOperationException("Falta BOLETURA_COMPANY_ID");string baseUrl = Environment.GetEnvironmentVariable("BOLETURA_BASE_URL") ?? "https://api.boletura.com";
// La clave y el cuerpo se guardan antes del primer envío y se reutilizan tal cual en cada reintento.string idempotencyKey = Guid.NewGuid().ToString();byte[] body = JsonSerializer.SerializeToUtf8Bytes(new{ type = "boleta", company_id = companyId, external_id = $"venta-{Guid.NewGuid()}", customer = new { document_type = "dni", document_number = "45678912", name = "Juan Pérez" }, items = new[] { new { description = "Estacionamiento 2 horas", quantity = 1, unit = "ZZ", unit_price = 12.00m } },});
using HttpClient client = new() { BaseAddress = new Uri(baseUrl), Timeout = TimeSpan.FromSeconds(30) };client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
async Task<HttpResponseMessage> IssueAsync(){ ByteArrayContent content = new(body); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); HttpRequestMessage request = new(HttpMethod.Post, "/v1/documents") { Content = content }; request.Headers.Add("Idempotency-Key", idempotencyKey); return await client.SendAsync(request);}
using HttpResponseMessage first = await IssueAsync();string json = await first.Content.ReadAsStringAsync();if ((int)first.StatusCode != 201){ Console.Error.WriteLine($"Error {(int)first.StatusCode}: {json}"); return 1;}JsonElement document = JsonDocument.Parse(json).RootElement;Console.WriteLine( $"Emitido: 201 {document.GetProperty("full_number")} (sunat_status: {document.GetProperty("sunat_status")})");
using HttpResponseMessage retry = await IssueAsync();string replayed = retry.Headers.TryGetValues("Idempotent-Replayed", out IEnumerable<string>? values) ? string.Join(",", values) : "false";Console.WriteLine($"Reintento con la misma clave: {(int)retry.StatusCode}, Idempotent-Replayed: {replayed}");return 0;// Emite una boleta de S/ 12 en test y repite la misma solicitud, como lo haría tras perder la respuesta.// Java 11 o superior: java IssueDocument.java. Variables: BOLETURA_API_KEY (bol_test_…), BOLETURA_COMPANY_ID y, si no// es producción, BOLETURA_BASE_URL.import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.nio.charset.StandardCharsets;import java.time.Duration;import java.util.UUID;import java.util.regex.Matcher;import java.util.regex.Pattern;
public class IssueDocument { public static void main(String[] args) throws Exception { String apiKey = required("BOLETURA_API_KEY"); String companyId = required("BOLETURA_COMPANY_ID"); String baseUrl = System.getenv().getOrDefault("BOLETURA_BASE_URL", "https://api.boletura.com");
// La clave y el cuerpo se guardan antes del primer envío y se reutilizan tal cual en cada reintento. String idempotencyKey = UUID.randomUUID().toString(); String json = String.format("{" + "\"type\": \"boleta\"," + "\"company_id\": \"%s\"," + "\"external_id\": \"venta-%s\"," + "\"customer\": {\"document_type\": \"dni\", \"document_number\": \"45678912\", \"name\": \"Juan Pérez\"}," + "\"items\": [{\"description\": \"Estacionamiento 2 horas\", \"quantity\": 1, \"unit\": \"ZZ\", \"unit_price\": 12.00}]" + "}", companyId, UUID.randomUUID()); byte[] body = json.getBytes(StandardCharsets.UTF_8);
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + "/v1/documents")) .timeout(Duration.ofSeconds(30)) .header("Authorization", "Bearer " + apiKey) .header("Idempotency-Key", idempotencyKey) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofByteArray(body)) .build();
HttpResponse<String> first = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (first.statusCode() != 201) { System.err.println("Error " + first.statusCode() + ": " + first.body()); System.exit(1); } System.out.println("Emitido: 201 " + field(first.body(), "full_number") + " (sunat_status: " + field(first.body(), "sunat_status") + ")");
HttpResponse<String> retry = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); String replayed = retry.headers().firstValue("Idempotent-Replayed").orElse("false"); System.out.println("Reintento con la misma clave: " + retry.statusCode() + ", Idempotent-Replayed: " + replayed); }
private static String required(String name) { String value = System.getenv(name); if (value == null || value.isEmpty()) { throw new IllegalStateException("Falta " + name); } return value; }
// Java no trae un lector de JSON: en tu sistema usa el que ya tengas (Jackson, Gson). Aquí basta con dos campos. private static String field(String json, String name) { Matcher matcher = Pattern.compile("\"" + name + "\":\"([^\"]*)\"").matcher(json); return matcher.find() ? matcher.group(1) : ""; }}// Emite una boleta de S/ 12 en test y repite la misma solicitud, como lo haría tras perder la respuesta.// Node 24 o superior, que ejecuta TypeScript sin compilar: node issue-document.ts. Variables: BOLETURA_API_KEY// (bol_test_…), BOLETURA_COMPANY_ID y, si no es producción, BOLETURA_BASE_URL.
const apiKey = requiredEnv('BOLETURA_API_KEY')const companyId = requiredEnv('BOLETURA_COMPANY_ID')const baseUrl = process.env.BOLETURA_BASE_URL ?? 'https://api.boletura.com'
interface IssuedDocument { full_number: string sunat_status: string}
// La clave y el cuerpo se guardan antes del primer envío y se reutilizan tal cual en cada reintento.const idempotencyKey = crypto.randomUUID()const body = JSON.stringify({ type: 'boleta', company_id: companyId, external_id: `venta-${crypto.randomUUID()}`, customer: { document_type: 'dni', document_number: '45678912', name: 'Juan Pérez' }, items: [{ description: 'Estacionamiento 2 horas', quantity: 1, unit: 'ZZ', unit_price: 12.0 }],})
function issue(): Promise<Response> { return fetch(`${baseUrl}/v1/documents`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Idempotency-Key': idempotencyKey, 'Content-Type': 'application/json', }, body, signal: AbortSignal.timeout(30_000), })}
function requiredEnv(name: string): string { const value = process.env[name] if (!value) throw new Error(`Falta ${name}`) return value}
const first = await issue()if (first.status !== 201) { console.error(`Error ${first.status}: ${await first.text()}`) process.exit(1)}const document = (await first.json()) as IssuedDocumentconsole.log(`Emitido: 201 ${document.full_number} (sunat_status: ${document.sunat_status})`)
const retry = await issue()const replayed = retry.headers.get('Idempotent-Replayed') ?? 'false'console.log(`Reintento con la misma clave: ${retry.status}, Idempotent-Replayed: ${replayed}`)Sigue con reintentos e idempotencia, para recuperar una emisión cuya respuesta se perdió.