MiPlato. Desarrolladores
API de Integración · v1.0.4

Conecte su sistema de gestión con MiPlato

La API REST de MiPlato permite que cualquier ERP, punto de venta o sistema propio publique su catálogo en el menú online del comercio, reciba los pedidos en el momento y actualice su estado, sin intervención manual.

URL base https://app.miplato.com.py/api/v1
  • REST + JSON sobre HTTPS
  • Webhooks firmados con HMAC-SHA256
  • Feed de eventos para sistemas sin IP pública
  • Escrituras idempotentes y seguras ante reintentos

Qué puede hacer su sistema

1

Publicar el catálogo

Categorías, productos, precios, disponibilidad, fotos y adicionales, usando los códigos de su propio sistema.

2

Recibir los pedidos

Por webhook en el momento, o consultando el feed de eventos. Cada pedido llega completo, con cliente, dirección, pago e ítems.

3

Confirmar y registrar

Informe que el pedido se importó (o por qué no se pudo), con el número de venta de su sistema.

4

Actualizar el estado

Aceptado, en preparación, en camino, entregado. El cliente lo sigue en vivo desde su celular.

Principios de la integración. Todo intercambio se hace por HTTP: ningún sistema externo accede a la base de datos de MiPlato, y MiPlato nunca escribe en la base de su sistema. Cada credencial pertenece a un único comercio. Si su sistema queda fuera de servicio, el comercio sigue recibiendo y gestionando pedidos desde el panel de MiPlato.

Inicio rápido

  1. Obtenga una credencial

    Para desarrollar, use el ambiente de homologación: allí recibe una tienda de prueba y genera su propio token de pruebas. En producción, la credencial la genera cada comercio:

    El comercio ingresa a su panel en Panel → Integraciones → Conectar un sistema, elige «Sistema propio / otro ERP» (o el sistema que corresponda), define los permisos y el modo de catálogo, y copia la credencial. La credencial se muestra una sola vez y tiene este formato:

    Credencial
    mpk_live_xxxxxxxxxxxxxxxxxxxxxxxx.mps_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

    Su sistema debe guardarla cifrada y nunca mostrarla en pantallas ni registros. Si su sistema atiende a varios comercios, guarde una credencial por comercio.

  2. Verifique la conexión

    La primera llamada válida activa la integración y confirma con qué comercio está conectado.

    cURL
    curl https://app.miplato.com.py/api/v1/me \
      -H "Authorization: Bearer $MIPLATO_TOKEN"
  3. Publique el catálogo

    Con la integración en modo erp_managed, envíe primero las categorías y después los productos, identificados por los códigos de su sistema (external_id).

    cURL
    curl -X PUT https://app.miplato.com.py/api/v1/catalog/categories/G-12 \
      -H "Authorization: Bearer $MIPLATO_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "Lomitos", "position": 1, "active": true}'
    
    curl -X PUT https://app.miplato.com.py/api/v1/catalog/products/P-55 \
      -H "Authorization: Bearer $MIPLATO_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "Lomito árabe", "category_external_id": "G-12", "price": "35000"}'
  4. Reciba los pedidos

    Configure un webhook (ver Webhooks) o consulte periódicamente el feed de eventos. Use siempre el id del pedido (ord_…) como clave única en su sistema para no duplicarlo.

  5. Confirme la importación y actualice el estado

    cURL
    curl -X POST https://app.miplato.com.py/api/v1/orders/ord_01JA9Z.../ack \
      -H "Authorization: Bearer $MIPLATO_TOKEN" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: ack-ord_01JA9Z..." \
      -d '{"external_id": "VENTA-88231", "external_number": "88231"}'
    
    curl -X POST https://app.miplato.com.py/api/v1/orders/ord_01JA9Z.../status \
      -H "Authorization: Bearer $MIPLATO_TOKEN" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: st-ord_01JA9Z...-accepted" \
      -d '{"status": "accepted", "estimated_minutes": 30}'

Flujo de integración

El recorrido completo de un pedido entre MiPlato y su sistema:

Su sistemaCatálogoPUT productos o POST /catalog/batch
MiPlatoPedido nuevoEl cliente pide en el menú online. Evento order.created
Su sistemaImportaciónPOST /orders/{id}/ack o /import-failure
AmbosEstadoPOST /orders/{id}/status · evento order.status_changed

¿Webhook o feed?

Webhook: MiPlato envía cada evento a una URL HTTPS pública de su sistema, al instante. Requiere que su servidor sea accesible desde internet.

Feed: su sistema consulta GET /v1/events cada 15 a 30 segundos. Ideal para sistemas instalados en una red local o detrás de NAT. Recomendamos usar el feed también como conciliación aunque tenga webhook (por ejemplo, cada 5 minutos).

Red de seguridad

