Guía de integración

De cero a una lectura completada, y de ahí a producción. Lo que aporta esta página es el orden y las decisiones: qué se hace antes de qué, y qué elegir en cada bifurcación. El detalle de cada endpoint —parámetros, esquemas y ejemplos campo a campo— está en la referencia de la API, que se genera del contrato; el documento en bruto es /openapi.yaml. Lo que hace cada pantalla de la consola está en el manual de la consola.

Los ejemplos son curl y se pegan tal cual. Antes, dos variables:

export LINCE_HOST="https://el-host-del-servicio"   # el que te hayamos dado
export LINCE_KEY="sk_test_tu_key_de_prueba"        # de test: nunca una live para explorar

Las API keys y los dos modos

Una API key es una cadena con prefijo de modo: sk_live_… o sk_test_…. Tras el prefijo van 43 caracteres aleatorios. El servicio no guarda el secreto: solo su SHA-256 y los doce primeros caracteres, que son los que ves luego en la lista de keys.

Los dos modos comparten código y no comparten nada más:

sk_test_sk_live_
Ejecuta el motor realSí, el mismoSí
Se facturaNoSí
Qué lecturas veSolo las hechas con keys testSolo las hechas con keys live
Cuota que consumelimits.test_quota_per_monthlimits.monthly_quota del plan
Necesita suscripción o saldoNoSí

«Solo ve las suyas» es literal: pedir con una key test una lectura hecha en live responde 404 resource_not_found, igual que si fuera de otra cuenta. No existe una key que vea las dos cosas.

Dónde se crean. En la consola, en la pantalla de API keys, que llama a POST /api/admin/api-keys con un nombre y un modo. El campo secret de esa respuesta es la única vez que el secreto existe fuera de tu código: si se pierde, no se recupera; se emite otra key. Una key live se puede crear sin suscripción activa, pero no admitirá lecturas hasta que la haya.

Qué puede hacer cada key. Una key lleva uno o más alcances: read (las llamadas GET: lecturas, uso y cuenta), write (enviar facturas y dar feedback) y delete (borrar lecturas). Al crearla en la consola salen los tres marcados; desmarca lo que esa integración no necesite. Una llamada que pide un alcance que la key no tiene responde 403 insufficient_scope y no hace nada. Los alcances no se cambian después: se emite otra key y se revoca la vieja.

El número de keys activas por cuenta tiene tope (limits.max_active_api_keys); por encima, la creación responde 422 api_key_limit_reached. Las revocadas no cuentan.

Cómo se rota sin cortar el servicio. En este orden, y nunca al revés:

  1. Emite la key nueva y guárdala donde guardes los secretos.
  2. Despliega con la nueva. Las dos keys funcionan a la vez: no hay que cambiar nada de golpe.
  3. Comprueba en la lista de keys que la vieja ya no se usa. Cada key lleva un last_used_at que se actualiza como mucho una vez por minuto: es una pista, no una auditoría, pero basta para ver si algo que creías apagado la sigue usando.
  4. Revócala con DELETE /api/admin/api-keys/{id}. Revocar es idempotente y tiene efecto en la petición siguiente: a partir de ahí esa key responde 401 invalid_api_key.

Autenticación

La key se envía en una de estas dos cabeceras:

Authorization: Bearer sk_test_tu_key_de_prueba
X-Api-Key: sk_test_tu_key_de_prueba

Si llegan las dos, prevalece X-Api-Key. Tiene un motivo: donde un IAP ocupa Authorization con su propio token, es la única forma de que tu key llegue entera.

Sin key, con una desconocida o con una revocada: 401 invalid_api_key. Con la cuenta suspendida: 403 account_suspended: lo que ya está leído se puede seguir consultando; lo que no se admite son lecturas nuevas.

Toda la API está bajo /api: las integraciones llaman a /api/v1/… (con API key) y la administración de la cuenta a /api/admin/… (como persona, con un ID token de Identity Platform, no con una key). Las rutas sin ese prefijo —/v1/… y /admin/…— siguen respondiendo, pero son un alias transitorio que existe para no romper a quien ya las llamaba: no las uses en una integración nueva.

La primera llamada, que no gasta ninguna lectura y confirma que la key vale:

curl -sS "$LINCE_HOST/api/v1/account" \
  -H "Authorization: Bearer $LINCE_KEY" \
  -H "Accept: application/json"

Ahí dentro, en limits, están todos los topes de tu cuenta ya resueltos. Vuelve a esta llamada cuando algo responda 429: los números de esta página son los del perfil, y los de tu cuenta son los que devuelve el endpoint.

