Skip to content

Webhooks

En vez de preguntar cada rato, registra una URL y Motochaski te avisa cuando un pedido cambia.

Registrar

POST /v1/webhooks con scope manage:webhooks.

bash
curl -X POST https://api.motochaski.com/v1/webhooks \
  -H "X-API-Key: mck_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://mi-tienda.com/motochaski/webhook",
    "events": ["order.assigned", "order.delivered", "order.cancelled"]
  }'
  • La URL tiene que ser HTTPS.
  • La respuesta trae el secret con el que se firma cada envío. Se muestra una sola vez: guárdalo.
  • Con llave atada a una tienda, el webhook solo recibe eventos de esa tienda. Con llave sin tienda, de todo el delivery (opcionalmente acotado con store_id).

GET /v1/webhooks lista los activos. DELETE /v1/webhooks/:id lo desactiva.

Eventos

EventoCuándo
order.createdSe creó el pedido (por API, por el panel de la tienda o por el del delivery)
order.assignedEl delivery le asignó motorizado. Trae rider.name y rider.phone
order.deliveredSe entregó. Trae delivery_proof y actual_amount_collected
order.cancelledSe anuló, a mano o por auto-cancelación a las 24 h

Qué llega

http
POST /motochaski/webhook HTTP/1.1
Content-Type: application/json
X-Motochaski-Event: order.delivered
X-Motochaski-Timestamp: 1758211200
X-Motochaski-Signature: sha256=3f5a…
json
{
  "event": "order.delivered",
  "tenant_id": "0a1b…",
  "timestamp": "1758211200",
  "data": {
    "id": "b1c2d3…",
    "guide_code": "MC-K7M3P9X",
    "status": "delivered",
    "status_label": "Entregado",
    "store_id": "…",
    "branch_id": "…",
    "recipient_name": "María Quispe",
    "recipient_phone": "+51987654321",
    "delivery_address": "Av. Larco 1234, dpto 502",
    "delivery_reference": "Frente al parque",
    "product": "2 polos talla M",
    "size": "M",
    "amount_to_collect": 89.90,
    "actual_amount_collected": 89.90,
    "shipping_cost": 12.00,
    "payment_type": "cash_on_delivery",
    "operative_day": "2026-09-18",
    "created_at": "2026-09-18T14:02:11Z",
    "received_at": "2026-09-18T15:10:40Z",
    "delivered_at": "2026-09-18T18:45:03Z",
    "rider": { "name": "Juan Quispe", "phone": "+51912345678" },
    "location": { "id": "…", "name": "Miraflores" },
    "delivery_proof": {
      "photo_url": "https://files.motochaski.com/…jpg",
      "captured_at": "2026-09-18T18:44:50Z"
    }
  }
}

amount_to_collect es lo pactado; actual_amount_collected lo que el motorizado cobró de verdad. Son datos distintos y los dos van.

delivery_proof solo aparece si hay foto. Para los webhooks del delivery trae además lat/lng de dónde se tomó; para los de una tienda no, salvo que el operador lo habilite en su configuración. La foto es la última que tomó el motorizado, la misma que ve el panel.

Verificar la firma

La firma es el SHA-256 en hexadecimal de secret + "." + timestamp + "." + body, donde body son los bytes exactos del cuerpo. Calcúlala sobre el cuerpo crudo, antes de parsear el JSON.

js
import { createHash, timingSafeEqual } from 'node:crypto'

export function verify(req, rawBody, secret) {
  const ts = req.headers['x-motochaski-timestamp']
  const given = (req.headers['x-motochaski-signature'] ?? '').replace('sha256=', '')
  const expected = createHash('sha256')
    .update(`${secret}.${ts}.`).update(rawBody).digest('hex')
  return given.length === expected.length &&
    timingSafeEqual(Buffer.from(given), Buffer.from(expected))
}
php
function verify(string $rawBody, string $secret): bool {
  $ts = $_SERVER['HTTP_X_MOTOCHASKI_TIMESTAMP'] ?? '';
  $given = str_replace('sha256=', '', $_SERVER['HTTP_X_MOTOCHASKI_SIGNATURE'] ?? '');
  $expected = hash('sha256', $secret . '.' . $ts . '.' . $rawBody);
  return hash_equals($expected, $given);
}
python
import hashlib, hmac

def verify(headers, raw_body: bytes, secret: str) -> bool:
    ts = headers.get("X-Motochaski-Timestamp", "")
    given = headers.get("X-Motochaski-Signature", "").removeprefix("sha256=")
    expected = hashlib.sha256(f"{secret}.{ts}.".encode() + raw_body).hexdigest()
    return hmac.compare_digest(given, expected)

Rechaza también timestamps con más de cinco minutos de diferencia con tu reloj: evita que alguien reenvíe un webhook viejo.

Responder

Responde 2xx rápido (menos de 5 segundos) y procesa después. Cualquier otra cosa cuenta como fallo.

Sin reintentos, por ahora

Si tu servidor no responde 2xx, el evento se pierde: hoy es un intento y a log. Por eso conviene que tu receptor solo encole y responda. Está previsto reintentar con espera creciente; ver Pendiente. Mientras tanto, si sospechas que perdiste un evento, GET /v1/orders/:id siempre tiene el estado real.

Idempotencia del receptor

Un mismo evento puede llegarte más de una vez (hoy no, pero con reintentos sí). Usa data.id + event como clave y descarta lo que ya procesaste.

API pública de Motochaski · v1