Developer documentation
EspañolCreate administrative applications for your clients from your own software. You send us the data; your client reviews, signs and pays on our domain; we prepare and file the case.
What this API automates, and what it does not
Filing with the public administration is carried out by a Managora professional. The API automates the preparation of the case file, not the filing. Once the case file is complete, the order sits at siguiente_paso: "presentacion_por_gestor" with its deadline in working days. We do not dress it up as processing: there is a person behind it, and your product is better off saying so too.
Try it right here
Real calls against managora.net, with your test key. Nothing to install.
You need a key. We issue two, at different moments: first the test (mgk_sandbox_…) one and, once you have been through the whole flow, the production (mgk_live_…). Write to us at tramites@managora.net.
curl https://managora.net/api/partner/v1/me \
-H "Authorization: Bearer $MANAGORA_API_KEY"The response carries cobra_de_verdad. It is the field worth checking before anything else: it says whether that key charges money or is the test one.
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"The second returns the fields you have to send, with their type, their accepted values (opciones), their visibility conditions (visible_si) and the reasons why the service would not apply (no_elegible_si). That call is the living list: do not copy the fields by hand, because they change.
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" }
}'The full, verified example, with every real required field and valid values, is in the reference and in the Postman collection.
From there the link goes out by email to your client and you follow the order with GET /orders/{id} or by waiting for the webhooks.
A mgk_sandbox_ key runs through the same code as a real one: same checks, same case file, same screen and same generated document. The only difference is that nothing is charged.
Real in test mode
Not real in test mode
The emails really are sent, and that puts something on you
The recipient is whatever address you declare in datos. If you put one of your clients' addresses, that client receives a real email from Managora inviting them to sign an application. Test with mailboxes you control.
The case file that gets created is real, which is why the amount recorded is the one the gateway would have charged, not zero: that way you can check the numbers add up. When you finish testing, tell us and we purge them.
On your client's screen, where the payment button would be, there is one that reads Simulate the payment and continue, with a notice that it is a test. From there the application carries on the same: the document is generated and the webhooks go out.
The signing link is never returned to you, and that option will not exist
That link mints the access credential to your client's case file. If we handed it over, your server could redeem it and sign the authorisation on their behalf, and the document that evidences who signed would carry your server's metadata instead of the holder's. The signature is valid precisely because the stroke belongs to whoever signs.
The commercial consequence, said plainly: if your client does not open the link, there is no application. With enlace_enviado_en, enlace_caduca_en and enlace_abierto_en you can chase them from your own system, and resend it with POST /orders/{id}/hosted-link.
Five events, and only five. Each one is emitted from the place that writes that fact.
| Event | When |
|---|---|
order.signed | Your client has signed the authorisation. |
order.paid | The payment has come in. |
order.documents_complete | Your client has uploaded the last required document. |
order.submitted | A professional has filed it and written the real reference the authority returned. |
order.completed | The public administration has resolved and the case file is closed. |
You register them from the API itself (POST /webhooks) and you can edit them, rotate the secret, switch them off and switch them back on without writing to us.
Every webhook carries Managora-Signature: t=<epoch>,v1=<hex>. The value is an HMAC-SHA256 of <t>.<raw body> with the endpoint secret.
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;
}Test your validation on day one, not in production
POST /webhooks/{id}/test sends a signed webhook to your endpoint there and then, and returns what your server answered plus the exact body we signed so you can compare it byte for byte.
With {"evento": "order.submitted", "pedido_id": "…"} on a test order you get a sample of that specific event, shaped like the real one. It covers the two you cannot trigger yourself (order.submitted and order.completed), because we write those when we file and when we close.
Answer 2xx as soon as you receive and process afterwards. We time out at 10 seconds and do not follow redirects: a 3xx counts as a failure. We retry at 1 minute, 5, 30, 2 hours and 6 hours. Delivery is at-least-once: deduplicate on the Managora-Entrega-Id header, which is the same across every retry of the same webhook.
If your endpoint fails 20 times in a row we switch it off and tell you by email. You switch it back on with POST /webhooks/{id}/reactivate, which also requeues whatever did not arrive. And GET /events is the full history, with the body of every webhook.
Every error has the same shape. Branch on error.codigo, which is stable; the message is written for a human to read and may change.
{
"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)"]
}
]
}
}The most common ones. The full table, with all 52, is in the reference.
| HTTP | Code | What happened |
|---|---|---|
| 400 | campos_desconocidos | You are sending keys that do not exist on the service. The extra ones are listed in `detalles`. |
| 400 | idempotency_key_requerida | The header is missing, or is not between 8 and 200 characters. |
| 400 | servicio_fuera_de_su_catalogo | That service is not open to your account. The ones that are, in `detalles`. |
| 400 | url_no_valida | The webhook URL is not https, carries credentials or points to an internal host. |
| 401 | clave_desconocida | The key does not exist or the secret does not match. Check you copied it whole. |
| 403 | scope_insuficiente | Your key lacks the permission for that route. The ones it does have are in the message. |
| 404 | pedido_no_encontrado | That order is not yours or does not exist. We answer 404 and not 403 on purpose. |
| 405 | metodo_no_permitido | Wrong method on a route that does exist. The Allow header says which ones work. |
| 409 | cliente_no_elegible | For that data the service does not apply. You can anticipate it with `no_elegible_si`. |
| 409 | idempotency_key_reutilizada | That key was already used with a DIFFERENT body. Use a new one for the new order. |
| 409 | referencia_duplicada | You already have an order with that reference. It is unique per account and never reused. |
| 413 | cuerpo_demasiado_grande | The body is over 256 KB. Files do not travel in `datos`. |
| 422 | datos_incompletos | A required, visible field is missing. `detalles` carries the field, its label and why. |
| 422 | datos_invalidos | A value is not one we accept. `detalles` carries the valid `opciones`. |
| 429 | cuota_excedida | More than 120 requests in the last minute with that key. Wait for the Retry-After. |
| 500 | error_interno | Our fault. Retry with increasing backoff and write to us with the exact time. |
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, so you can slow down before you hit the wall.Idempotency-Key is required on every order. A retry with the same body returns the same order with reintento: true. With a different body it returns 409: that is not a retry, and handing you back the old order would make you believe the new data was saved.documentos_pendientes.The full reference is generated from the contract itself, so it never drifts from what the API actually does.