El ciclo de una lectura, de punta a punta

Una lectura funciona así: subes un fichero, el servicio lo encola, un worker lo lee y la lectura termina en completed o en failed. Los cuatro estados son queued, processing, completed y failed; los dos últimos son finales.

1. Subir

POST /api/v1/invoices con multipart/form-data. Solo PDF. Un fichero por lectura, de uno a N ficheros por petición, y la respuesta es siempre una lista, también con un solo fichero:

curl -sS -X POST "$LINCE_HOST/api/v1/invoices" \
  -H "Authorization: Bearer $LINCE_KEY" \
  -H "Accept: application/json" \
  -F "files[]=@factura-marzo.pdf" \
  -F "wait=30" \
  -F 'metadata={"batch":"lote-3"}'

Campos del cuerpo de la petición, además de files[]:

CampoQué es
waitSegundos (0-60) que la respuesta espera a que las lecturas terminen. Por defecto, 0
metadataUn string JSON con un objeto plano, hasta 20 campos escalares. Se devuelve tal cual y filtra el listado
originEtiqueta tuya para el origen de la petición (^[a-z0-9][a-z0-9._-]*$, hasta 64). Se devuelve en origin y filtra el listado

metadata va como string porque multipart no transporta objetos: si mandas algo que no es un objeto JSON, la petición entera responde 422 validation_failed con param: metadata.

Qué hacemos con el fichero. Se lee y se te devuelve la lectura. No entra en el conjunto con el que afinamos el motor, que es material propio reunido aparte y que no se alimenta del tráfico del servicio. La separación la sostiene un test que falla si alguien conecta las dos cosas; el detalle, en el manual.

Cuánto se guarda. El PDF y la lectura se quedan: hoy no hay borrado automático ni fecha de caducidad, y se conservan en la UE, cifrados en reposo. Es la política de hoy, no una promesa perpetua. Si necesitas que eliminemos algo, se hace a mano y no es inmediato — el detalle, en el manual.

2. Esperar

Respuesta con wait=30, cuando la lectura terminó dentro de esos segundos: 200 (recortada; el objeto completo, campo a campo, está en la referencia).

{
  "object": "list",
  "data": [
    {
      "id": "rd_01a073bc-ee8e-70a3-ad0d-be4ebf7dd7fd",
      "object": "reading",
      "status": "completed",
      "mode": "test",
      "api_key": { "id": "key_01a073bc-ee8e-70a3-ad0d-be4ebf7dd7fd", "name": "produccion", "prefix": "sk_test_EJEM" },
      "created_at": "2026-09-05T22:42:36Z",
      "completed_at": "2026-09-05T22:42:36Z",
      "duration_ms": 1,
      "file": {
        "name": "factura-marzo.pdf",
        "bytes": 218,
        "pages": 1,
        "sha256": "c012d7f843bf9bfbe128cb3862eaf8ca85bf2b3f5634c1c347aab42646a83e2c",
        "stored": true
      },
      "engine": { "version": "3.0", "model": "claude-sonnet-5" },
      "review": { "needed": false, "mode": "flag", "threshold": 0.85, "confidence": 1, "resolution": null },
      "invoice": {
        "type": "electricity",
        "bills": [
          {
            "id": "inv_01a073bc-ee9d-7355-864b-62037038536c",
            "cups": "ES0031300666852008VE",
            "rate": "2.0TD",
            "initial_date": "2026-07-01",
            "final_date": "2026-07-31",
            "days_billed": 31,
            "total_cost": 58.43,
            "consumption": { "p1": 37, "p2": 41, "p3": 122, "p4": 0, "p5": 0, "p6": 0 }
          }
        ]
      },
      "billing": { "price_eur": 0 },
      "metadata": { "batch": "lote-3" },
      "origin": null,
      "error": null
    }
  ]
}

El ejemplo es una factura de electricidad. Cómo cambian rate, consumption y los precios en una de gas o en una dual, más abajo.

Y si el tiempo se agota con alguna lectura todavía en curso, 202 con el mismo cuerpo y la lectura en el estado en que esté:

{
  "object": "list",
  "data": [
    {
      "id": "rd_01a073bc-ee8e-70a3-ad0d-be4ebf7dd7fd",
      "object": "reading",
      "status": "queued",
      "mode": "test",
      "completed_at": null,
      "duration_ms": null,
      "engine": null,
      "review": null,
      "invoice": null,
      "billing": { "price_eur": 0 },
      "error": null
    }
  ]
}

