Ir al contenido
Maple

Pedidos

Construya el ciclo de pedidos: conecte un local, suscríbase a webhooks, verifique las entregas y gestione cada pedido con las llamadas accept, deny, ready y complete.

Este es el núcleo de una integración con Maple. Cuando un cliente hace un pedido en un local al que usted está conectado, Maple envía el pedido a su webhook; usted decide si lo prepara e informa su avance a medida que se mueve. Nada de esto involucra pagos: de eso se encarga Maple.

Una integración de pedidos funcional consta de dos partes:

# Qué construye Esfuerzo habitual
1 Recibir pedidos: un endpoint de webhook con verificación de firma ~1–2 días
2 Decidir pedidos: llamadas accept / deny / status hacia Maple ~1–2 días

Si aún no lo ha hecho, revise Cómo funciona Maple para conocer el modelo de objetos, y siga el Inicio rápido para obtener una clave y conectar un local de prueba.

Paso 1 — Conecte un local

Los pedidos llegan a usted solo cuando su app es el receptor conectado de un local:

Terminal window
curl -X POST $MAPLE_BASE/locations/{locationId}/connection \
-H "Authorization: Bearer $MAPLE_KEY"

Las conexiones son exclusivas por entorno: si otra app ya tiene el local, recibirá un 409. GET y DELETE en la misma ruta consultan y eliminan su conexión.

Paso 2 — Suscríbase a los webhooks

Registre un endpoint HTTPS y los tipos de evento que desea. El catálogo completo está en GET /v1/webhook_event_types; para el ciclo de pedidos normalmente querrá estos:

Terminal window
curl -X POST $MAPLE_BASE/webhook_subscriptions \
-H "Authorization: Bearer $MAPLE_KEY" \
-H "Content-Type: application/json" \
-d '{
"notification_url": "https://your-app.example.com/maple/webhooks",
"event_types": ["order.notification", "order.validation_requested", "order.cancelled"]
}'

La respuesta incluye un secreto de firma de un solo uso (mwhsec_…). Guárdelo; no se vuelve a mostrar. La URL debe ser HTTPS pública: se rechazan las direcciones privadas e internas.

Paso 3 — Verifique cada entrega

Cada entrega es un sobre al estilo de Stripe:

{
"object": "event",
"id": "evt_...",
"type": "order.notification",
"created": 1765432100,
"data": { "order_id": "ord_...", "location_id": "str_...", "...": "the order content — see Step 4" }
}

Las entregas incluyen dos encabezados: maple-webhook-id (el id del evento) y maple-webhook-signature con el formato t=<unix_seconds>,v1=<hex_hmac>. La cadena firmada es {timestamp}.{subscription_id}.{notification_url}.{raw_body}. Verifíquela contra el cuerpo sin procesar, sin analizar:

import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyMapleWebhook(params: {
signatureHeader: string; // maple-webhook-signature
subscriptionId: string; // your subscription id (dws_…)
notificationUrl: string; // the exact URL you registered
rawBody: string; // the unparsed request body
signingSecret: string; // mwhsec_…
toleranceSeconds?: number; // default 300
}): boolean {
const parts = Object.fromEntries(params.signatureHeader.split(',').map((p) => p.split('=')));
const timestamp = Number(parts.t);
const provided = parts.v1 ?? '';
// Reject stale deliveries to prevent replay.
const skew = Math.abs(Math.floor(Date.now() / 1000) - timestamp);
if (!Number.isFinite(timestamp) || skew > (params.toleranceSeconds ?? 300)) return false;
const signed = `${timestamp}.${params.subscriptionId}.${params.notificationUrl}.${params.rawBody}`;
const expected = createHmac('sha256', params.signingSecret).update(signed).digest('hex');
return expected.length === provided.length && timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
}

La verificación anterior rechaza marcas de tiempo vencidas (de más de ~5 minutos) para evitar repeticiones. Además, elimine duplicados con el id del sobre: la entrega es al menos una vez. El contrato de confiabilidad completo, con el calendario de reintentos y el libro de eventos, está en Webhooks.

Ponerse al día tras una caída