GET /v1/orders?ack_status=pending devuelve los pedidos que su sistema todavía no confirmó. Consúltelo al iniciar su sistema y periódicamente: así ningún pedido queda sin importar, aunque se haya perdido un webhook.

Ambiente de homologación

Desarrolle y pruebe su integración en un ambiente separado del MiPlato oficial, con base de datos propia, una tienda de prueba y datos ficticios. Nada de lo que haga allí afecta a comercios reales.

AmbienteURL de la APIToken
Homologaciónhttps://sandbox.miplato.com.py/api/v1mpk_test_…, generado por usted en la consola de homologación
Producciónhttps://app.miplato.com.py/api/v1mpk_live_…, generado por cada comercio en su panel
  1. Solicite el acceso en https://sandbox.miplato.com.py/homologacion con los datos de su empresa y de su sistema.
  2. Con la solicitud aprobada, ingrese con su correo y su contraseña. En el menú Homologación encontrará la URL de la API, el botón para generar su token de pruebas y el simulador de pedidos (delivery, retiro con factura y producto sin código).
  3. El reporte de homologación se completa solo, con los registros reales de sus llamadas: conexión, catálogo, recepción de pedidos, rechazo de firmas inválidas, importación, estados y reintentos.
  4. Con todas las pruebas obligatorias aprobadas, emita el certificado de homologación, verificable por su código.
Paso a producción: su sistema sólo cambia la URL de la API y el token, que cada comercio genera en Panel → Integraciones → Conectar un sistema. Los tokens de homologación nunca funcionan en producción, ni al revés: un error de configuración no puede afectar a un comercio real.

Convenciones

URL basehttps://app.miplato.com.py/api/v1. Todas las rutas de esta documentación son relativas a /v1.
TransporteSólo HTTPS.
FormatoContent-Type: application/json; charset=utf-8 en solicitudes y respuestas. Textos en UTF-8.
Campossnake_case. Su sistema debe ignorar los campos que no conozca en las respuestas (la v1 sólo agrega campos compatibles). En escritura, un campo desconocido se rechaza con 422 para evitar errores de tipeo silenciosos.
MontosTexto decimal con punto y sin separador de miles: "15000" o "15000.50" (hasta 2 decimales). Nunca números de punto flotante. Moneda: PYG.
FechasISO 8601 con desplazamiento explícito, por ejemplo 2026-10-12T21:30:00-03:00. Las respuestas usan el huso America/Asuncion. Lea siempre el desplazamiento informado; no asuma uno fijo.
IdentificadoresOpacos, con prefijo y 26 caracteres: ord_01JA2B3C4D5E6F7G8H9J0KMNPQ. Prefijos: str_ comercio, int_ integración, cat_ categoría, prd_ producto, ogr_ grupo de adicionales, opt_ adicional, ord_ pedido, evt_ evento, dlv_ entrega de webhook, req_ solicitud.
Sus códigosexternal_id: de 1 a 64 caracteres A-Z a-z 0-9 . _ : -, único por integración y sensible a mayúsculas. Es el código de su sistema; MiPlato guarda la relación con sus propios IDs.
PaginaciónPor cursor: ?limit=50&cursor=…. La respuesta trae next_cursor (null en la última página). limit máximo: 200.
Límites120 solicitudes por minuto por integración (encabezados X-RateLimit-Limit y X-RateLimit-Remaining). Cuerpo máximo: 5 MB.
RastreoCada respuesta incluye X-Request-Id: req_…. Indíquelo al contactar a soporte.

Respuestas exitosas:

Recurso
{ "data": { "id": "ord_01JA9Z…", "…": "…" } }
Lista
{ "data": [ { "…": "…" } ], "next_cursor": "c_MTIz" }

Autenticación

Envíe la credencial completa en el encabezado Authorization de cada solicitud:

Encabezado
Authorization: Bearer mpk_live_7Hq2…Zx.mps_live_9fK3…aQ
  • Las credenciales de producción empiezan con mpk_live_ / mps_live_. Las de un entorno de pruebas, con mpk_test_ / mps_test_. Una credencial de pruebas nunca funciona en producción, ni al revés.
  • MiPlato guarda sólo un resumen criptográfico (SHA-256) del secreto: no puede volver a mostrarlo. Si se pierde, el comercio genera una nueva.
  • Credencial ausente, inválida, vencida o revocada → 401 unauthenticated. Después de 30 fallas por minuto desde la misma IP, las solicitudes reciben 429 rate_limited hasta que termine el minuto.
  • Integración suspendida → 403 integration_suspended. Comercio suspendido → 403 store_suspended.

Rotación de la credencial

El comercio puede rotar la credencial en el panel sin detener la integración: durante un período de superposición (24 horas de forma predeterminada, configurable de 0 a 72) la credencial anterior y la nueva funcionan al mismo tiempo. Mientras su sistema use una credencial que está por vencer, GET /v1/me devuelve credential.expires_at con la fecha de vencimiento: úselo para alertar al usuario. Procedimiento recomendado:

  1. El comercio rota la credencial en el panel.
  2. Carga la nueva credencial en su sistema.
  3. Su sistema la verifica con GET /v1/me.
  4. La anterior vence sola al terminar la superposición.

