Appearance
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
secretcon 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
| Evento | Cuándo |
|---|---|
order.created | Se creó el pedido (por API, por el panel de la tienda o por el del delivery) |
order.assigned | El delivery le asignó motorizado. Trae rider.name y rider.phone |
order.delivered | Se entregó. Trae delivery_proof y actual_amount_collected |
order.cancelled | Se 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.