Esa es la regla entera del código de estado: 200 si terminaron todas, 202 si alguna sigue en curso. Nunca es un error, y el 202 tampoco significa que algo haya ido mal.

3. Recoger

Con la lectura terminada, GET /api/v1/invoices/{id} devuelve el mismo objeto Reading, y GET /api/v1/invoices la lista de la cuenta en el modo de la key, con filtros (status, created_after, created_before, metadata[campo]=valor, origin, review_needed) y paginación por cursor.

curl -sS "$LINCE_HOST/api/v1/invoices/rd_01a073bc-ee8e-70a3-ad0d-be4ebf7dd7fd" \
  -H "Authorization: Bearer $LINCE_KEY" \
  -H "Accept: application/json"

4. Borrar

Si has prometido a tus usuarios que su factura desaparece pasado un plazo, DELETE /api/v1/invoices/{id} borra la lectura de verdad: las facturas con los datos del titular, el feedback, las entregas de webhook y el PDF. Responde 204 y no se puede deshacer. Lo facturado se queda: el saldo y el uso diario no cambian.

Una lectura que aún se está leyendo, o cuyo coste no se ha liquidado (unos minutos tras cerrarse), responde 422 reading_not_settled sin borrar nada. Una que no existe o ya se borró responde 404: si borras por calendario, cuenta un 404 como hecho.

Hace falta una key con el alcance delete. Cada borrado queda apuntado —qué key, qué lectura, cuándo y con qué resultado, sin ningún dato del titular— y se ve en la pantalla de API keys de la consola.

curl -sS -X DELETE "$LINCE_HOST/api/v1/invoices/rd_01a073bc-ee8e-70a3-ad0d-be4ebf7dd7fd" \
  -H "Authorization: Bearer $LINCE_KEY"

Las tres formas de recibir el resultado

Son tres, y la elección depende del volumen y de quién está esperando.

wait en la propia subida (0-60 s). La respuesta se queda abierta hasta que todas las lecturas de esa petición terminan o se acaba el tiempo. Es una sola llamada y no hace falta guardar nada. Úsalo cuando hay una persona delante esperando el resultado y son pocos ficheros. Ten en cuenta que mantiene abierta la conexión —tuya y nuestra— todo ese rato: wait=60 sobre un lote grande es un worker ocupado un minuto en cada lado.

Consultar el detalle en bucle (polling). Guarda el id que te devolvió el 202 y consulta GET /api/v1/invoices/{id} hasta que status sea completed o failed. Ojo: ese endpoint responde al momento con lo que hay, no mantiene la conexión esperando; lo único que espera es el wait de la subida. Sirve para reanudar un trabajo que se cortó, o para vigilar unos pocos ids. Lo que acota el ritmo es tu límite de peticiones por minuto (limits.rate_per_minute), así que espacia las consultas y no consultes sin pausa entre llamadas.

Webhook. El servicio te hace un POST en cuanto la lectura termina. Es el recomendado para volumen: ni conexiones abiertas ni polling. La URL y su secreto se fijan con PUT /api/admin/webhook, y cada PUT con URL genera un secreto nuevo (whsec_…) que se enseña una sola vez.

Lo imprescindible, con las instrucciones completas —verificación de firma en PHP y en Node incluida— en la referencia:

  • El payload es el mismo objeto Reading que devuelve GET /api/v1/invoices/{id}. No hay un formato aparte para el webhook.
  • Dos eventos y no más: reading.completed y reading.failed.
  • Cabeceras: X-Lince-Event, X-Lince-Delivery-Id, X-Lince-Signature y un User-Agent fijo, Lince-Webhooks/1.0.
  • La firma es t=<unix>,v1=<HMAC-SHA256> sobre <t>.<raw body> con el secreto de la cuenta. Firma los bytes tal como llegaron: deserializar y volver a serializar los cambia y la firma deja de cuadrar.
  • Entregado = un 2xx en menos de 10 segundos. El cuerpo de tu respuesta se ignora. Responde rápido y haz el trabajo después.
  • Un 3xx es un intento fallido: las redirecciones no se siguen. Mover el endpoint con un 301 no funciona; hay que cambiar la URL con PUT /api/admin/webhook.
  • Cinco intentos, cuatro esperas: 1 min, 5 min, 30 min y 2 h. Tras el quinto, la entrega se marca como fallida y se abandona —algo más de dos horas y media después del primero—.
  • Si tu receptor estuvo caído más tiempo, te las reenviamos. Escríbenos con los X-Lince-Delivery-Id que te faltan o con la franja horaria en que estuvo caído, y las volvemos a mandar: con el mismo id, así que tu deduplicación sigue valiendo, firmadas con tu secreto actual y a la URL que tengas configurada en ese momento. No hay endpoint para pedirlo tú, y no hace falta esperar: GET /api/v1/invoices te devuelve esas lecturas igual.
  • Deduplica por X-Lince-Delivery-Id, que es el mismo en los cinco intentos. Una entrega puede repetirse porque tu 2xx llegó tarde.
  • Se manda una entrega por lectura terminada, y nada más: lo que cambie después no emite un segundo evento.