Permisos

Cada integración tiene permisos (scopes) que el comercio define al conectarla y puede cambiar en el panel. Si falta el permiso necesario, la respuesta es 403 insufficient_scope.

PermisoHabilita
store:readLeer los datos operativos del comercio y recibir el evento store.updated.
store:writeAbrir o pausar la recepción de pedidos.
catalog:readLeer categorías y productos.
catalog:writeCrear, actualizar y desactivar categorías, productos, precios, disponibilidad y adicionales.
orders:readListar y consultar pedidos, leer el feed y recibir los eventos de pedidos.
orders:writeConfirmar la importación, informar fallas y cambiar el estado de los pedidos.
webhooks:manageConfigurar el webhook y reenviar entregas.

Configuración de la integración

El comercio la define en el panel; su sistema la lee en GET /v1/me y recibe el evento integration.updated cuando cambia.

ClaveValoresEfecto
catalog_modemanual (predet.), erp_managedEn erp_managed su sistema es el dueño del catálogo y esos campos quedan bloqueados en el panel. En manual las escrituras de catálogo responden 403 field_not_managed.
order_status_authorityboth (predet.), erp, miplatoQuién puede cambiar el estado de los pedidos. Con miplato, POST /status responde 403 status_authority_denied.
missing_items_policykeep (predet.), deactivateQué pasa, en una sincronización completa, con los ítems que no vinieron (ver Lote).
ack_expectedon_receipt (predet.), after_acceptanceCuándo se espera la confirmación de importación: al recibir el pedido, o recién después de que el comercio lo acepta.
ack_alert_minutes1 a 1440 (predet. 5)Minutos sin confirmación antes de que el panel avise «pedido no registrado en el sistema».

Errores

Todos los errores tienen el mismo formato. Use code para el tratamiento automático; message está en español y puede mostrarse al usuario.

Error 422
{
  "error": {
    "code": "validation_failed",
    "message": "Uno o más campos son inválidos.",
    "details": [
      { "field": "price", "code": "invalid", "message": "Debe ser un decimal positivo en texto, p. ej. \"15000\"." }
    ],
    "request_id": "req_01JA2B…"
  }
}
HTTPcodeCuándo
400malformed_jsonEl cuerpo no es un objeto JSON válido.
400idempotency_key_required, invalid_idempotency_keyFalta el encabezado Idempotency-Key donde es obligatorio, o su formato es inválido.
400invalid_cursorEl cursor de paginación no es válido.
401unauthenticatedCredencial ausente, inválida, vencida o revocada.
403insufficient_scope, integration_suspended, store_suspended, status_authority_denied, field_not_managedLa credencial es válida, pero la operación no está permitida.
404not_foundEl recurso no existe o pertenece a otro comercio (nunca se revela cuál de los dos).
409idempotency_conflict, request_in_progressMisma Idempotency-Key con otro cuerpo, o la primera solicitud todavía se procesa.
409invalid_status_transitionLa transición de estado no está permitida (details trae current_status).
409external_id_conflict, already_acknowledgedEl pedido ya fue confirmado con otro código, o ya fue confirmado.
409category_not_empty, already_linkedCategoría con productos activos; producto ya vinculado a otro código.
409batch_in_progress, snapshot_closed, snapshot_out_of_orderReglas del lote.
409delivery_in_progressLa entrega de webhook se está enviando en este momento.
413payload_too_largeCuerpo mayor a 5 MB o lote con más de 500 operaciones.
422validation_failedDatos inválidos; details trae cada campo.
429rate_limitedSe superó el límite. Espere los segundos de Retry-After.
500internal_errorError inesperado. Puede repetirse con seguridad si la solicitud es idempotente.
503maintenanceNo disponible temporalmente (ver Retry-After).
Cuándo reintentar. Sólo ante 429, 500, 502, 503, 504 o fallas de red, con espera exponencial (por ejemplo 1 s, 2 s, 4 s, 8 s…) y respetando Retry-After. Nunca repita un error 4xx sin corregir la causa.

Idempotencia

