Documentación para desarrolladores
EnglishDé de alta trámites para sus clientes desde su propio software. Usted nos manda los datos; su cliente revisa, firma y paga en nuestro dominio; nosotros preparamos y presentamos el expediente.
Qué automatiza esta API y qué no
La presentación ante la Administración la realiza un profesional de Managora. La API automatiza la preparación del expediente, no la presentación. Cuando el expediente está completo, el pedido queda en siguiente_paso: "presentacion_por_gestor" con su plazo en días hábiles. No lo disfrazamos de processing: hay una persona detrás y conviene que su producto lo cuente igual.
Pruébela aquí mismo
Llamadas reales contra managora.net, con su clave de pruebas. Sin instalar nada.
Necesita una clave. Se emiten dos, en momentos distintos: primero la de pruebas (mgk_sandbox_…) y, cuando haya recorrido el flujo entero, la de producción (mgk_live_…). Escríbanos a tramites@managora.net.
curl https://managora.net/api/partner/v1/me \
-H "Authorization: Bearer $MANAGORA_API_KEY"La respuesta trae cobra_de_verdad. Es el campo que conviene mirar antes de nada: dice si esa clave cobra o si es la de pruebas.
curl https://managora.net/api/partner/v1/services \
-H "Authorization: Bearer $MANAGORA_API_KEY"
curl https://managora.net/api/partner/v1/services/titulo_nautico_per \
-H "Authorization: Bearer $MANAGORA_API_KEY"El segundo devuelve los campos que hay que mandar, con su tipo, sus valores admitidos (opciones), sus condiciones de visibilidad (visible_si) y las causas por las que el trámite no procedería (no_elegible_si). Esa llamada es la lista viva: no copie los campos a mano, porque cambian.
curl -X POST https://managora.net/api/partner/v1/orders \
-H "Authorization: Bearer $MANAGORA_API_KEY" \
-H "Idempotency-Key: pedido-4417" \
-H "Content-Type: application/json" \
-d '{
"tramite": "titulo_nautico_per",
"referencia": "4417",
"datos": { "...": "GET /services/titulo_nautico_per" }
}'El ejemplo completo y verificado, con todos los campos obligatorios reales y valores válidos, está en la referencia y en la colección de Postman.
A partir de ahí, el enlace sale por correo a su cliente y usted sigue el pedido con GET /orders/{id} o esperando los webhooks.
Una clave mgk_sandbox_ recorre el mismo código que una real: mismas comprobaciones, mismo expediente, misma pantalla y mismo escrito. Lo único que cambia es que no hay cobro.
Es real en pruebas
No es real en pruebas
Los correos salen de verdad, y esto le obliga a algo
El destinatario es la dirección que usted declare en datos. Si pone la de un cliente suyo, ese cliente recibe un aviso real de Managora invitándole a firmar un trámite. Pruebe con buzones que controle usted.
El expediente que se crea es real, y por eso el importe que se registra es el que habría cobrado la pasarela, no cero: así puede comprobar que los números cuadran. Cuando termine sus pruebas, dígalo y los purgamos.
En la pantalla de su cliente, donde iría el botón de pago, sale uno que dice Simular el pago y continuar, con el aviso de que es una prueba. A partir de ahí el trámite sigue igual: se genera el escrito y salen los webhooks.
El enlace de firma no se le devuelve a usted, y no va a existir esa opción
Ese enlace acuña la credencial de acceso al expediente de su cliente. Si se lo diéramos, su servidor podría canjearlo y firmar el mandato en su nombre, y el documento que acredita quién firmó llevaría grabados los metadatos de su servidor en vez de los del titular. La firma vale precisamente porque el trazo es de quien firma.
Consecuencia comercial, dicha claramente: si su cliente no abre el enlace, no hay trámite. Con enlace_enviado_en, enlace_caduca_en y enlace_abierto_en puede perseguirlo desde su propio sistema, y reenviárselo con POST /orders/{id}/hosted-link.
Cinco eventos, y solo cinco. Cada uno se emite desde el sitio que escribe ese hecho.
| Evento | Cuándo |
|---|---|
order.signed | Su cliente ha firmado el mandato. |
order.paid | Ha entrado el pago. |
order.documents_complete | Su cliente ha subido el último documento obligatorio. |
order.submitted | Un profesional lo ha presentado y ha escrito la referencia real del organismo. |
order.completed | La Administración ha resuelto y el expediente se ha cerrado. |
Se registran desde la propia API (POST /webhooks) y se pueden editar, rotar el secreto, apagar y volver a encender sin escribirnos.
Cada aviso lleva Managora-Signature: t=<epoch>,v1=<hex>. El valor es un HMAC-SHA256 de <t>.<cuerpo crudo> con el secreto del endpoint.
const crypto = require("crypto");
function firmaValida(cuerpoCrudo, cabecera, secreto) {
const partes = String(cabecera).split(",").map((p) => p.trim());
const t = Number(partes.find((p) => p.startsWith("t="))?.slice(2));
if (!Number.isFinite(t)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;
const esperada = crypto
.createHmac("sha256", secreto)
.update(`${t}.${cuerpoCrudo}`)
.digest("hex");
return partes
.filter((p) => p.startsWith("v1="))
.some((p) => {
const a = Buffer.from(esperada, "utf8");
const b = Buffer.from(p.slice(3), "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}<?php
function firma_valida(string $cuerpoCrudo, string $cabecera, string $secreto): bool {
$partes = array_map('trim', explode(',', $cabecera));
$t = null;
foreach ($partes as $p) {
if (str_starts_with($p, 't=')) { $t = (int) substr($p, 2); }
}
if ($t === null || abs(time() - $t) > 300) { return false; }
$esperada = hash_hmac('sha256', $t . '.' . $cuerpoCrudo, $secreto);
foreach ($partes as $p) {
if (str_starts_with($p, 'v1=') && hash_equals($esperada, substr($p, 3))) {
return true;
}
}
return false;
}Pruebe su validación el primer día, no en producción
POST /webhooks/{id}/test manda un aviso firmado a su endpoint en ese momento y le devuelve lo que contestó su servidor, más el cuerpo exacto que hemos firmado para que lo compare byte a byte.
Con {"evento": "order.submitted", "pedido_id": "…"} sobre un pedido de pruebas recibe una muestra de ese evento concreto, con la forma del real. Sirve para los dos que no puede provocar usted (order.submitted y order.completed), porque los escribimos nosotros al presentar y al cerrar.
Conteste 2xx en cuanto reciba y procese después. Cortamos a los 10 segundos y no seguimos redirecciones: un 3xx lo contamos como fallo. Reintentamos a 1 minuto, 5, 30, 2 horas y 6 horas. La entrega es at-least-once: deduplique por la cabecera Managora-Entrega-Id header, que es la misma en todos los reintentos del mismo aviso.
Si su endpoint falla 20 veces seguidas lo apagamos y se lo decimos por correo. Lo vuelve a encender con POST /webhooks/{id}/reactivate, que además reencola lo que no llegó. Y GET /events es el historial completo, con el cuerpo de cada aviso.
Todos los errores tienen la misma forma. Ramifique por error.codigo, que es estable; el mensaje está escrito para que lo lea una persona y puede cambiar.
{
"error": {
"codigo": "datos_invalidos",
"mensaje": "Hay campos con valores que no admitimos.",
"campo": "datos",
"detalles": [
{
"campo": "lista",
"etiqueta": "Lista del registro",
"motivo": "\"Lista 7\" no es uno de los valores admitidos.",
"opciones": ["Lista 3 (pesca)", "Lista 6 (alquiler)", "Lista 7 (recreo)"]
}
]
}
}Los más habituales. La tabla completa, con los 52, está en la referencia.
| HTTP | Código | Qué ha pasado |
|---|---|---|
| 400 | campos_desconocidos | Manda claves que no existen en la ficha. La lista sobrante va en `detalles`. |
| 400 | idempotency_key_requerida | Falta la cabecera, o no tiene entre 8 y 200 caracteres. |
| 400 | servicio_fuera_de_su_catalogo | Ese trámite no está abierto a su cuenta. Los que sí, en `detalles`. |
| 400 | url_no_valida | La URL del webhook no es https, lleva credenciales o apunta a un host interno. |
| 401 | clave_desconocida | La clave no existe o el secreto no casa. Revise que la copió entera. |
| 403 | scope_insuficiente | Su clave no tiene el permiso de esa ruta. Los que sí tiene van en el mensaje. |
| 404 | pedido_no_encontrado | Ese pedido no es suyo o no existe. Respondemos 404 y no 403 a propósito. |
| 405 | metodo_no_permitido | Método equivocado en una ruta que existe. La cabecera Allow dice cuáles valen. |
| 409 | cliente_no_elegible | Para esos datos el trámite no procede. Se puede anticipar con `no_elegible_si`. |
| 409 | idempotency_key_reutilizada | Esa clave ya se usó con OTRO cuerpo. Use una nueva para el pedido nuevo. |
| 409 | referencia_duplicada | Ya tiene un pedido con esa referencia. Es única por cuenta y no se reutiliza. |
| 413 | cuerpo_demasiado_grande | El cuerpo pasa de 256 KB. Los ficheros no van en `datos`. |
| 422 | datos_incompletos | Falta un campo obligatorio y visible. `detalles` trae campo, etiqueta y motivo. |
| 422 | datos_invalidos | Un valor no es de los admitidos. `detalles` trae las `opciones` válidas. |
| 429 | cuota_excedida | Más de 120 peticiones en el último minuto con esa clave. Espere el Retry-After. |
| 500 | error_interno | Fallo nuestro. Reintente con espera creciente y escríbanos con la hora exacta. |
X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset, para que frene antes de chocar.Idempotency-Key obligatoria en cada pedido. Un reintento con el mismo cuerpo devuelve el mismo pedido con reintento: true. Con un cuerpo distinto devuelve 409: eso no es un reintento, y darle por bueno el pedido viejo le haría creer que se guardó lo nuevo.documentos_pendientes.La referencia completa se genera del propio contrato, así que no se desincroniza de lo que la API hace de verdad.