Cuando su receptor no estuvo disponible (el software local estaba apagado o la red cayó), reconcilie así:

  1. Llame a GET /v1/orders?since=<timestamp>&limit=100 con su último punto de control exitoso (exclusivo, ISO 8601 UTC). Con since, los resultados vienen del más antiguo al más reciente y has_more indica que hay otra página.
  2. Mientras has_more sea true, repita la solicitud con starting_after igual al último id de pedido de la página anterior. Continúe hasta vaciar el rezago y luego avance su punto de control.
  3. Llame a POST /v1/orders/{orderId}/resend cuando necesite que Maple publique un order.notification nuevo para un pedido activo que no recibió su webhook. Requiere el alcance (scope) webhooks:write, crea una entrega nueva, firmada y con reintentos, y no cambia el estado del pedido. Los pedidos en estado terminal (REJECTED, CUSTOMER_CANCELLED o STORE_CANCELLED) no se pueden reenviar ni reactivar y devuelven 400; reconcílielos con GET /v1/orders?since=... y GET /v1/orders/{orderId}.
  4. Use el registro de eventos en lugar de resend cuando importe conservar la identidad original del evento.

Esperar también funciona en una caída corta: el calendario de reintentos vuelve a entregar un evento fallido durante unas 10 horas antes de considerarlo fallido por completo.

Paso 4 — Lea el pedido

El data del payload de order.notification es el contenido del pedido: sus IDs, cliente, artículos y totales, de modo que puede empezar a prepararlo directamente desde el webhook sin otra llamada. Usa los mismos nombres de campo y montos en centavos enteros que el recurso del pedido, pero es un objeto más pequeño: order_id (no id), location_id, fulfillment_type, scheduled_for, delivery_address, customer, line_items y totals. No incluye object, id, livemode, created, status, payment_status, external_id ni payment, y los artículos no llevan tax por artículo. order.validation_requested lleva el mismo data más validation_id:

{
"order_id": "ord_...",
"location_id": "str_...",
"fulfillment_type": "delivery",
"scheduled_for": null,
"delivery_address": {
"street": "123 Main Street",
"unit": "Apt 4B",
"city": "New York",
"state": "NY",
"zip": "10001",
"notes": "Leave at the side door"
},
"customer": { "name": "Alex", "phone": "+14155551234", "phone_last_four": "1234" },
"line_items": [
{
"name": "Latte",
"menu_entity_id": "itm-latte",
"quantity": 1,
"base_price": 450,
"modifiers": [{ "name": "Oat milk", "menu_entity_id": "mod-oat", "quantity": 1, "price": 75 }]
}
],
"totals": { "currency": "USD", "subtotal": 525, "tax": 42, "surcharge": 0, "tip": 0, "delivery_fee": 599, "delivery_tip": 0, "total": 1166 }
}

GET /v1/orders/{orderId} devuelve en cambio el recurso completo del pedido, que añade object, id, livemode, created, status, payment_status, external_id, payment y el tax por artículo. Consúltelo en cualquier momento para obtener el estado actual y autorizado:

{
"object": "order",
"id": "ord_...",
"livemode": false,
"created": 1765432100,
"status": "PENDING",
"payment_status": "unpaid",
"fulfillment_type": "delivery",
"scheduled_for": null,
"location_id": "str_...",
"delivery_address": {
"street": "123 Main Street",
"unit": "Apt 4B",
"city": "New York",
"state": "NY",
"zip": "10001",
"notes": "Leave at the side door"
},
"customer": { "name": "Alex", "phone": "+14155551234", "phone_last_four": "1234" },
"line_items": [
{
"name": "Latte",
"menu_entity_id": "itm-latte",
"quantity": 1,
"base_price": 450,
"tax": 36,
"modifiers": [{ "name": "Oat milk", "menu_entity_id": "mod-oat", "quantity": 1, "price": 75 }]
}
],
"totals": { "currency": "USD", "subtotal": 525, "tax": 42, "surcharge": 0, "tip": 0, "delivery_fee": 599, "delivery_tip": 0, "total": 1166 },
"external_id": null,
"payment": { "provider": "stripe", "status": "unpaid", "payment_link_url": "https://..." }
}

Algunos puntos que conviene tener presentes:

  • El dinero está en centavos de USD enteros. 450 son $4.50. Nunca lo analice como número decimal. Todos los montos en totals y en los artículos siguen esta convención.
  • Los totales vienen precalculados. totals.total es el valor autorizado. No debe recalcular ningún precio.
  • Los artículos hacen referencia a su menú mediante menu_entity_id, cuyo valor es el externalId que usted publicó para ese artículo. Publique primero un menú y estos coincidirán con su propio catálogo. (Los payloads de menú usan camelCase; los de pedidos y webhooks usan snake_case; consulte Convenciones.)
  • Los modificadores son las opciones de primer nivel seleccionadas directamente. Las selecciones de modificadores anidados más profundas no se expanden en el recurso del pedido en v1.
  • Los datos del cliente son intencionalmente mínimos. Recibe el nombre y el número de teléfono del cliente (phone en formato E.164, más phone_last_four para mostrar). Los pedidos a domicilio también incluyen delivery_address con la calle, la unidad, la ciudad, el estado, el código postal y las instrucciones de entrega; es null en pedidos para recoger o cuando Maple no tiene una dirección guardada. Las seis claves de la dirección están presentes siempre que el bloque tenga datos.
  • Revise scheduled_for antes de enviar la comanda. Es null en pedidos para ahora. Cuando tiene valor (ISO 8601, UTC), el cliente eligió esa hora de recogida o entrega; usted recibe el pedido de inmediato, así que programe la preparación para scheduled_for, no para el momento de recibirlo.

