Developer documentation

Español

The Managora services API

Create 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.

The email is really sent. A real case file is created in test mode and the recipient gets an email from Managora inviting them to complete the application. Nothing is charged, no invoice is issued and nothing is filed with the public administration.

Getting started

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.

1

Check the key and which environment it is on

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.

2

Read the catalogue and the field contract

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.

3

Create the order

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.

The test environment

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

  • The case file and every validation.
  • The email to your client. It really is sent.
  • The client screen and the signing of the authorisation.
  • Document uploads.
  • The webhooks, signed the same way.
  • The document that gets generated.

Not real in test mode

  • The payment: no gateway opens and no card is ever requested.
  • The invoice: none is issued.
  • The filing: we do not file anything with the authorities.

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 flow, and why it works this way

  1. You create the order with your client's data. We check that the service applies, that the values are among the accepted ones and that the document generator can draft with them. If anything fails, it is rejected before anything is created.
  2. We send the link to your client, by email. It is personal, single-use and expires (24 to 168 hours, your choice).
  3. Your client reviews, signs and pays on managora.net, in their own browser.
  4. A Managora professional prepares and files the case file and records the real reference the authority returns.

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.

Webhooks

Five events, and only five. Each one is emitted from the place that writes that fact.

EventWhen
order.signedYour client has signed the authorisation.
order.paidThe payment has come in.
order.documents_completeYour client has uploaded the last required document.
order.submittedA professional has filed it and written the real reference the authority returned.
order.completedThe 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.

The signature

Every webhook carries Managora-Signature: t=<epoch>,v1=<hex>. The value is an HMAC-SHA256 of <t>.<raw body> with the endpoint secret.

  • Lowercase hexadecimal.
  • The secret is used as UTF-8 bytes, including the whsec_ prefix and without decoding it.
  • Sign the raw body you receive, never the re-serialised JSON: it is the number one mistake when validating an HMAC.
  • Reject anything that arrives more than 5 minutes old.
  • During a rotation two v1 values are sent and validating one is enough.
Node
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
<?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.

Delivery

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.

Errors

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.

HTTPCodeWhat happened
400campos_desconocidosYou are sending keys that do not exist on the service. The extra ones are listed in `detalles`.
400idempotency_key_requeridaThe header is missing, or is not between 8 and 200 characters.
400servicio_fuera_de_su_catalogoThat service is not open to your account. The ones that are, in `detalles`.
400url_no_validaThe webhook URL is not https, carries credentials or points to an internal host.
401clave_desconocidaThe key does not exist or the secret does not match. Check you copied it whole.
403scope_insuficienteYour key lacks the permission for that route. The ones it does have are in the message.
404pedido_no_encontradoThat order is not yours or does not exist. We answer 404 and not 403 on purpose.
405metodo_no_permitidoWrong method on a route that does exist. The Allow header says which ones work.
409cliente_no_elegibleFor that data the service does not apply. You can anticipate it with `no_elegible_si`.
409idempotency_key_reutilizadaThat key was already used with a DIFFERENT body. Use a new one for the new order.
409referencia_duplicadaYou already have an order with that reference. It is unique per account and never reused.
413cuerpo_demasiado_grandeThe body is over 256 KB. Files do not travel in `datos`.
422datos_incompletosA required, visible field is missing. `detalles` carries the field, its label and why.
422datos_invalidosA value is not one we accept. `detalles` carries the valid `opciones`.
429cuota_excedidaMore than 120 requests in the last minute with that key. Wait for the Retry-After.
500error_internoOur fault. Retry with increasing backoff and write to us with the exact time.

Limits and good practice

  • 120 requests per minute per key. Every response carries 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.
  • The reference is unique per account and is never reused, not even for the same client.
  • Files do not travel through the API. Your client uploads them from their own screen, after paying, and you see what is missing in documentos_pendientes.
  • Everything is JSON, errors included. A mistyped route or a wrong method returns the same envelope, never an HTML page.

To integrate

The full reference is generated from the contract itself, so it never drifts from what the API actually does.