Las redes fallan: su sistema debe poder repetir una solicitud sin duplicar el efecto.

  • PUT por external_id es idempotente por naturaleza: enviar el mismo producto dos veces deja el mismo resultado.
  • Encabezado Idempotency-Key (8 a 100 caracteres: letras, números, - _ . :; se recomienda un UUID). Es obligatorio en /ack, /import-failure, /status y /catalog/batch, y opcional en los demás POST y PATCH.
    • Misma clave y mismo cuerpo, dentro de 7 días: devuelve la misma respuesta que la primera vez, con el encabezado Idempotent-Replay: true.
    • Misma clave con otro cuerpo: 409 idempotency_conflict.
    • Misma clave mientras la primera solicitud todavía se procesa: 409 request_in_progress con Retry-After.
    • Las respuestas 5xx no se guardan: la misma clave puede reintentarse de inmediato.
  • Cambios fuera de orden: categorías y productos aceptan source_updated_at (fecha del último cambio en su sistema). Si llega una versión más antigua que la ya aplicada, no se aplica y la respuesta es 200 con "result": "ignored_stale". Así puede reenviar colas atrasadas con seguridad.
  • Pedidos: use el id del pedido (ord_…) como clave única en su sistema. El mismo pedido puede llegar más de una vez (webhook, feed, reintento) y nunca debe generar dos ventas.

Integración y comercio

#GET/v1/mecualquier credencial

Confirma el vínculo y devuelve la integración, sus permisos, su configuración y el comercio.

Respuesta 200
{
  "data": {
    "integration": {
      "id": "int_01JA…",
      "system": "custom",
      "status": "active",
      "scopes": ["store:read", "catalog:read", "catalog:write", "orders:read", "orders:write", "webhooks:manage"],
      "settings": {
        "catalog_mode": "erp_managed",
        "order_status_authority": "both",
        "missing_items_policy": "keep",
        "ack_alert_minutes": 5,
        "ack_expected": "on_receipt"
      },
      "environment": "live",
      "credential": { "key_id": "mpk_live_7Hq2…", "expires_at": null }
    },
    "store": { "id": "str_01JA…", "slug": "lomiteria-nanduti", "name": "Lomitería Ñandutí" },
    "api_version": "1.0"
  }
}
#GET/v1/storestore:read

Datos operativos del comercio: si está recibiendo pedidos, modalidades de entrega y formas de pago.

Respuesta 200
{
  "data": {
    "id": "str_01JA…",
    "slug": "lomiteria-nanduti",
    "name": "Lomitería Ñandutí",
    "timezone": "America/Asuncion",
    "currency": "PYG",
    "accepting_orders": true,
    "fulfillment_types": ["delivery", "pickup"],
    "payment_methods": [
      { "code": "cash", "name": "Efectivo" },
      { "code": "bank_transfer", "name": "Transferencia bancaria" },
      { "code": "card_on_delivery", "name": "Tarjeta (POS al entregar)" }
    ]
  }
}

Los code de pago son estables: relaciónelos con las formas de pago de su sistema. Códigos base: cash, bank_transfer, card_on_delivery, qr, other. Un comercio puede tener códigos propios con el prefijo custom_.

#PATCH/v1/storestore:write

Abre o pausa la recepción de pedidos. Por ejemplo, su sistema puede pausar al cerrar la caja. Acepta sólo accepting_orders. Si el comercio no cumple los requisitos para abrir (por ejemplo, sin productos), responde 422 con el detalle.

Solicitud
{ "accepting_orders": false }

Lote y sincronización completa

#POST/v1/catalog/batchcatalog:writeIdempotency-Key

Hasta 500 operaciones por llamada, procesadas en el orden enviado. Cada operación tiene su propio resultado: si una falla, las demás se aplican igual.

Operaciones: upsert_category, deactivate_category, upsert_product, patch_product, deactivate_product, patch_option. El campo data lleva el mismo cuerpo que el endpoint individual.

Solicitud
{
  "operations": [
    { "op": "upsert_category", "external_id": "G-12",
      "data": { "name": "Lomitos", "position": 1 } },
    { "op": "upsert_product", "external_id": "P-55",
      "data": { "name": "Lomito árabe",
                "category_external_id": "G-12",
                "price": "35000" } },
    { "op": "patch_product", "external_id": "P-56",
      "data": { "available": false } },
    { "op": "deactivate_product", "external_id": "P-90" }
  ]
}
Respuesta 200
{
  "data": {
    "summary": { "total": 4, "succeeded": 3, "failed": 1 },
    "results": [
      { "index": 0, "external_id": "G-12", "result": "unchanged", "id": "cat_01JA…" },
      { "index": 1, "external_id": "P-55", "result": "created", "id": "prd_01JA…" },
      { "index": 2, "external_id": "P-56", "result": "updated", "id": "prd_01JB…" },
      { "index": 3, "external_id": "P-90", "result": "error",
        "error": { "code": "not_found", "message": "Producto no encontrado." } }
    ],
    "sync_run_id": "snc_01JA…"
  }
}
LímiteValorAl superarlo
Operaciones por lote500413 payload_too_large; no se procesa ninguna.
Lotes simultáneos por integración1409 batch_in_progress con Retry-After.
Lotes por minuto10429 rate_limited.
Adicionales por producto20 grupos × 50 opcionesEsa operación falla con error; el resto sigue.

Sincronización completa (full_snapshot)

