🚀 Fase beta. ERP en fase privada —solicita acceso
Guías de la API Guía 3 de 6

Subir una factura y recoger el resultado

Qué se puede subir, cómo se dice si es compra o venta, el número de trabajo y cómo se consulta hasta que termina, respetando Retry-After.

Qué entra

Lo que admite cada subida
Formato y tamaño

Un PDF, JPEG o PNG de hasta 15 MB.

Páginas

Se leen como mucho 5 páginas. Si el documento tiene más, el resultado llega con el aviso PAGINAS_NO_LEIDAS.

Una factura por documento

Un documento con varias facturas se rechaza (MULTIFACTURA): un proceso es una factura.

Solo facturas

Recibidas o emitidas. Lo que no es una factura (un ticket, una nómina, un extracto, un acta…) se rechaza (NO_ES_FACTURA).

Compra o venta, siempre desde la empresa de la llave

En cada subida dices su tipo, y se dice desde el punto de vista de la empresa de la llave de procesar:

  • compra si esa empresa recibe la factura (aparece como cliente; un proveedor se la emite);
  • venta si la emite (aparece como proveedor).

Decide cómo se lee la factura entera. El documento lo repite en direccion (RECIBIDA o EMITIDA). Si en el papel la empresa aparece en el lado contrario, se lee igual como dijiste y llega con el aviso TIPO_DISCREPANTE (lo cuenta la guía del resultado).

El NIF de la empresa es el de la llave: no se manda en la petición.

Subir y recoger, paso a paso

1

Sube la factura

POST /v1/trabajos, en multipart/form-data, con la llave de procesar, el tipo y el fichero en factura:

curl -sS https://api.munincloud.com/v1/trabajos \
  -H "Authorization: Bearer $MC_LLAVE_PROCESAR" \
  -H "Idempotency-Key: 7f6c1b0e-factura-0001" \
  -F "tipo=compra" \
  -F "factura=@factura.pdf;type=application/pdf"

La API contesta enseguida con un 202 y un número de trabajo:

{
  "trabajo": "trb_01J9ZQ3K4T7W2B5N8C6D0F1G2H",
  "estado": "en_curso",
  "creado": "2026-10-02T16:18:24Z",
  "pool": {"estado": "activa", "disponibles": 2412, "aviso_80": false}
}

Con las cabeceras Location (la URL del trabajo), Retry-After (los segundos que conviene esperar antes de preguntar) y X-Pool-Disponibles / X-Pool-Estado (lo que le queda a la pool).

ℹ️
Idempotency-Key: reintentar sin pagar dos veces

Si repites la subida con la misma cabecera Idempotency-Key (y la misma llave) en menos de 24 horas, recibes el mismo trabajo y no se descuenta otra vez. Es un texto libre de 8 a 100 caracteres. Úsala si reintentas tras un corte de red.

2

Consulta el trabajo hasta que deje de estar en curso

El proceso es asíncrono: tarda de segundos a unos minutos. GET /v1/trabajos/{trabajo} devuelve el trabajo siempre con 200 mientras exista, en uno de tres estados:

  • en_curso: vuelve a preguntar tras los segundos de Retry-After;
  • terminado: documento trae la factura auditada v1, con sus avisos;
  • rechazado: error dice por qué.

El bucle del arranque rápido, con curl:

while :; do
  curl -sS -D cabeceras.txt "$API/v1/trabajos/$TRABAJO" \
    -H "Authorization: Bearer $(cat mc_llave_procesar)" > resultado.json
  [ "$(campo estado < resultado.json)" = "en_curso" ] || break
  ESPERA=$(sed -n 's/^[Rr]etry-[Aa]fter: *\([0-9]*\).*/\1/p' cabeceras.txt)
  sleep "${ESPERA:-5}"
done

Y con Python, que además espera ante un 429 o un 503:

while True:
    r = sesion.get(f"{API}/v1/trabajos/{trabajo}", timeout=30)
    if r.status_code in (429, 503) or (r.status_code == 200 and r.json()["estado"] == "en_curso"):
        time.sleep(int(r.headers.get("Retry-After", "5")))
        continue
    break

campo, $API, sesion y API son los del paso 0 de cada guion: los dos enteros están en la documentación.

3

Terminado o rechazado

Un trabajo terminado trae el documento v1: cómo leerlo lo cuenta la guía siguiente. Un trabajo rechazado trae el error, y no descuenta de la pool:

{
  "trabajo": "trb_01J9ZQ3K4T7W2B5N8C6D0F1G2K",
  "estado": "rechazado",
  "creado": "2026-10-02T16:20:00Z",
  "terminado": "2026-10-02T16:20:31Z",
  "error": {
    "codigo": "MULTIFACTURA",
    "mensaje": "El documento contiene más de una factura. Un proceso es una factura: súbalas por separado."
  },
  "pool": {"estado": "activa", "disponibles": 2412, "aviso_80": false}
}

En NO_ES_FACTURA, error.tipo_detectado dice qué se reconoció (TICKET, NOMINA, ONBOARDING o bancos), o null si no se sabe.

⚠️
El resultado se guarda 24 horas

No guardamos nada de la factura. El resultado se conserva 24 horas desde que termina (caduca dice cuándo), para que lo recojas; después el trabajo ya no existe. Solo la llave que lo subió puede verlo: uno que no existe, que es de otra llave o que ya caducó contesta lo mismo, 410 NO_ENCONTRADO.

Lo que puede contestar la subida

HTTPcodigoqué pasa
401LLAVE_INVALIDA, LLAVE_ANULADA, IP_NO_PERMITIDA, LLAVE_SIN_PERMISOla llave falta, no es válida, está anulada, se usa desde una IP no permitida o no sirve para esta ruta
402POOL_AGOTADA, POOL_CERRADAla pool no puede procesar
413DOCUMENTO_NO_VALIDOel fichero pasa de 15 MB
415DOCUMENTO_NO_VALIDOel fichero no es PDF, JPEG ni PNG
422DOCUMENTO_NO_VALIDO, PETICION_NO_VALIDAfalta el fichero o está roto, o la petición está mal formada
429RITMO_EXCEDIDOdemasiadas peticiones: espera Retry-After y reintenta
500ERROR_INTERNOun fallo nuestro; no se ha descontado nada y puedes reintentar
503ERROR_INTERNOel motor de lectura no está disponible ahora mismo: reintenta tras Retry-After; no se ha descontado nada

Si el trabajo terminó pero en ese momento no se puede devolver el resultado, la consulta contesta 503 con Retry-After: el trabajo sigue ahí; vuelve a consultar pasados esos segundos (dentro de las 24 horas).