Cómo funciona
Cómo se usa, en cuatro pasos
- Tras la compra recibes un enlace de recogida de un solo uso. Al abrirlo generas tu llave de administrar (
mc_live_…), que se ve una vez. Nunca te la mandaremos por correo. - Con la llave de administrar creas una llave de procesar por cada empresa cuyas facturas vayas a enviar (
POST /v1/llaves), indicando su NIF y su nombre o razón social (los dos obligatorios: con ellos el motor reconoce qué parte de la factura es tu empresa). Puedes limitar cada llave a unas IP. - Con una llave de procesar subes facturas (
POST /v1/trabajos), diciendo en cada una si es unacomprao unaventade esa empresa, y recibes un número de trabajo. El proceso es asíncrono: tarda de segundos a unos minutos. - Consultas el trabajo (
GET /v1/trabajos/{trabajo}) hasta que estéterminado(con su documentov1) orechazado(con su error). Respeta la cabeceraRetry-Afterentre consultas.
Qué entra. Solo facturas, recibidas o emitidas: un PDF, JPEG o PNG de hasta 15 MB. Se leen como mucho 5 páginas. Un documento con varias facturas se rechaza (MULTIFACTURA): un proceso es una factura. Lo que no es una factura (un ticket, una nómina, un extracto, un acta…) se rechaza (NO_ES_FACTURA). Al subirla dices si es una compra (la empresa de la llave la recibe) o una venta (la emite): la factura se lee así y la respuesta lo repite en documento.direccion (RECIBIDA o EMITIDA). Si en el papel la empresa aparece en el lado contrario, se lee igual como dijiste y el documento lleva el aviso TIPO_DISCREPANTE.
Qué se cobra. Cada factura que se entrega terminada descuenta un proceso de tu pool. Un trabajo rechazado o fallido no descuenta.
Qué guardamos. Nada de la factura. El resultado se conserva 24 horas desde que termina, para que lo recojas; después, el trabajo ya no existe (NO_ENCONTRADO).
El documento v1 está descrito en respuesta.schema.json. Regla de v1: se pueden añadir campos, nunca quitarlos ni renombrarlos. Tu programa debe ignorar los campos que no conozca y tratar un código de aviso que no conozca como «revise el documento».
Los errores siempre llevan un codigo estable (el que debe mirar tu programa) y un mensaje para personas. Esta API no usa los códigos HTTP 400, 403 ni 404: una ráfaga de ellos hace que el cortafuegos de entrada bloquee tu IP durante horas. Los fallos de llave son 401; los de la pool, 402; los de ritmo, 429; los del documento, 413, 415 o 422; y si el motor de lectura no está disponible en ese momento, 503 con Retry-After (al subir y al consultar). La documentación trae la tabla de todos los errores, con un ejemplo de cada uno.
Cómo conseguir acceso
Acceso bajo solicitud
La API se activa con alta previa: escríbenos y te preparamos el acceso.
Con el acceso preparado, el ciclo empieza por la recogida de tu llave: lo cuenta Las llaves.
Arranque rápido
Con curl
El guion entero: inicio_rapido.sh. Se lanza con
MC_VALE=… MC_NIF=… MC_NOMBRE="…" MC_TIPO=compra bash inicio_rapido.sh factura.pdf: el NIF y el nombre son
los de la empresa cuyas facturas vas a enviar, y MC_TIPO dice si esa factura es una compra o una venta de
esa empresa (ver Las llaves).
Paso 0 · lo que usan los pasos: la dirección de la API, la factura, compra o venta, y un lector de JSON
API="${MC_API:-https://api.munincloud.com}"
FACTURA="${1:?uso: MC_VALE=… MC_NIF=… MC_NOMBRE=… MC_TIPO=compra|venta bash inicio_rapido.sh factura.pdf}"
TIPO="${MC_TIPO:?pon en MC_TIPO compra (la empresa recibe la factura) o venta (la emite)}"
case "$TIPO" in compra|venta) ;; *) echo "MC_TIPO tiene que ser compra o venta, no «$TIPO»" >&2; exit 2 ;; esac
campo() { python3 -c 'import json,sys; print(json.load(sys.stdin)[sys.argv[1]])' "$1"; }
umask 077 # los ficheros de las llaves nacen legibles solo por ti
Paso 1 · canjear el vale por la llave de administrar (una sola vez)
# El vale es lo que va detrás de «#vale=» en el enlace que recibes tras la compra. Sirve UNA vez.
curl -sS -X POST "$API/v1/recogida" \
-H "Content-Type: application/json" \
-d "{\"vale\": \"${MC_VALE:?pon en MC_VALE el vale de tu enlace}\"}" > recogida.json
campo llave < recogida.json > mc_llave_administrar # sin «&&»: con set -e, un fallo aquí PARA el guion
rm recogida.json
Paso 2 · crear una llave de procesar para la empresa: su NIF y su nombre, los dos obligatorios
# El JSON lo escribe python3 para que un nombre con comillas o tildes vaya bien escrito.
: "${MC_NIF:?pon en MC_NIF el NIF de la empresa}" "${MC_NOMBRE:?pon en MC_NOMBRE el nombre o razón social de la empresa}"
python3 -c 'import json,os; print(json.dumps({"cif": os.environ["MC_NIF"], "nombre": os.environ["MC_NOMBRE"]}))' > llave_nueva.json
curl -sS -X POST "$API/v1/llaves" \
-H "Authorization: Bearer $(cat mc_llave_administrar)" \
-H "Content-Type: application/json" \
-d @llave_nueva.json > llave.json
campo llave < llave.json > mc_llave_procesar
rm llave.json
Paso 3 · subir una factura diciendo si es compra o venta: la API contesta enseguida con un número de trabajo
curl -sS "$API/v1/trabajos" \
-H "Authorization: Bearer $(cat mc_llave_procesar)" \
-H "Idempotency-Key: $(basename "$FACTURA")-$TIPO-$(date +%Y%m%d)" \
-F "tipo=$TIPO" \
-F "factura=@$FACTURA" | tee trabajo.json
echo
TRABAJO=$(campo trabajo < trabajo.json)
Paso 4 · consultar el trabajo hasta que deje de estar «en_curso», respetando Retry-After
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
cat resultado.json; echo
Paso 5 · ver lo que queda en la pool
curl -sS "$API/v1/pool" -H "Authorization: Bearer $(cat mc_llave_procesar)"
echo
Con Python
El guion entero: inicio_rapido.py
(pip install requests; MC_LLAVE_PROCESAR=… python3 inicio_rapido.py factura.pdf compra).
Paso 0 · lo que usan los pasos
import json
import os
import sys
import time
import requests
API = os.environ.get("MC_API", "https://api.munincloud.com")
def error_de(r):
"""Los errores siempre traen un `codigo` estable (el que debe mirar tu programa) y un `mensaje` para personas."""
try:
return r.json()["error"]["codigo"]
except (ValueError, KeyError):
return f"HTTP {r.status_code}"
Paso 1 · la sesión, con la llave de procesar en la cabecera
sesion = requests.Session()
sesion.headers["Authorization"] = "Bearer " + os.environ["MC_LLAVE_PROCESAR"]
Paso 2 · subir la factura, diciendo si es compra o venta
if len(sys.argv) != 3 or sys.argv[2] not in ("compra", "venta"):
sys.exit("uso: python3 inicio_rapido.py factura.pdf compra|venta")
ruta, tipo = sys.argv[1], sys.argv[2]
with open(ruta, "rb") as fh:
r = sesion.post(f"{API}/v1/trabajos",
headers={"Idempotency-Key": f"{os.path.basename(ruta)}-{tipo}-{time.strftime('%Y%m%d')}"},
data={"tipo": tipo}, files={"factura": (os.path.basename(ruta), fh)}, timeout=60)
if r.status_code != 202:
sys.exit(f"no se pudo subir: {error_de(r)}")
trabajo = r.json()["trabajo"]
print("trabajo:", trabajo)
Paso 3 · consultar hasta que deje de estar «en_curso», respetando Retry-After
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
if r.status_code != 200:
sys.exit(f"no se pudo consultar: {error_de(r)}")
resultado = r.json()
if resultado["estado"] == "terminado":
documento = resultado["documento"]
print(json.dumps(documento["factura"], ensure_ascii=False, indent=2))
print("dirección:", documento["direccion"]) # RECIBIDA si dijiste compra, EMITIDA si dijiste venta
print("avisos:", [a["codigo"] for a in documento.get("avisos", [])])
else:
print("rechazado:", resultado["error"]["codigo"], "-", resultado["error"]["mensaje"])
Paso 4 · lo que queda en la pool
pool = sesion.get(f"{API}/v1/pool", timeout=30).json()
print("disponibles:", pool["disponibles"], "· estado:", pool["estado"])
Las llaves: quién paga, qué empresa se analiza, compra o venta
En la API hay dos papeles, y pueden ser la misma empresa o no:
- Quien paga: la empresa (o la gestoría) que compra el plan. Tiene la pool (el saldo de procesos del mes) y una llave de administrar, que sirve para crear, listar, limitar y anular las llaves de procesar y para ver la pool, pero no para subir facturas.
- La empresa cuyas facturas se analizan: cada llave de procesar es de UNA empresa, con su NIF y su nombre o razón social, los dos obligatorios. Con ellos el motor reconoce qué parte de cada factura es esa empresa. Una gestoría crea una llave por cliente; una empresa que se lleva su contabilidad crea una sola, con su propio NIF y nombre. Todas descuentan de la misma pool.
Compra o venta se dice en cada subida (tipo), siempre desde el punto de vista de la
empresa de la llave: es una compra si esa empresa recibe la factura (aparece como
cliente; un proveedor se la emite) y una venta si la emite (aparece como
proveedor). La factura se lee como digas, y 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 (ver Avisos).
El ciclo de la llave
- Recogida: tras la compra recibes un enlace de un solo uso
(
https://api.munincloud.com/v1/recogida#vale=…). Al abrirlo (o conPOST /v1/recogida) obtienes la llave de administrar, que se ve una vez. Un vale usado, caducado o inexistente contesta410 RECOGIDA_NO_VALIDA. - Administrar: con ella creas una llave de procesar por empresa (su NIF y su nombre), las listas, las limitas a unas IP y las anulas.
- Procesar: con una llave de procesar subes facturas, consultas trabajos y ves la pool.
| grupo | qué hace y con qué llave | operaciones |
|---|---|---|
trabajos | Subir facturas y recoger su resultado. Llave de procesar. | POST /v1/trabajos GET /v1/trabajos/{trabajo} |
pool | El saldo del mes. Cualquier llave de la pool. | GET /v1/pool |
llaves | Crear, listar, limitar por IP y anular las llaves de procesar. Llave de administrar. | GET /v1/llaves POST /v1/llaves PATCH /v1/llaves/{llave} DELETE /v1/llaves/{llave} |
recogida | El enlace de un solo uso que entrega la llave de administrar tras el cobro. Sin llave. | GET /v1/recogida POST /v1/recogida |
Errores
Cada error lleva un codigo estable, que es el que debe mirar tu programa, y un mensaje para
personas. Esta tabla sale tal cual de los ejemplos del contrato (18 casos).
| HTTP | codigo | cuándo | ejemplo (literal del contrato) | dónde |
|---|---|---|---|---|
| 200 (trabajo rechazado) | MULTIFACTURA | El trabajo termina | {
"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
}
} | GET /v1/trabajos/{trabajo} |
| 200 (trabajo rechazado) | NO_ES_FACTURA | El trabajo termina | {
"trabajo": "trb_01J9ZQ3K4T7W2B5N8C6D0F1G2J",
"estado": "rechazado",
"creado": "2026-10-02T16:18:39Z",
"terminado": "2026-10-02T16:18:41Z",
"error": {
"codigo": "NO_ES_FACTURA",
"mensaje": "El documento no es una factura (recibida o emitida). Hoy solo se procesan facturas.",
"tipo_detectado": null
},
"pool": {
"estado": "activa",
"disponibles": 2412,
"aviso_80": false
}
} | GET /v1/trabajos/{trabajo} |
| 401 | IP_NO_PERMITIDA | La llave falta, no es válida, está anulada, se usa desde una IP no permitida o no sirve para esta ruta. Códigos: LLAVE_INVALIDA, LLAVE_ANULADA, IP_NO_PERMITIDA, LLAVE_SIN_PERMISO. | {
"error": {
"codigo": "IP_NO_PERMITIDA",
"mensaje": "Esta llave no admite peticiones desde esta IP."
}
} | DELETE /v1/llaves/{llave} GET /v1/llaves GET /v1/pool GET /v1/trabajos/{trabajo} PATCH /v1/llaves/{llave} POST /v1/llaves POST /v1/trabajos |
| 401 | LLAVE_ANULADA | La llave falta, no es válida, está anulada, se usa desde una IP no permitida o no sirve para esta ruta. Códigos: LLAVE_INVALIDA, LLAVE_ANULADA, IP_NO_PERMITIDA, LLAVE_SIN_PERMISO. | {
"error": {
"codigo": "LLAVE_ANULADA",
"mensaje": "Esta llave está anulada. Usa otra llave de la pool o crea una nueva."
}
} | DELETE /v1/llaves/{llave} GET /v1/llaves GET /v1/pool GET /v1/trabajos/{trabajo} PATCH /v1/llaves/{llave} POST /v1/llaves POST /v1/trabajos |
| 401 | LLAVE_INVALIDA | La llave falta, no es válida, está anulada, se usa desde una IP no permitida o no sirve para esta ruta. Códigos: LLAVE_INVALIDA, LLAVE_ANULADA, IP_NO_PERMITIDA, LLAVE_SIN_PERMISO. | {
"error": {
"codigo": "LLAVE_INVALIDA",
"mensaje": "Falta la llave o no es válida. Envíala como «Authorization: Bearer mc_live_…»."
}
} | DELETE /v1/llaves/{llave} GET /v1/llaves GET /v1/pool GET /v1/trabajos/{trabajo} PATCH /v1/llaves/{llave} POST /v1/llaves POST /v1/trabajos |
| 401 | LLAVE_SIN_PERMISO | La llave falta, no es válida, está anulada, se usa desde una IP no permitida o no sirve para esta ruta. Códigos: LLAVE_INVALIDA, LLAVE_ANULADA, IP_NO_PERMITIDA, LLAVE_SIN_PERMISO. | {
"error": {
"codigo": "LLAVE_SIN_PERMISO",
"mensaje": "Esta llave no sirve para esta ruta (hace falta la de administrar o la de procesar)."
}
} | DELETE /v1/llaves/{llave} GET /v1/llaves GET /v1/pool GET /v1/trabajos/{trabajo} PATCH /v1/llaves/{llave} POST /v1/llaves POST /v1/trabajos |
| 402 | POOL_AGOTADA | La pool no puede procesar. Códigos POOL_AGOTADA (parada, se recarga con las mismas llaves) y POOL_CERRADA. | {
"error": {
"codigo": "POOL_AGOTADA",
"mensaje": "La pool está parada porque se han agotado el plan del mes y las recargas. Al recargarla, las mismas llaves vuelven a funcionar.",
"recargar": "https://munincloud.com/api/recargar"
}
} | POST /v1/trabajos |
| 402 | POOL_CERRADA | La pool no puede procesar. Códigos POOL_AGOTADA (parada, se recarga con las mismas llaves) y POOL_CERRADA. | {
"error": {
"codigo": "POOL_CERRADA",
"mensaje": "La pool está cerrada y sus llaves anuladas. Hace falta una pool nueva."
}
} | GET /v1/pool POST /v1/llaves POST /v1/trabajos |
| 410 | NO_ENCONTRADO | No existe, es de otra llave o ya caducó; las tres contestan igual (NO_ENCONTRADO). | {
"error": {
"codigo": "NO_ENCONTRADO",
"mensaje": "No existe, no es de esta llave o ya caducó (los resultados se guardan 24 horas)."
}
} | DELETE /v1/llaves/{llave} GET /v1/trabajos/{trabajo} PATCH /v1/llaves/{llave} |
| 410 | RECOGIDA_NO_VALIDA | El vale está usado, caducado o no existe (RECOGIDA_NO_VALIDA). | {
"error": {
"codigo": "RECOGIDA_NO_VALIDA",
"mensaje": "Este enlace ya se ha usado, ha caducado o no existe. Si no recogiste tu llave, escríbenos."
}
} | POST /v1/recogida |
| 413 | DOCUMENTO_NO_VALIDO | El fichero pasa de 15 MB (DOCUMENTO_NO_VALIDO). | {
"error": {
"codigo": "DOCUMENTO_NO_VALIDO",
"mensaje": "El fichero pasa de 15 MB."
}
} | POST /v1/trabajos |
| 415 | DOCUMENTO_NO_VALIDO | El fichero no es PDF, JPEG ni PNG (DOCUMENTO_NO_VALIDO). | {
"error": {
"codigo": "DOCUMENTO_NO_VALIDO",
"mensaje": "El fichero no es PDF, JPEG ni PNG."
}
} | POST /v1/trabajos |
| 422 | DOCUMENTO_NO_VALIDO | Falta el fichero o está roto (DOCUMENTO_NO_VALIDO), o la petición está mal formada (PETICION_NO_VALIDA). | {
"error": {
"codigo": "DOCUMENTO_NO_VALIDO",
"mensaje": "Falta el campo «factura» o el fichero está vacío o roto."
}
} | POST /v1/trabajos |
| 422 | PETICION_NO_VALIDA | Falta el fichero o está roto (DOCUMENTO_NO_VALIDO), o la petición está mal formada (PETICION_NO_VALIDA). | {
"error": {
"codigo": "PETICION_NO_VALIDA",
"mensaje": "El campo «cif» no tiene forma de NIF (9 caracteres, letras y números)."
}
} | DELETE /v1/llaves/{llave} PATCH /v1/llaves/{llave} POST /v1/llaves POST /v1/recogida POST /v1/trabajos |
| 429 | RITMO_EXCEDIDO | Demasiadas peticiones (RITMO_EXCEDIDO). | {
"error": {
"codigo": "RITMO_EXCEDIDO",
"mensaje": "Demasiadas peticiones seguidas. Espera y reintenta.",
"reintentar_en_s": 5
}
} | DELETE /v1/llaves/{llave} GET /v1/llaves GET /v1/pool GET /v1/trabajos/{trabajo} PATCH /v1/llaves/{llave} POST /v1/llaves POST /v1/recogida POST /v1/trabajos |
| 500 | ERROR_INTERNO | Un fallo nuestro (ERROR_INTERNO). Nunca lleva trazas ni rutas internas. | {
"error": {
"codigo": "ERROR_INTERNO",
"mensaje": "No se pudo completar la petición por un fallo nuestro. No se ha descontado nada; puedes reintentar."
}
} | DELETE /v1/llaves/{llave} GET /v1/llaves GET /v1/pool GET /v1/trabajos/{trabajo} PATCH /v1/llaves/{llave} POST /v1/llaves POST /v1/recogida POST /v1/trabajos |
| 503 | ERROR_INTERNO | El motor de lectura no está disponible ahora mismo (ERROR_INTERNO); reintenta tras Retry-After. No se ha descontado nada. | {
"error": {
"codigo": "ERROR_INTERNO",
"mensaje": "El motor de lectura no está disponible ahora mismo. No se ha descontado nada.",
"reintentar_en_s": 30
}
} | POST /v1/trabajos |
| 503 | ERROR_INTERNO | El trabajo terminó, pero el motor de lectura no puede devolver el resultado ahora mismo (ERROR_INTERNO). El trabajo sigue guardado; vuelve a consultar tras Retry-After. | {
"error": {
"codigo": "ERROR_INTERNO",
"mensaje": "El resultado no se puede recoger ahora mismo. El trabajo sigue guardado: vuelve a consultarlo en unos segundos.",
"reintentar_en_s": 30
}
} | GET /v1/trabajos/{trabajo} |
Avisos
Un aviso no rechaza la factura: el trabajo llega terminado, con su documento, y
descuenta de la pool como cualquier otro; pero conviene revisarla antes de contabilizarla. Van en
documento.avisos, cada uno con su codigo estable y un mensaje. La lista crece: un
código que tu programa no conozca, trátalo como «revise el documento». Los de hoy (7):
| codigo | mensaje que recibes (… = el dato de tu factura) |
|---|---|
LECTURA_INCOMPLETA | La auditoría terminó sin que todas las comprobaciones cuadraran. Revise el documento antes de contabilizarlo. |
DESCUADRE | Los importes del documento no cuadran entre sí (bases, cuotas y total) ni tras releerlo. Puede que la factura esté mal calculada en origen. |
NIF_CONTRAPARTE_NO_LOCALIZADO | No se pudo localizar el NIF de la otra parte de la factura (el emisor si es recibida, el receptor si es emitida). |
CIERRE_DESCONOCIDO | No se pudo determinar cómo terminó la auditoría. Revise el documento. |
PAGINAS_NO_LEIDAS | El documento tiene … páginas y solo se leen las … primeras. |
EMPRESA_NO_APARECE | El NIF asociado a la llave (…) no aparece entre los identificadores leídos en el documento. Compruebe que la factura es de esa empresa. |
TIPO_DISCREPANTE | Se ha leído como …, como se indicó al subirla, pero en el documento la empresa de la llave aparece como …. Compruebe si es una compra o una venta. |
Referencia
Referencia interactiva (Swagger UI 5.24.2, servido desde este sitio) ·
openapi.yaml · respuesta.schema.json
(el documento v1).
Conectores
Código abierto (licencia MIT) para llevar el documento v1 a tu
programa de contabilidad:
munincloud-conector-csv— el documentov1a CSV genérico.munincloud-conector-a3— el documentov1a a3 (Importador de Datos e Importia).munincloud-conector-erpnext— el documentov1a ERPNext (Data Import de facturas de compra y venta).munincloud-conector-odoo— el documentov1a Odoo (importación de facturas en account.move).munincloud-conector-contasol— el documentov1a Contasol (ficheros IVS y APU de facturas recibidas).munincloud-conector-sage50— el documentov1a Sage 50 (importador de facturas en asientos).munincloud-conector-holded— el documentov1a Holded (plantilla de compras).
a3® es una marca de Wolters Kluwer. MuninCloud no está asociado a, ni certificado por, Wolters Kluwer. a3ASESOR®, a3innuva® e Importia® también son marcas de Wolters Kluwer. ERPNext® es una marca de Frappe Technologies Pvt. Ltd. MuninCloud no está asociado a, ni certificado por, Frappe Technologies Pvt. Ltd. Frappe® también es una marca de Frappe Technologies Pvt. Ltd. Odoo® es una marca de Odoo S.A. MuninCloud no está asociado a, ni certificado por, Odoo S.A. Contasol® es una marca de Software DELSOL. MuninCloud no está asociado a, ni certificado por, Software DELSOL. Sage® es una marca de Sage Global Services Limited o sus licenciantes. MuninCloud no está asociado a, ni certificado por, Sage Global Services Limited o sus licenciantes. Holded® es una marca de Holded. MuninCloud no está asociado a, ni certificado por, Holded.