Con "full_snapshot": true, los ítems vinculados a su sistema que no vengan en el envío reciben el tratamiento de missing_items_policy: keep (no se hace nada) o deactivate. Para catálogos de más de 500 ítems, divida el envío en partes:

Envío en tres partes
{ "full_snapshot": true, "snapshot_id": "sync-2026-10-12", "snapshot_part": 1, "operations": [ … ] }
{ "full_snapshot": true, "snapshot_id": "sync-2026-10-12", "snapshot_part": 2, "operations": [ … ] }
{ "full_snapshot": true, "snapshot_id": "sync-2026-10-12", "snapshot_part": 3, "snapshot_last": true, "operations": [ … ] }
  • Las partes deben llegar en orden (1, 2, 3…). Fuera de orden → 409 snapshot_out_of_order con details.expected_part. Reenviar la misma parte es seguro.
  • La política se aplica sólo al llegar la última parte, y sólo si ninguna operación falló. Si hubo errores, no se desactiva nada ("reason": "failed_operations"): corrija y envíe un snapshot nuevo con otro snapshot_id.
  • Un snapshot incompleto vence a las 2 horas sin desactivar nada.
  • Un ítem enviado con error de validación cuenta como presente: nunca se desactiva por un error.

Vincular productos ya existentes

Si el comercio ya tenía su menú cargado en MiPlato, vincule cada producto existente con su código en lugar de crear un duplicado: use link_to_id con el id del producto (obtenido en GET /catalog/products).

Operación
{ "op": "upsert_product", "external_id": "P-55", "link_to_id": "prd_01JA…", "data": { "name": "Lomito árabe", "category_external_id": "G-12", "price": "35000" } }

Pedidos

Los pedidos nacen en MiPlato cuando el cliente confirma la compra. Su sistema recibe una copia completa, confirma la importación y, si la configuración lo permite, actualiza el estado.

El objeto Pedido

Pedido
{
  "id": "ord_01JA9Z…",
  "number": 1042,
  "store_id": "str_01JA…",
  "status": "pending",
  "status_reason": null,
  "fulfillment_type": "delivery",
  "estimated_minutes": null,
  "created_at": "2026-10-12T21:40:12-03:00",
  "updated_at": "2026-10-12T21:40:12-03:00",
  "scheduled_for": null,
  "customer": {
    "name": "María González",
    "phone": "+595981123456",
    "billing": { "document_type": "ruc", "document": "80012345-6", "business_name": "María González" }
  },
  "delivery_address": {
    "street": "Av. Mariscal López",
    "number": "1234",
    "neighborhood": "Villa Morra",
    "city": "Asunción",
    "reference": "Portón verde, frente a la farmacia",
    "latitude": null,
    "longitude": null
  },
  "payment": { "method_code": "cash", "method_name": "Efectivo", "change_for": "100000", "status": "unpaid" },
  "items": [
    {
      "line": 1,
      "product": { "id": "prd_01JA…", "external_id": "P-55", "sku": "7840000000123" },
      "name": "Lomito árabe",
      "unit_price": "35000",
      "quantity": 2,
      "options": [
        {
          "group_name": "Extras",
          "group_external_id": "ADIC-EXTRA",
          "option": { "id": "opt_01JA…", "external_id": "EX-HUEVO" },
          "name": "Huevo",
          "unit_price": "4000",
          "quantity": 1
        }
      ],
      "notes": "Sin cebolla",
      "line_total": "78000"
    }
  ],
  "notes": "Tocar el timbre",
  "totals": { "subtotal": "78000", "delivery_fee": "10000", "discount": "0", "total": "88000", "currency": "PYG" },
  "integration": {
    "external_id": null,
    "external_number": null,
    "ack_status": "pending",
    "acknowledged_at": null,
    "last_import_error": null
  },
  "status_history": [
    { "status": "pending", "at": "2026-10-12T21:40:12-03:00", "by": "customer", "reason": null }
  ]
}
  • number es correlativo por comercio, para pantallas e impresión. La identificación técnica es siempre id.
  • Nombres y precios de items son una copia del momento de la compra: los cambios posteriores del catálogo no afectan al pedido.
  • option.quantity es la cantidad por unidad del ítem. line_total = (unit_price + Σ(option.unit_price × option.quantity)) × quantity; total = subtotal + delivery_fee − discount. Todos los montos los calcula MiPlato.
  • fulfillment_type: delivery o pickup (retiro en el local, con delivery_address: null).
  • customer.billing (opcional) trae los datos para la factura: document_type ruc o ci.
  • payment.change_for: monto con el que el cliente paga en efectivo, para preparar el vuelto. payment.status: unpaid o paid.
  • product.external_id es null en productos creados en el panel: su sistema debe resolverlos (por ejemplo, con un producto genérico) o informar la falla.
  • status_history[].by: customer, store_user, integration o system.
