Webhooks
Los webhooks son el mecanismo push de Pickwise: en lugar de que tu sistema haga polling, Pickwise te notifica vía HTTP POST cuando ocurren eventos (cambio de estado de una orden, issue detectado, stock actualizado, etc.).
Esta es la guía más crítica del portal. Prestale especial atención a la validación de firma y al sistema de retry.
¿Qué es un webhook?
Sección titulada «¿Qué es un webhook?»Un webhook es una URL tuya que Pickwise llama cuando pasa algo. Tu endpoint recibe un POST con un JSON describiendo el evento y headers de seguridad/trazabilidad.
Comparado con polling:
- Más rápido: latencia de segundos en lugar de minutos.
- Más barato en rate limit: no consumís cupo haciendo GETs.
- Más simple lógicamente: reaccionás al evento en lugar de mantener estado de “qué ya vi”.
El costo es operacional: tu endpoint tiene que estar up, validar firma y manejar reintentos.
Flujo end-to-end
Sección titulada «Flujo end-to-end»- Registrás un endpoint en Pickwise con
POST /webhooks. - Cuando ocurre un evento, Pickwise envía un POST firmado a tu URL.
- Tu endpoint valida la firma HMAC y responde
2xxrápido. - Procesás el evento (de forma asincrónica, idealmente).
Eventos disponibles
Sección titulada «Eventos disponibles»| Evento | Se dispara cuando… | Payload |
|---|---|---|
order.status_changed | Una orden cambia de estado (ej: PENDING → IN_PICKING) | Ver abajo |
order.issue_detected | Se detecta un problema en una orden (producto no encontrado, stock insuficiente) | Ver abajo |
order.issue_resolved | Un problema previamente detectado fue resuelto | Ver abajo |
order.items_adjusted | El equipo de almacén ajustó los items de la orden | Similar a issue_detected |
product.stock_updated | El stock de un producto cambió (post-picking o ajuste manual) | Ver abajo |
Registrar un webhook
Sección titulada «Registrar un webhook»POST /webhooks — permisos: webhooks:manage
curl -X POST https://api-{CLIENTE}.pickwise.com.ar/api/v1/public/webhooks \ -H "Authorization: Bearer pk_live_xxxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://mi-erp.com/pickwise/webhook", "events": [ "order.status_changed", "order.issue_detected", "order.issue_resolved", "product.stock_updated" ], "secret": "mi-secret-para-firmar-al-menos-32-chars" }'const res = await fetch( 'https://api-{CLIENTE}.pickwise.com.ar/api/v1/public/webhooks', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PICKWISE_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://mi-erp.com/pickwise/webhook', events: [ 'order.status_changed', 'order.issue_detected', 'order.issue_resolved', 'product.stock_updated' ], secret: process.env.MY_WEBHOOK_SECRET }) });const { data } = await res.json();console.log('Webhook id:', data.id);// data.secret se devuelve solo en este momentoHeaders que recibe tu endpoint
Sección titulada «Headers que recibe tu endpoint»Content-Type: application/jsonX-Pickwise-Signature: sha256=a1b2c3d4e5f6...X-Pickwise-Event: order.status_changedX-Pickwise-Timestamp: 2026-03-07T18:00:00ZX-Pickwise-Delivery-Id: 550e8400-e29b-41d4-a716-446655440000| Header | Uso |
|---|---|
X-Pickwise-Signature | Firma HMAC-SHA256. Validarla siempre. |
X-Pickwise-Event | Nombre del evento. Útil para routing sin parsear el body. |
X-Pickwise-Timestamp | Cuándo se disparó en Pickwise. Útil para detectar eventos muy viejos. |
X-Pickwise-Delivery-Id | UUID único del intento. Usalo para dedupe. |
Validar la firma (crítico)
Sección titulada «Validar la firma (crítico)»La firma se calcula como HMAC-SHA256 del body raw con tu secret. Para validarla correctamente:
- Usá el body raw (bytes tal como llegaron), no el body re-serializado después de un
JSON.parse. - Usá una función de comparación tiempo-constante (
timingSafeEqual/hash_equals). Comparar con===o==es vulnerable a timing attacks.
import crypto from 'crypto';import express from 'express';
const app = express();
// Importante: usar raw body para el HMAC, no body parseadoapp.post( '/webhooks/pickwise', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.header('X-Pickwise-Signature'); const secret = process.env.PICKWISE_WEBHOOK_SECRET;
if (!verifySignature(req.body, signature, secret)) { return res.status(401).send('Invalid signature'); }
const event = JSON.parse(req.body.toString('utf8'));
// Dedupe por delivery-id (idempotencia) const deliveryId = req.header('X-Pickwise-Delivery-Id'); if (alreadyProcessed(deliveryId)) { return res.status(200).send('OK'); // No reprocesar }
// Responder rápido, procesar async res.status(200).send('OK'); processEventAsync(event, deliveryId); });
function verifySignature(rawBody, signature, secret) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) );}<?php$rawBody = file_get_contents('php://input');$signature = $_SERVER['HTTP_X_PICKWISE_SIGNATURE'] ?? '';$secret = getenv('PICKWISE_WEBHOOK_SECRET');
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($signature, $expected)) { http_response_code(401); exit('Invalid signature');}
$event = json_decode($rawBody, true);$deliveryId = $_SERVER['HTTP_X_PICKWISE_DELIVERY_ID'] ?? '';
if (alreadyProcessed($deliveryId)) { http_response_code(200); exit('OK');}
http_response_code(200);echo 'OK';processEventAsync($event, $deliveryId);# curl no es un cliente de webhook: esto es solo ilustrativo del formato.# El cálculo en bash puro sería:
body='{"event":"order.status_changed",...}'secret='mi-secret'signature="sha256=$(echo -n "$body" | openssl dgst -sha256 -hmac "$secret" -hex | awk '{print $2}')"echo "$signature"Payloads de eventos
Sección titulada «Payloads de eventos»order.status_changed
Sección titulada «order.status_changed»{ "event": "order.status_changed", "timestamp": "2026-03-07T18:00:00Z", "data": { "externalId": "ERP-ORD-98765", "orderNumber": "OC-2026-00543", "previousStatus": "PACKED", "status": "DISPATCHED", "updatedAt": "2026-03-07T18:00:00Z" }}order.issue_detected
Sección titulada «order.issue_detected»{ "event": "order.issue_detected", "timestamp": "2026-03-07T14:00:00Z", "data": { "externalId": "ERP-ORD-98770", "orderNumber": "OC-2026-00548", "status": "PENDING_VALIDATION", "hasIssues": true, "issues": [ { "type": "UNRESOLVED_PRODUCT", "productExternalId": "ERP-PROD-UNKNOWN", "productSku": "SKU-NO-ENCONTRADO" } ] }}order.issue_resolved
Sección titulada «order.issue_resolved»{ "event": "order.issue_resolved", "timestamp": "2026-03-07T15:00:00Z", "data": { "externalId": "ERP-ORD-98770", "orderNumber": "OC-2026-00548", "status": "PENDING", "hasIssues": false, "resolvedIssues": ["UNRESOLVED_PRODUCT"] }}product.stock_updated
Sección titulada «product.stock_updated»{ "event": "product.stock_updated", "timestamp": "2026-03-07T18:15:00Z", "data": { "productId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "sku": "AURICULAR-BT500", "previousStock": 150, "newStock": 148, "reason": "ORDER_PICKED" }}| Campo | Descripción |
|---|---|
productId | UUID interno del producto en Pickwise |
sku | SKU del producto |
previousStock | Stock antes del cambio |
newStock | Stock después del cambio |
reason | Motivo (ORDER_PICKED, MANUAL_ADJUSTMENT, INVENTORY_SYNC) |
Respuesta esperada
Sección titulada «Respuesta esperada»Tu endpoint debe:
- Responder con código 2xx (200, 201, 202, 204) dentro de 10 segundos.
- Aceptar POST con
Content-Type: application/json. - Ser accesible vía HTTPS en producción.
- Ser idempotente — podés recibir el mismo evento más de una vez.
Si tu endpoint tarda > 10s o devuelve no-2xx, se considera fallido y entra al sistema de retry.
Sistema de retry
Sección titulada «Sistema de retry»Si tu endpoint falla (timeout, 5xx, conexión rechazada), Pickwise reintenta automáticamente con backoff:
| Intento | Demora aproximada |
|---|---|
| 1 | 1 minuto |
| 2 | 15 minutos |
| 3 | 1 hora |
| 4 | 3 horas |
| 5 | 6 horas |
Después de 5 intentos fallidos, el evento se mueve a la Dead Letter Queue (DLQ).
Dead Letter Queue
Sección titulada «Dead Letter Queue»Los eventos en DLQ se pueden listar y re-enviar.
Listar eventos en DLQ
Sección titulada «Listar eventos en DLQ»GET /webhooks/{id}/dead-lettersReplay de eventos
Sección titulada «Replay de eventos»POST /webhooks/{id}/replayAcepta un body opcional con filtros (rango de fechas, tipo de evento, etc.) para replay selectivo. Ver la referencia del endpoint para el schema completo.
Historial de entregas (exitosas y fallidas)
Sección titulada «Historial de entregas (exitosas y fallidas)»GET /webhooks/{id}/deliveriesTesting
Sección titulada «Testing»Para probar tu endpoint sin esperar un evento real:
POST /webhooks/{id}/test{ "event": "order.status_changed" }Envía un payload sintético (marcado con "test": true) sin reintentos. Útil para validar signing y conectividad.
Otros endpoints de gestión
Sección titulada «Otros endpoints de gestión»| Método | Endpoint | Descripción |
|---|---|---|
GET | /webhooks | Listar tus webhooks |
GET | /webhooks/{id} | Detalle de un webhook |
PATCH | /webhooks/{id} | Actualizar URL, eventos, activar/desactivar |
DELETE | /webhooks/{id} | Eliminar un webhook |
Mantener la integración saludable
Sección titulada «Mantener la integración saludable»Troubleshooting
Sección titulada «Troubleshooting»| Síntoma | Causa probable | Solución |
|---|---|---|
| ”Signature verification failed” | Secret equivocado | Regenerar webhook o verificar env vars |
| ”Signature verification failed” | Parsear body antes del HMAC | Usar raw body |
| Eventos no llegan | Webhook desactivado | GET /webhooks/{id}, revisar isActive |
| Eventos no llegan | URL incorrecta | PATCH /webhooks/{id} con la URL buena |
| Eventos no llegan | Firewall / ACL bloquea IPs de Pickwise | Pedir lista de IPs a soporte y whitelistear |
429 en mis deliveries | Mi endpoint tiene rate limit propio | Levantar o exceptuar la IP de Pickwise |
| Evento duplicado | Retry automático por timeout previo | Dedupe con delivery-id |
| Evento muy viejo llega ahora | Replay desde DLQ, o retry después de 6h | Revisar X-Pickwise-Timestamp y decidir si procesarlo |