Lo razonable es combinar: webhook para el caso normal y un barrido periódico con GET /api/v1/invoices?status=… como red, que es además lo que te cubre sin esperar a nadie cuando una entrega agota sus cinco intentos.

Idempotencia

Manda una cabecera Idempotency-Key en cada POST /api/v1/invoices:

curl -sS -X POST "$LINCE_HOST/api/v1/invoices" \
  -H "Authorization: Bearer $LINCE_KEY" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: lote-3-intento-1" \
  -F "files[]=@factura-marzo.pdf" \
  -F "wait=30"

Las reglas:

  • El ámbito es la API key, y el valor se recuerda durante 24 horas. Dos keys distintas pueden usar la misma cadena sin colisionar.
  • Entre 1 y 255 caracteres.
  • Repetirla con el mismo cuerpo devuelve las mismas lecturas, sin crear ninguna nueva y sin volver a cobrar.
  • Repetirla con otro cuerpo responde 422 idempotency_key_reused con param: Idempotency-Key.
  • «El cuerpo» aquí son los SHA-256 de los ficheros en orden, la metadata y el origin. Cambiar solo el origin ya es otro cuerpo.

Por qué importa. Un timeout de red al subir un lote no te dice si la petición llegó. Sin la cabecera solo tienes dos malas opciones: no reintentar y perder el lote, o reintentar y crear —y pagar— las lecturas dos veces. Con ella, reintentar es seguro por definición, y el reintento se reconoce antes de consumir concurrencia, cuota o saldo. Usa un valor por intento lógico, no por petición HTTP: si reintentas el mismo lote, el valor es el mismo.

Un fallo de lectura no es un error HTTP

Es la distinción en la que más se equivoca una integración nueva.

Lo que va mal en la petición —falta files, el wait está fuera de rango, no hay saldo— devuelve el objeto error que se describe abajo, y no se crea ninguna lectura. Lo que va mal en un fichero no es un error HTTP: la petición fue perfectamente bien y esa lectura concreta se crea directamente como failed, dentro de un 200, con su motivo en error:

{
  "id": "rd_01a073bc-ee8e-70a3-ad0d-be4ebf7dd7fd",
  "status": "failed",
  "file": { "name": "nope.txt", "pages": null, "stored": false },
  "engine": null,
  "review": null,
  "invoice": null,
  "billing": { "price_eur": 0 },
  "error": {
    "type": "processing",
    "code": "unsupported_file_type",
    "message": "Only PDF files are accepted"
  }
}

En una petición de N ficheros la lista respeta el orden de la petición y mezcla sin problema lecturas completed y failed. Recorre data[] y mira status y error.code; el código HTTP por sí solo no te dice si una factura se leyó.

Los cuatro que son culpa del fichero, y ninguno tiene coste (billing.price_eur es 0 y no hay cargo):

error.codeQué pasó
unsupported_file_typeNo es un PDF
file_too_largePesa más que limits.max_file_mb
too_many_pagesTiene más páginas que limits.max_pages
unreadable_documentEl PDF está dañado y no se abre, o se abre y no trae una factura legible

Los tres primeros —y el cuarto, cuando el PDF ni siquiera se deja abrir— los decide una comprobación barata antes de llegar al motor: esa lectura ya es final cuando se crea, pongas wait o no. El unreadable_document que tarda es el otro caso, el del documento que se abrió y no tenía dentro una factura. Hay además tres códigos que son nuestros, no tuyos: model_unavailable, engine_error y never_started. Esos sí admiten volver a subir el mismo fichero.

El formato de error

Uno solo para toda la API. Cuando la petición no sale adelante, el cuerpo es siempre este:

{
  "error": {
    "type": "invalid_request",
    "code": "validation_failed",
    "message": "The files field is required.",
    "param": "files",
    "docs_url": "https://seleya.es/docs/errors#validation_failed",
    "request_id": "req_01a073bc-eeaf-73b6-af43-1ae071158b3b"
  }
}
  • code es lo estable: la cadena a la que enganchar tu switch. El message va en inglés y es para máquinas y para logs; puede cambiar de redacción sin previo aviso.
  • param dice qué campo tiene la culpa en un 422 (files, files.2, wait, metadata, Idempotency-Key…), y es null cuando no aplica.
  • docs_url apunta a la entrada concreta de la tabla de errores, no a la página en general. El host es el del servicio que te respondió.
  • request_id es el req_… de esa petición.

X-Request-Id llega en TODA respuesta, tenga éxito o no, y en un error es exactamente el mismo valor que error.request_id. Lo emitimos nosotros siempre: si mandas tú la cabecera, se ignora. Y lo lleva todo, incluidas las respuestas que nunca llegan a tu ruta —un 404 de una ruta que no existe, o un 413 por un cuerpo descomunal—.

Qué mandar a soporte: el request_id. Con él encontramos la petición en los logs. Guárdalo en tu propio log junto a cada llamada, también en las que salen bien: el día que haya que reconstruir qué pasó, es el único dato que une tu traza con la nuestra.

Cuotas y límites

Lo primero: los límites de tu cuenta te los dice GET /api/v1/account en limits. Los de aquí abajo son los valores de cada perfil, que es de donde salen si el plan o la cuenta no los sobrescriben.

limits.*Perfil defaultPerfil marketer
rate_per_minute30120
max_files_per_request520
max_file_mb810
max_pages1010
max_concurrent_readings520
test_quota_per_month50200
max_active_api_keys1020

La cuota mensual de las keys live (monthly_quota), las lecturas incluidas y los precios los pone el plan. Aquí no se repiten a propósito, para que esta página no se quede desfasada el día que cambien: el catálogo de planes públicos lo sirve GET /console/config (público, sin autenticación) y lo enseña la pantalla de Facturación de la consola, y lo que aplica a tu cuenta está en GET /api/v1/account.

La admisión es todo o nada

Antes de crear ninguna lectura, el POST comprueba en este orden concurrencia → cuota → tope diario → saldo, contando los N ficheros de la petición. La primera protección que salta responde por la petición entera: o entran las N lecturas o no entra ninguna. Nunca te vas a encontrar medio lote creado.

Qué se reintenta y qué no

CódigoHTTP¿Reintentar?
rate_limit_exceeded429Sí. Respeta Retry-After; la respuesta trae también X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset
concurrency_exceeded429Sí, con backoff. Es transitorio: espera a que terminen las lecturas que ya tienes en curso. Ojo, no trae Retry-After: el backoff lo pones tú
quota_exceeded429No dentro del mes. Se agotaron las lecturas contratadas: reintentar no las devuelve
daily_limit_exceeded429No hoy. Se reinicia a medianoche en Europe/Madrid
insufficient_credit402Nunca. Hace falta saldo; solo afecta a live
payment_required402Nunca. La suscripción no está activa o el periodo de prueba terminó. Las keys test siguen funcionando
account_suspended403Nunca
validation_failed422Nunca. Arregla la petición; param dice dónde
idempotency_key_reused422Nunca con ese cuerpo
payload_too_large413Nunca. Parte el lote
invalid_api_key401Nunca
internal_error500Sí, con backoff. Es nuestro. Guarda el request_id

La regla que resume la tabla: los 429 de rate limit (rate_limit_exceeded, concurrency_exceeded) se reintentan con backoff; los 429 de presupuesto (quota_exceeded, daily_limit_exceeded) y los 402 no se arreglan reintentando, por rápido o por despacio que lo hagas. Un bucle que trate los cuatro 429 igual se pasará el resto del día reintentando en vano.

Dos avisos sobre los topes que más confunden. Pasarse de max_files_per_request no es un código de cuota, es 422 validation_failed con param: files —un error de forma, no de presupuesto—, y max_concurrent_readings cuenta lecturas en curso, no peticiones: veinte ficheros en una sola petición ocupan veinte huecos.

La dirección llega en dos campos

Cada factura trae la dirección del suministro en dos campos que no se sustituyen:

  • supply_address es lo que el motor leyó del papel, sin tocar. Si la factura imprime una errata, aquí está la errata.
  • address es la dirección normalizada por el servicio, con su procedencia (source) y su bloque geo con coordenadas y precisión.