#GET/v1/ordersorders:read

Lista paginada (del más antiguo al más nuevo). Filtros:

statusUno o varios estados separados por coma: ?status=pending,accepted.
ack_statuspending, acknowledged o failed. ?ack_status=pending devuelve los pedidos aún no importados.
created_since, updated_sinceFecha ISO 8601 con desplazamiento.
cursor, limitPaginación (máx. 200).
#GET/v1/orders/{order_id}orders:read

Devuelve el pedido completo.

#GET/v1/orders/by-external-id/{external_id}orders:read

Busca el pedido por el código de venta que su sistema informó en el ack.

#POST/v1/orders/{order_id}/ackorders:writeIdempotency-Key

Confirma que su sistema importó el pedido e informa su propio código. Responde 200 con el pedido (integration.ack_status = "acknowledged").

Solicitud
{ "external_id": "VENTA-88231", "external_number": "88231" }
  • external_id obligatorio (1 a 64 caracteres, mismo formato que los códigos de catálogo); external_number opcional (hasta 64), para mostrar en el panel.
  • Repetir con el mismo external_id: éxito, sin efecto. Con otro: 409 external_id_conflict.
  • El ack no acepta el pedido: aceptar es un cambio de estado aparte.
#POST/v1/orders/{order_id}/import-failureorders:writeIdempotency-Key

Informa que su sistema no pudo importar el pedido. El comercio ve el aviso en el panel y el pedido sigue operable allí. Una vez resuelto, su sistema puede importarlo y enviar el ack normalmente.

Solicitud
{
  "code": "unmapped_product",
  "message": "El producto «Promo 2x1» no tiene código en el sistema.",
  "retryable": false
}

code: unmapped_product, invalid_customer, register_closed, validation_error u other. message: obligatorio, hasta 500 caracteres. Un pedido ya confirmado responde 409 already_acknowledged.

#POST/v1/orders/{order_id}/statusorders:writeIdempotency-Key

Cambia el estado del pedido (si order_status_authority es erp o both). Responde 200 con el pedido actualizado.

Solicitud
{
  "status": "accepted",
  "estimated_minutes": 30,
  "occurred_at": "2026-10-12T21:42:00-03:00"
}
  • reason: obligatorio (3 a 255 caracteres) para rejected y cancelled. El cliente lo ve en el seguimiento.
  • estimated_minutes (1 a 600): sólo con accepted; el cliente ve el tiempo estimado.
  • occurred_at: momento real del cambio en su sistema (opcional).
  • Enviar el estado actual: 200 sin efecto. Transición no permitida: 409 invalid_status_transition.

Estados del pedido

pendingacceptedpreparingreadyout_for_deliverycompleted
EstadoEn el panelLo que ve el cliente
pendingNuevoRecibido — esperando confirmación
acceptedAceptadoConfirmado
preparingEn preparaciónEn preparación
readyListo para entregar / retirarListo
out_for_deliveryEn camino (sólo delivery)En camino
completedEntregado / RetiradoEntregado (final)
rejectedRechazadoRechazado (final)
cancelledCanceladoCancelado (final)
  1. Desde pending sólo se pasa a accepted, rejected o cancelled.
  2. Después de aceptado se puede avanzar salteando etapas (por ejemplo accepted → completed).
  3. out_for_delivery sólo vale para pedidos delivery.
  4. No hay retroceso. completed, rejected y cancelled son finales.
  5. rejected sólo desde pending; cancelled desde cualquier estado no final.

Cada cambio genera el evento order.status_changed, también cuando lo hizo su propio sistema: ignore los eventos cuyo data.changed_by.integration_id sea el de su integración.

Webhooks

Un webhook por integración. Puede configurarlo por la API o el comercio puede hacerlo en el panel.

#PUT/v1/webhookwebhooks:manage

Crea o actualiza el webhook. url HTTPS pública (hasta 500 caracteres) y events: lista de tipos o ["*"] (todos los que sus permisos permiten). active opcional. Al crearlo responde 201 e incluye signing_secret, que se muestra una sola vez.

Solicitud
{
  "url": "https://erp.ejemplo.com.py/miplato/webhook",
  "events": ["order.created", "order.status_changed", "order.updated"],
  "active": true
}
Respuesta 201
{
  "data": {
    "url": "https://erp.ejemplo.com.py/miplato/webhook",
    "events": ["order.created", "order.status_changed", "order.updated"],
    "active": true,
    "paused": false,
    "paused_reason": null,
    "signing_secret": "whsec_…"
  }
}
EndpointDescripción
GET /v1/webhookConfiguración actual.
POST /v1/webhook/rotate-secretNuevo secreto de firma. Durante 24 h MiPlato firma con el nuevo y con el anterior.
POST /v1/webhook/testEnvía un evento webhook.test (responde 202).
GET /v1/webhook/deliveriesEntregas recientes con sus intentos. Filtro ?status=pending|delivering|succeeded|failed.
POST /v1/webhook/deliveries/{delivery_id}/retryReenvía una entrega ahora.