Paso 5 — Decida e informe el avance

Responda mediante los endpoints de decisión a medida que avanza el pedido. Ninguno recibe cuerpo, salvo donde se indica:

Llamada Cuándo
POST /v1/orders/{orderId}/accept Puede prepararlo
POST /v1/orders/{orderId}/deny No puede; { "reason": "…" } es opcional
POST /v1/orders/{orderId}/ready Listo para recoger o para entregar al repartidor
POST /v1/orders/{orderId}/complete Entregado
POST /v1/orders/{orderId}/cancel Debe cancelar después de aceptar; { "reason": "…" } es opcional
POST /v1/orders/{orderId}/status Transición detallada (vea abajo)
Terminal window
curl -X POST $MAPLE_BASE/orders/{orderId}/accept \
-H "Authorization: Bearer $MAPLE_KEY"
{ "object": "order_decision", "order_id": "ord_...", "decision": "accept", "status": "received" }

Si prefiere un solo endpoint, POST /v1/orders/{orderId}/status recibe una transición explícita:

{ "status": "ACCEPTED" }

Valores válidos: ACCEPTED, READY, IN_DELIVERY, FULFILLED, REJECTED, STORE_CANCELLED. Para saber qué significa cada estado y las transiciones permitidas entre ellos, consulte el Ciclo de vida del pedido.

Opcional — Prevalide los pedidos

La prevalidación es opcional y se activa suscribiéndose a order.validation_requested:

  • Si no se suscribe, no hay paso de validación. Maple envía order.notification directamente y su accept/deny es la única barrera.
  • Si se suscribe, Maple le pide confirmar que cada pedido se puede preparar (disponibilidad de artículos, viabilidad de precios, posibilidad de inyección en el POS) antes de enviar la notificación, y espera su respuesta. Por lo tanto, activarla es un compromiso: un pedido que no valide a tiempo se rechaza (vea abajo).

Ese es todo el mecanismo: la suscripción es el interruptor.

Una vez suscrito, responda a cada order.validation_requested con un resultado. Incluya el validation_id del data de ese evento para que el resultado quede asociado a ese intento; un resultado de un intento vencido o anterior se rechaza con 404:

Terminal window
curl -X POST $MAPLE_BASE/orders/{orderId}/validation_result \
-H "Authorization: Bearer $MAPLE_KEY" \
-H "Content-Type: application/json" \
-d '{ "validation_id": "<validation_id del evento>", "status": "valid", "partner_reference": "pos_quote_123" }'

Para bloquear el pedido:

{
"validation_id": "<validation_id del evento>",
"status": "invalid",
"reason": { "code": "ITEM_AVAILABILITY", "explanation": "86'd", "item_external_ids": ["itm-latte"] }
}

Un resultado valid permite que el pedido pase a la notificación; invalid lo bloquea antes de que se cobre al cliente. Tiene unos 5 minutos para responder antes de que la solicitud venza (el recurso incluye expires_at, a ~300 segundos). Si vence, Maple solicita la validación una vez más; si esa segunda solicitud tampoco se responde, el pedido se rechaza sin notificación, así que suscríbase solo cuando su manejador responda de forma confiable y a tiempo. Es la misma verificación de disponibilidad que haría al aceptar, solo que antes, para que los clientes no paguen por algo que usted no puede preparar.

Cómo se ve “terminado”

Usted construye Usted obtiene
Un manejador de webhook que verifica, elimina duplicados y enruta Pedidos en tiempo real con totales precalculados
Llamadas accept / deny / ready / complete / status Los clientes ven el avance del pedido en vivo
(Opcional) respuestas de validación Pedidos inválidos bloqueados antes de cobrar al cliente

Un webhook y un puñado de llamadas de decisión. Los pagos y los precios quedan de nuestro lado, y el equipo de Maple está disponible durante toda su integración.

Siguiente