Para casar con tus datos, supply_address. Para dibujar un mapa o agrupar por municipio, address. Las dos reglas que hay que saber antes de programar contra ellas: address.geo es siempre un objeto —sin coordenadas, los campos van a null y fallback_reason explica por qué—, y una lectura nunca falla porque no se haya podido geolocalizar.

El resto —qué campos trae cada uno, qué significa cada source, cada precision y cada fallback_reason— está en la sección de dirección y geolocalización de la referencia, que es donde se mantiene.

Gas y facturas duales

El motor lee facturas de electricidad, de gas y duales —un solo documento que factura los dos suministros—. invoice.type dice cuál es, e invoice.bills trae una factura por suministro: una en electricidad o gas, y dos en una dual, cada una con su propio invoice_type y su propio CUPS, porque el punto de suministro de gas tiene el suyo. El ciclo de la lectura es el mismo; lo que cambia es qué campos traen valor:

ElectricidadGas
rate2.0TD, 3.0TD o 6.1TDRL.1 a RL.5
consumptionPor periodo: p1…p6Un solo total, en kWh
Término fijopower_prices (potencia contratada por periodo) y costs.powerfix_term_prices (precio por día y días facturados) y costs.fix_term
costs.tax_energyImpuesto especial sobre la electricidadImpuesto sobre hidrocarburos

La misma lectura de antes si el PDF hubiera sido una factura de gas, recortada igual:

{
  "invoice": {
    "type": "gas",
    "bills": [
      {
        "id": "inv_01a073bc-f1a2-7c3e-9d4b-5e6f7a8b9c0d",
        "invoice_type": "gas",
        "cups": "ES0230000000000001SR",
        "rate": "RL.2",
        "initial_date": "2026-07-01",
        "final_date": "2026-07-31",
        "days_billed": 31,
        "total_cost": 35.2,
        "consumption": { "total": 512 },
        "costs": { "energy": 18.4, "fix_term": 9.3, "tax_energy": 1, "tax_iva": 6.15, "service": 0.35, "power": 0 },
        "fix_term_prices": [{ "price": 0.3, "unit": "day", "duration": 31, "discount": 0 }],
        "power_prices": []
      }
    ]
  }
}

En una dual, type es "dual" y bills trae dos objetos como estos, el de electricidad y el de gas. Si solo te interesa uno de los dos suministros, filtra por el invoice_type de cada factura, no por invoice.type.

De test a live

Si has llegado hasta aquí con una key test, ya tienes la integración entera. Pasar a producción no cambia una línea de tu código: cambia la key. Antes de hacerlo, repasa esto:

  1. La suscripción está activa. Una key live sin suscripción activa, o con el periodo de prueba terminado, responde 402 payment_required en cada llamada. La test seguiría funcionando, así que esto no se descubre probando en test.
  2. Hay saldo. Cada lectura live lo descuenta, y la admisión mira el saldo contra las N lecturas de la petición: si no llega, el POST responde 402 insufficient_credit y no crea ninguna.
  3. Los límites de live son otros. test_quota_per_month no tiene nada que ver con el monthly_quota del plan. Vuelve a mirar GET /api/v1/account con la key nueva.
  4. Las lecturas de test no cruzan. No esperes ver en live nada de lo que probaste en test: son dos mundos separados, y el id de una lectura de test responde 404 a una key live.
  5. El webhook es de la cuenta, no del modo. Lo que ya sabrás si has probado con él: las lecturas test también generan entregas. Si la URL apunta a tu entorno de pruebas, cámbiala antes, o las entregas de producción irán allí.
  6. Estás guardando el request_id de cada llamada en tu log.
  7. Tus reintentos distinguen los 429 de rate limit de los de presupuesto, y mandan Idempotency-Key.

Para vigilar el uso, GET /api/v1/usage da el uso diario de la cuenta en el modo de la key —días cortados a medianoche en Europe/Madrid; por defecto, el mes en curso—.

Si algo no cuadra

Escríbenos con el request_id de la petición concreta. Con eso solemos tener bastante; sin eso, casi nunca. El code del error y la hora aproximada (di en qué zona horaria) ayudan, pero el request_id es lo que identifica tu petición entre todas.


Esta página se ha escrito y verificado contra el código del servicio. Los límites, códigos, tiempos y nombres de campo que aparecen aquí salen de ese código, no de una nota anterior: si alguno no se comporta como dice esta guía, es un fallo nuestro y queremos saberlo.