Saltar al contenido principal

Tu primera factura

Un recorrido completo con curl. Ten a mano tu API key y al menos un externalProductId ya mapeado por el equipo de Contadito.

aviso

El paso 4 emite una factura legal

Consume un correlativo del rango CAI de tu empresa. Si todavía estás armando tu integración, quédate en Pruebas y vuelve aquí cuando lo que quieras sea facturar de verdad.

export CONTADITO_API_KEY="ctd_sk_..."
export BASE="https://integration.contadito.com"

Paso 1 — ¿está viva la API?

curl -s $BASE/v1/health
{ "status": "ok" }

Paso 2 — registra un cliente (opcional)

Solo lo necesitas si vas a emitir una venta exonerada o si quieres que la factura salga a nombre de alguien. Una venta de mostrador puede omitirlo.

curl -s -X POST $BASE/v1/clients \
-H "Authorization: Bearer $CONTADITO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"clientName": "Servicios Digitales S. de R.L.",
"email": "facturacion@cliente.hn",
"legalId": "08011999123456",
"phone": "+504 9999-9999",
"address": "Tegucigalpa, Honduras"
}'
{
"id": 1042,
"clientName": "Servicios Digitales S. de R.L.",
"email": "facturacion@cliente.hn",
"legalId": "08011999123456",
"phone": "+504 9999-9999",
"address": "Tegucigalpa, Honduras",
"createdAt": "2026-08-10T15:04:05.000Z"
}

Guarda ese id: es el clientId de la factura. Guárdalo en tu base de datos, junto a tu propio id de cliente. Si alguna vez lo pierdes, puedes listar tus clientes con GET /v1/clients:

curl -s "$BASE/v1/clients?page=1&limit=50" \
-H "Authorization: Bearer $CONTADITO_API_KEY"
{
"clients": [ { "id": 1042, "clientName": "Servicios Digitales S. de R.L.", "...": "..." } ],
"count": 1
}

Eso sí: ese endpoint no filtra por RTN ni por nombre, así que no lo uses en cada venta. Ver GET /v1/clients.

Paso 3 — previsualiza (sin emitir nada)

Antes de gastar un correlativo real, comprueba que los precios, los impuestos y el formato impreso salgan como esperas:

curl -s -X POST $BASE/v1/invoices/preview \
-H "Authorization: Bearer $CONTADITO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"clientId": 1042,
"items": [
{ "externalProductId": "SKU-WEB-HOSTING-01", "quantity": 2 }
]
}'
{
"preview": true,
"total": 2300,
"totalInLetters": "DOS MIL TRESCIENTOS LEMPIRAS EXACTOS",
"taxBreakdown": [{ "rate": 15, "taxAmount": 300, "taxedAmount": 2000 }],
"pdfBase64": "JVBERi0xLjQK..."
}

Mismo body y mismos cálculos que la emisión real, pero no consume CAI, no avanza el correlativo y no lleva Idempotency-Key. Repítelo cuantas veces quieras mientras ajustas tu integración. El PDF viene marcado SIN VALIDEZ FISCAL: no es entregable a un cliente. Ver POST /v1/invoices/preview.

Paso 4 — emite la factura

curl -s -X POST $BASE/v1/invoices \
-H "Authorization: Bearer $CONTADITO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-2026-0917" \
-d '{
"clientId": 1042,
"items": [
{ "externalProductId": "SKU-WEB-HOSTING-01", "quantity": 2 }
]
}'
{
"id": 8801,
"invoiceNumber": 1573,
"cai": "A1B2C3-D4E5F6-A1B2C3-D4E5F6-A1B2C3-12",
"invoiceDate": "2026-08-10T15:04:05.000Z",
"clientId": 1042,
"importe": 2000,
"discountTotal": 0,
"exemptedAmount": 0,
"exoneratedAmount": 0,
"totalTaxes": 300,
"total": 2300,
"totalInLetters": "DOS MIL TRESCIENTOS LEMPIRAS EXACTOS",
"isPaid": true,
"isAnnulled": false,
"taxBreakdown": [
{ "rate": 15, "taxAmount": 300, "taxedAmount": 2000 }
],
"lines": [
{
"externalProductId": "SKU-WEB-HOSTING-01",
"quantity": 2,
"unitPrice": 1000,
"importe": 2000,
"discountAmount": 0,
"subTotal": 2000,
"exemptAmount": null
}
],
"replayed": false,
"pdfBase64": "JVBERi0xLjQK..."
}

pdfBase64 es el comprobante A4 listo para imprimir o enviar por correo. Si alguna vez viene null, la factura igual se emitió: descarga el PDF con GET /v1/invoices/{id}/pdf.

Fíjate en lo que no mandaste: impuestos, correlativo, CAI. Todo eso lo pone Contadito. Tú mandas qué se vendió y cuánto; el precio salió de tu lista de precios, aunque puedes fijarlo por línea con items[].price cuando la venta se cerró a otro precio.

peligro

Antes de mandar esta llamada en producción

El Idempotency-Key tiene que estar persistido en tu base de datos antes de hacer la solicitud, no generado al vuelo. Si no, un timeout te deja sin forma segura de reintentar. Lee Idempotencia y reintentos.

Paso 5 — vuelve a leerla

curl -s $BASE/v1/invoices/8801 \
-H "Authorization: Bearer $CONTADITO_API_KEY"

Devuelve el mismo objeto (con replayed: false y pdfBase64: null). La consulta está limitada a tu empresa: cualquier id fuera de ese alcance responde 404 EXTERNAL_INVOICE_NOT_FOUND.

Y el PDF, cuando lo necesites de nuevo:

curl -s $BASE/v1/invoices/8801/pdf \
-H "Authorization: Bearer $CONTADITO_API_KEY" \
-o factura-8801.pdf

Ejemplo en Node.js

const BASE = 'https://integration.contadito.com';

async function emitirFactura(orden) {
const res = await fetch(`${BASE}/v1/invoices`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CONTADITO_API_KEY}`,
'Content-Type': 'application/json',
// Persistido junto con la orden ANTES de llegar aquí.
'Idempotency-Key': orden.idempotencyKey,
},
body: JSON.stringify({
clientId: orden.contaditoClientId,
items: orden.lineas.map((l) => ({
externalProductId: l.sku,
quantity: l.cantidad,
})),
}),
});

const body = await res.json();

if (!res.ok) {
// Programa contra body.code, nunca contra body.message.
throw new ContaditoError(body.code, body.message, res.status);
}

return body; // body.replayed te dice si fue repetición
}

Ejemplo en PHP

<?php
$ch = curl_init('https://integration.contadito.com/v1/invoices');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('CONTADITO_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . $orden['idempotency_key'],
],
CURLOPT_POSTFIELDS => json_encode([
'clientId' => $orden['contadito_client_id'],
'items' => [
['externalProductId' => 'SKU-WEB-HOSTING-01', 'quantity' => 2],
],
]),
]);

$respuesta = json_decode(curl_exec($ch), true);
$estado = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($estado >= 400) {
throw new RuntimeException($respuesta['code'] . ': ' . $respuesta['message']);
}

Y ahora