Convenciones de la API
Reglas comunes a todos los endpoints: formato, paginación, filtros, idempotencia y cómo el backend resuelve los valores por defecto.
Base y formato
Base URL: https://api.test.bilteo.com/api/v1- Transporte: HTTPS. JSON en petición y respuesta (
Content-Type: application/json). - Autenticación:
Authorization: Bearer bk_live_…oBearer bo_at_…— ver Autenticación. - IDs: UUID (los de Bilteo). Para direccionar tus recursos por tu propio identificador, ver external_id.
Importes: decimales como string
Todos los importes y cantidades viajan como string, no como número JSON, para no perder precisión:
{ "quantity": "1", "unit_price": "100.00", "discount": "10" }En la respuesta los recibes igual ("base": "90.00"). Al enviarlos, una cadena decimal válida es suficiente; no hace falta fijar los decimales.
Fechas
- Fechas de operación:
YYYY-MM-DD(p. ej.operation_date: "2026-01-15"). - Marcas de tiempo (
created_at,updated_at,issued_at,sent_at): RFC 3339 en UTC (2026-01-15T10:00:00Z).
Paginación por cursor
Los listados (GET de colección) devuelven un sobre con un cursor opaco:
{
"data": [ /* … */ ],
"next_cursor": "AAECAwQFBgcICQoLDA0ODw",
"has_more": true
}| Parámetro | Descripción |
|---|---|
limit | Resultados por página. 1–100, por defecto 50. |
cursor | Cursor opaco. Pasa aquí el next_cursor de la respuesta anterior. |
| Campo de la respuesta | Descripción |
|---|---|
data | El array de recursos de esta página. |
next_cursor | El cursor de la siguiente página. Pásalo como ?cursor= en la próxima petición. |
has_more | true si quedan más páginas; cuando es false ya estás en la última. |
Recorrer las páginas
1ª petición — sin cursor (primera página):
curl "https://api.test.bilteo.com/api/v1/invoices?limit=2" \
-H "Authorization: Bearer bk_live_xxx"{
"data": [ { "id": "0192aa…" }, { "id": "0192bb…" } ],
"next_cursor": "AAECAwQFBgcICQoLDA0ODw", // ← cópialo
"has_more": true // ← hay más páginas
}2ª petición — pasa ese next_cursor en ?cursor=:
curl "https://api.test.bilteo.com/api/v1/invoices?limit=2&cursor=AAECAwQFBgcICQoLDA0ODw" \
-H "Authorization: Bearer bk_live_xxx"{
"data": [ { "id": "0192cc…" }, { "id": "0192dd…" } ],
"next_cursor": "BQYHCAkKCwwNDg8QERIT",
"has_more": false // ← última página: deja de paginar
}Repite el paso 2 (usando cada vez el next_cursor de la respuesta anterior) hasta que has_more sea false. El limit puedes mantenerlo constante entre páginas.
El cursor es opaco
No lo parsees ni lo construyas a mano: trátalo como un token. Pásalo tal cual lo recibiste. Un cursor mal formado devuelve 400 (pagination.invalid_cursor).
Filtros
Cada listado acepta filtros por query string. Se combinan con la paginación (?state=…&limit=50&cursor=…) y entre sí (AND):
| Recurso | Filtros | Valores |
|---|---|---|
GET /invoices | state, type, partner_id | state: draft, issued, canceled, needs_fix… · type: F1,F2,F3,R1–R5 · partner_id: uuid |
GET /external-invoices | state, external_id | state: pending, registered, rejected, cancelled |
GET /verifactu-records | invoice_id, state | state: pending, submitted, accepted, rejected, error · invoice_id: uuid |
GET /partners | type, is_active, search, external_id | type: per,com… · is_active: true/false |
GET /products | is_active, search, external_id | is_active: true/false |
GET /series | type, is_active | type: tipo de serie · is_active: true/false |
GET /taxes | type, tax_use, is_active | tax_use: sale,purchase,none |
GET /economic-activities | is_active | is_active: true/false |
searchhace coincidencia parcial (ILIKE) sobre los campos de texto del recurso.external_ides exacto y único por tenant, así que devuelve 0 o 1 resultado.- Los nombres exactos de los valores de cada filtro están en la Referencia de la API (cada parámetro declara su esquema).
Idempotencia
Hay dos mecanismos complementarios, para problemas distintos:
| Mecanismo | Para qué | Alcance |
|---|---|---|
Cabecera Idempotency-Key | Que un reintento de una escritura no duplique el efecto | Transitorio (24 h), por petición |
external_id | Enlace permanente entre tu registro y el de Bilteo (lookup/upsert) | Persistente, por recurso |
Reintentos seguros: Idempotency-Key
Si una petición de escritura (POST/PUT/PATCH/DELETE) se corta por un timeout o un error de red, no sabes si llegó a ejecutarse. Para reintentarla sin riesgo de duplicar el efecto (crear dos facturas, emitir dos veces…), manda una cabecera Idempotency-Key con un valor opaco único por operación (p. ej. un UUID v4):
curl -X POST "https://api.test.bilteo.com/api/v1/external-invoices" \
-H "Authorization: Bearer bk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7e9f…-uuid-por-operación" \
-d '{ "numero": "1", "fecha_expedicion": "2026-06-13", "tipo_factura": "F1", … }'Bilteo guarda la respuesta y, ante un reintento:
| Situación | Resultado |
|---|---|
| Misma clave + mismo cuerpo, ya procesada | Se reproduce la respuesta original (mismo status y body) con la cabecera Idempotent-Replayed: true. No se re-ejecuta el efecto. |
| Misma clave en vuelo (otra petición igual aún procesándose) | 409 (idempotency.request_in_progress). |
| Misma clave + cuerpo distinto | 422 (idempotency.key_reused_with_different_body). |
Una clave por operación
La clave es opcional: sin ella la escritura se procesa con normalidad. Genera un valor nuevo por cada operación lógica (no reutilices el mismo para facturas distintas) y reutiliza el mismo solo al reintentar esa misma operación. El alcance es tu tenant y se conserva 24 h.
Sincronización por external_id
Para mantener tus clientes y productos sincronizados desde cualquier sistema (ERP, e-commerce, TPV, tu propia app…) sin tener que guardar una tabla de equivalencias entre tus identificadores y los UUID de Bilteo, partners y products aceptan un external_id: tu propio identificador, único por tenant. Funciona igual para ambos recursos.
Con él direccionas el recurso por tu id en la ruta …/by-external-id/{external_id} (URL-encódealo), en lugar de por el UUID de Bilteo:
| Operación | Ejemplo (igual sobre /products) |
|---|---|
| Crear-o-actualizar (upsert) | PUT /partners/by-external-id/{ext} → 201 si lo creó, 200 si lo actualizó |
| Leer | GET /partners/by-external-id/{ext} |
| Actualizar (parcial) | PATCH /partners/by-external-id/{ext} |
| Borrar | DELETE /partners/by-external-id/{ext} |
| Buscar | GET /partners?external_id={ext} → 0 ó 1 resultado |
El PUT es la pieza clave para sincronizar: manda siempre el mismo PUT {ext} con el estado actual del recurso en tu sistema y olvídate de si ya existía en Bilteo — reenviarlo converge al mismo estado (201 la primera vez, 200 las siguientes), así no duplicas ni necesitas comprobar antes si existe.
# Cliente (partner)
curl -X PUT "https://api.test.bilteo.com/api/v1/partners/by-external-id/CLI-00042" \
-H "Authorization: Bearer bk_live_xxx" -H "Content-Type: application/json" \
-d '{ "type": "com", "name": "Comercial Aldealta SL", "id_num": "B85447761", "country": "ES" }'
# Producto — idéntico, sobre /products
curl -X PUT "https://api.test.bilteo.com/api/v1/products/by-external-id/SKU-9" \
-H "Authorization: Bearer bk_live_xxx" -H "Content-Type: application/json" \
-d '{ "name": "Licencia anual", "unit_price": "240.00", "main_tax_id": "…iva21…" }'Luego, al crear facturas, referencia el cliente/producto por su UUID de Bilteo (el que te devuelve cualquiera de estas llamadas) o resuélvelo cuando lo necesites con el GET …/by-external-id/{ext}.
El external_id lo pones tú
Mándalo en el POST de creación, o deja que el PUT lo fije desde la ruta. Para borrarlo en un PATCH, envía "external_id": "". Si intentas usar uno que ya ocupa otro recurso, recibes 409 (partner.external_id_taken / product.external_id_taken).
external_id vs Idempotency-Key
Son cosas distintas: el external_id es el enlace permanente entre tu registro y el de Bilteo (consultable, reutilizable); la Idempotency-Key solo protege un reintento puntual de una escritura. El PUT por external_id ya es convergente de por sí, así que para sincronizar el catálogo no necesitas la cabecera.
Valores por defecto (serie, actividad, moneda)
Al crear una factura, serie_id, economic_activity_id y currency_code son opcionales. Si los omites, el backend resuelve los defaults del tenant con la misma cascada que la web:
| Campo | Cascada |
|---|---|
| Actividad económica | La única activa → el default del tenant → la primera activa. |
| Serie | La serie por defecto de la actividad para ese tipo de factura → la primera serie de ese tipo. |
| Moneda | El default del tenant → EUR. |
Envíalos explícitamente solo cuando quieras fijar un valor concreto. Para saber qué serie elegirá una actividad, mira sus normal_serie_id / rectified_serie_id / simplified_serie_id en GET /economic-activities.
Códigos de estado HTTP
| Código | Cuándo |
|---|---|
200 | OK. |
201 | Recurso creado. |
204 | OK sin cuerpo (p. ej. DELETE). |
400 | Petición mal formada o validación de campos. |
401 | Credencial ausente, inválida o caducada. |
403 | La key no tiene el scope requerido, o el recurso no es editable. |
404 | El recurso no existe (o no es de tu tenant). |
409 | Conflicto (identificador duplicado, recurso en uso…). |
422 | La operación no puede completarse en el estado actual. |
El cuerpo de todo error sigue el mismo sobre — ver Errores.