Eventos

EventoCuándoPermiso
order.createdUn cliente hizo un pedido.orders:read
order.status_changedCambió el estado (desde cualquier origen). Trae previous_status y changed_by.orders:read
order.updatedCambiaron otros datos, por ejemplo el pago se marcó como paid.orders:read
store.updatedCambió la recepción de pedidos, las formas de pago o las modalidades.store:read
integration.updatedEl comercio cambió los permisos o la configuración.—
webhook.testPrueba manual.—
Envoltorio del evento
{
  "id": "evt_01JAA0…",
  "type": "order.status_changed",
  "api_version": "1.0",
  "created_at": "2026-10-12T21:42:00-03:00",
  "store_id": "str_01JA…",
  "integration_id": "int_01JA…",
  "data": {
    "order": { "id": "ord_01JA9Z…", "status": "accepted", "…": "objeto Pedido completo" },
    "previous_status": "pending",
    "changed_by": { "type": "store_user", "integration_id": null }
  }
}

Los eventos de pedido traen siempre el pedido completo: no hace falta una segunda llamada. Pueden llegar desordenados: compare data.order.updated_at con lo que ya tiene y descarte la versión más antigua.

Solicitud de entrega

Encabezados
POST https://erp.ejemplo.com.py/miplato/webhook
Content-Type: application/json
User-Agent: MiPlato-Webhooks/1.0
X-MiPlato-Event-Id: evt_01JAA0…
X-MiPlato-Event-Type: order.status_changed
X-MiPlato-Delivery-Id: dlv_01JAA1…
X-MiPlato-Timestamp: 1760316120
X-MiPlato-Signature: v1=5d41402abc4b2a76b9719d911017c592…

Responda 2xx en hasta 10 segundos; si el procesamiento es largo, guarde el evento en una cola y responda enseguida. Cualquier otra respuesta, un tiempo agotado o un error de conexión cuenta como falla y se reintenta después de 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h y 24 h (9 intentos en total). Las entregas fallidas pueden reenviarse y los eventos siguen disponibles en el feed por 30 días. Si todas las entregas fallan durante 72 horas seguidas, el webhook se pausa y el comercio recibe un aviso en el panel.

Verificar la firma

Algoritmo
firma = hex( HMAC_SHA256( signing_secret, X-MiPlato-Timestamp + "." + cuerpo_crudo ) )
  1. Calcule la firma sobre el cuerpo crudo, antes de decodificar el JSON.
  2. Compare en tiempo constante. Durante una rotación el encabezado trae dos firmas (v1=a,v1=b): alcanza con que coincida una.
  3. Rechace si la diferencia entre la hora actual y X-MiPlato-Timestamp supera 300 segundos.
  4. Descarte duplicados por X-MiPlato-Event-Id.
PHP
<?php
$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_MIPLATO_TIMESTAMP'] ?? '';
$header    = $_SERVER['HTTP_X_MIPLATO_SIGNATURE'] ?? '';
$secret    = getenv('MIPLATO_WEBHOOK_SECRET');

if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(400);
    exit;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $body, $secret);
$valid = false;
foreach (explode(',', $header) as $part) {
    if (str_starts_with($part, 'v1=') && hash_equals($expected, substr($part, 3))) {
        $valid = true;
        break;
    }
}
if (!$valid) {
    http_response_code(401);
    exit;
}
$event = json_decode($body, true);
// 1) Si $event['id'] ya fue procesado, responda 200 y termine.
// 2) Guarde el evento en una cola local y responda 200.
http_response_code(200);

Feed de eventos

#GET/v1/eventsorders:read

Los mismos eventos de los webhooks, en orden estricto de creación. Es la opción para sistemas sin dirección pública y la conciliación recomendada para todos.

afterEl último next_cursor procesado. Sin after, empieza por el evento más antiguo disponible.
limit1 a 200 (predet. 100).
typesFiltro opcional: ?types=order.created,order.status_changed.
Respuesta 200
{
  "data": [ { "id": "evt_01JAA0…", "type": "order.created", "…": "…" } ],
  "next_cursor": "c_MTA0Mg",
  "has_more": false,
  "cursor_expired": false
}
  • Guarde el next_cursor después de procesar los eventos y continúe desde allí. Si has_more es true, consulte de nuevo enseguida.
  • Los eventos se conservan 30 días. Si su cursor apunta a un evento ya eliminado, la respuesta empieza en el más antiguo disponible con "cursor_expired": true: concilie con GET /v1/orders?updated_since=…. Los pedidos nunca se eliminan.
  • Frecuencia sugerida: cada 15 a 30 segundos sin webhook; cada 5 minutos como conciliación con webhook.
