Saltar al contenido principal

Referencia HTTP del API

Extracción

OCR + extracción estructurada de gastos o ingresos. POST acepta uno o varios archivos/URLs y siempre responde { results, summary }.

Endpoints de este tema

POST/api/expenses

Extrae uno o varios documentos de gasto (JSON file_url/items o multipart). client_id obligatorio.

Petición
Single or bulk (autodetected):

JSON — one file:
{
  "client_id": "<uuid>",
  "file_url": "https://…"
}

JSON — bulk (max 20):
{
  "client_id": "<uuid>",
  "items": [{ "file_url": "https://…" }, …]
}

multipart/form-data:
- client_id (required)
- file (one or many; max 20 per request)
Respuesta
200 application/json — always:

{
  results: [
    { index, status: "extracted"|"cache"|"duplicate"|"quota_exceeded"|"plan_required"|"error", invoiceId?, invoice?, duplicate?, error? },
    …
  ],
  summary: { ok, failed, quotaExceeded, planRequired }
}

Each successful invoice spreads InvoiceExtract fields (counterparty, invoice_number, date, total, currency, items[], …).

POST/api/revenue

Extrae uno o varios documentos de ingreso (misma forma que POST /api/expenses). client_id obligatorio.

Petición
Single or bulk (autodetected):

JSON — one file:
{
  "client_id": "<uuid>",
  "file_url": "https://…"
}

JSON — bulk (max 20):
{
  "client_id": "<uuid>",
  "items": [{ "file_url": "https://…" }, …]
}

multipart/form-data:
- client_id (required)
- file (one or many; max 20 per request)
Respuesta
200 application/json — always:

{
  results: [
    { index, status: "extracted"|"cache"|"duplicate"|"quota_exceeded"|"plan_required"|"error", invoiceId?, invoice?, duplicate?, error? },
    …
  ],
  summary: { ok, failed, quotaExceeded, planRequired }
}

Each successful invoice spreads InvoiceExtract fields (counterparty, invoice_number, date, total, currency, items[], …).

POST /api/expenses

Documentación API

Esta página documenta POST /api/extract-invoice: JSON con file_url pública; el servidor descarga el archivo y devuelve campos estructurados. Para subir un archivo directamente sin URL pública, usa POST /api/extract-invoice-file (formulario multipart). Autentica con una clave API del panel o un token de acceso de sesión (el mismo JWT que envía el navegador tras iniciar sesión). Las rutas de facturas guardadas están en la referencia principal del API.

Endpoint

Método
POST
Ruta
/api/expenses
URL completa (producción)
https://TU_APP_HOST/api/expenses

Autenticación

Cada petición debe enviar un bearer token. El servidor acepta:

  • Clave API — créala en el panel; las claves empiezan por bf_live_. Usa la cadena tal cual como bearer token.
  • JWT de sesión — el mismo access_token que la web tras iniciar sesión.
Authorization: Bearer <YOUR_API_KEY_OR_ACCESS_TOKEN>

Envía Content-Type: application/json con cuerpo JSON (ver abajo). No uses form-data en esta ruta.

Cuerpo de la petición

El campo JSON file_url debe apuntar a un PDF u otro formato admitido (incluidas imágenes cuando aplica OCR). El servidor descarga el archivo; la URL debe ser pública y pasar las comprobaciones de seguridad de inbill.dev.

{
  "file_url": "https://example.com/invoices/invoice-2026-01.pdf",
  "invoice_kind": "expense"
}

file_url es obligatorio y debe ser una URL válida. Opcional invoice_kind: expense (por defecto) o revenue.

Ejemplos

cURL (clave API)

Sustituye YOUR_HOST y YOUR_API_KEY.

curl -X POST "https://YOUR_HOST/api/expenses" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_url":"https://cdn.example.com/docs/invoice.pdf","invoice_kind":"expense"}'

cURL (JWT de sesión)

curl -X POST "https://YOUR_HOST/api/expenses" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"file_url":"https://example.com/invoice.pdf","invoice_kind":"expense"}'

Node.js (fetch)

const res = await fetch("https://YOUR_HOST/api/expenses", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.INBILL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    file_url: "https://example.com/invoice.pdf",
    invoice_kind: "expense",
  }),
});

const data = await res.json();
if (!res.ok) {
  throw new Error(JSON.stringify(data.error ?? data));
}
return data;

Python (requests)

import os
import requests

r = requests.post(
    "https://YOUR_HOST/api/expenses",
    headers={
        "Authorization": f"Bearer {os.environ['INBILL_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "file_url": "https://example.com/invoice.pdf",
        "invoice_kind": "expense",
    },
    timeout=120,
)
r.raise_for_status()
print(r.json())

Respuesta correcta

200 OK con JSON. Los campos pueden ser null u omitirse; items por defecto es []. counterparty nombra a la contraparte en la factura. Un objeto meta opcional puede existir (p. ej. source: extracted | cache | duplicate, invoiceId) si la fila está en caché, deduplicada o recién guardada.

{
  "invoice_number": "INV-12345",
  "date": "2026-03-20",
  "counterparty": "Acme Corp",
  "total": 1200,
  "subtotal": 1300,
  "discount": 100,
  "tax": 180,
  "currency": "USD",
  "items": [
    { "description": "Service", "quantity": 1, "price": 1020, "line_total": null }
  ],
  "meta": { "source": "extracted", "invoiceId": "uuid" }
}

Respuestas de error

Los errores usan JSON con un objeto error con code y message (a veces detalles de validación).

  • 400 400 — VALIDATION (JSON inválido o no cumple el esquema) o INVALID_URL (URL no permitida).
  • 401 401 — UNAUTHORIZED — falta el bearer o no es válido.
  • 402 402 — QUOTA_EXCEEDED — cupo del plan agotado.
  • 422 422 — FETCH_FAILED (no se pudo descargar) o EXTRACTION_FAILED (error de análisis, OCR o modelo).
  • 500 500 — PERSIST_FAILED u otros errores al guardar uso.

Ejemplo de cuerpo de error:

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing or invalid credentials"
  }
}

Cuotas

Gratis: extracciones correctas limitadas por ventana móvil (por defecto 30 días) y un tope por cuenta. Pro: cupo mensual del plan. Solo cuentan las ejecuciones correctas que pasan el registro de uso.