PHP
<?php
// Consulta periódica del feed (por ejemplo, desde una tarea programada).
$cursor = load_cursor();   // último next_cursor guardado, o null
do {
    $url  = 'https://app.miplato.com.py/api/v1/events?limit=100' . ($cursor ? '&after=' . urlencode($cursor) : '');
    $resp = miplato_get($url);           // GET con Authorization: Bearer …
    foreach ($resp['data'] as $event) {
        process_once($event);            // idempotente por $event['id']
    }
    $cursor = $resp['next_cursor'] ?? $cursor;
    save_cursor($cursor);
} while ($resp['has_more']);

Lista de verificación

Lo que su sistema debe cumplir para operar en producción. Cada ítem indica cómo se comprueba:

  • Verificado el reporte del ambiente de homologación lo comprueba con sus llamadas reales; los obligatorios son condición para el certificado.
  • Validado por la API la API rechaza la solicitud si no se cumple: no hay forma de saltearlo.
  • Declaración no se puede comprobar desde afuera: su empresa lo declara al emitir el certificado.
  • VerificadoSe conecta con GET /v1/me usando la credencial del comercio.
  • DeclaraciónAvisa al usuario cuando credential.expires_at no es null (credencial por vencer).
  • DeclaraciónGuarda la credencial cifrada, una por comercio, y nunca la registra en archivos de log ni la muestra completa.
  • Validado por la APIEnvía montos como texto ("35000") y fechas con desplazamiento.
  • Validado por la APIEnvía las categorías antes que los productos (un producto con categoría desconocida se rechaza).
  • VerificadoRecomendadoIncluye source_updated_at en categorías y productos.
  • Validado por la APIUsa Idempotency-Key en ack, status, import-failure y lotes.
  • VerificadoRecomendadoReutiliza la misma Idempotency-Key al reintentar.
  • VerificadoRecomendadoReintenta sólo ante 429/5xx/fallas de red, sin repetir errores 4xx.
  • VerificadoRecibe los pedidos por webhook (respuesta 2xx) o por el feed de eventos.
  • VerificadoVerifica la firma de los webhooks: rechaza firmas inválidas y firmas de más de 300 s.
  • VerificadoResponde el webhook en menos de 10 segundos.
  • VerificadoUsa el id del pedido como clave única: el mismo pedido recibido dos veces no genera dos ventas.
  • VerificadoConcilia con GET /v1/orders?ack_status=pending o con el feed.
  • VerificadoEnvía el ack al registrar la venta, o import-failure si no pudo.
  • VerificadoResuelve los pedidos con productos external_id: null (creados en el panel).
  • VerificadoLleva un pedido de «aceptado» a «entregado» con POST /status (si la configuración le permite cambiar estados).
  • VerificadoRecomendadoCancela o rechaza informando reason.
  • VerificadoRecomendadoIgnora los eventos order.status_changed originados por su propia integración.
  • DeclaraciónIgnora los campos desconocidos de las respuestas y de los eventos.

Soporte

Para desarrollar y homologar su integración, solicite acceso al ambiente de homologación. Al reportar un problema, indique el X-Request-Id de la respuesta, la fecha y hora, y el endpoint utilizado. Nunca envíe la credencial ni el secreto del webhook por chat o correo.

Versiones

La versión va en la ruta (/v1). Dentro de la v1 sólo se agregan cambios compatibles: nuevos campos, endpoints o eventos. Un cambio incompatible tendría una ruta nueva (/v2) y un período de convivencia anunciado con anticipación.

VersiónCambios
1.0Catálogo (individual y por lote, con sincronización completa), pedidos (consulta, confirmación de importación, fallas y estados), webhooks firmados y feed de eventos.

Entregas independientes

Además del pedido, el ERP puede coordinar repartidores. La cuenta del balcón es informativa: no registra el pago del pedido ni modifica stock, caja o facturación.

POST /entregas crea la solicitud (permiso orders:write, Idempotency-Key obligatoria). Envíe request_key y el order_id de un pedido de delivery ya aceptado y cotizado, o los datos de una entrega manual. Para transferencias se exige recepción confirmada por el comercio. GET /entregas/{id} consulta únicamente entregas del comercio de la credencial (orders:read).

POST /entregas/{id}/{action} acepta pickup (settlement_confirmed=true), raise_fee (amount), cancel (reason) o returned (goods_received, refund_confirmed si corresponde y return_fee opcional). Requiere orders:write e Idempotency-Key.

Los importes son guaraníes enteros. customer_total, customer_fee y courier_fee son independientes. balance positivo es dinero que el repartidor declara entregar al comercio; negativo es dinero que el comercio declara entregar al repartidor. Nunca use ese neto como pago completo de un pedido con otro total.

El código y tracking_url son privados del cliente: la credencial del comercio puede consultarlos para compartirlos con él, nunca con el repartidor. La especificación OpenAPI contiene campos, estados y permisos.