Webhooks
Receive real-time notifications when events happen in a store.
Overview
Webhooks allow your application to receive HTTP POST notifications when events occur in a Whatalo store. Instead of polling the API, your server receives data automatically.
Before you begin: managing webhooks requires an API key with the
read:webhooks and write:webhooks scopes. When the merchant creates the key
in Settings > Developer > API Keys, they select the Automatizaciones
permission group (which contains both scopes). A key missing them receives a
403 SCOPE_INSUFFICIENT response. Once the key has the scopes, your
integration registers webhooks itself through POST /v1/webhooks — no manual
webhook setup in the dashboard is required.
How Webhooks Work
1. An event occurs (e.g., new order placed)
2. Whatalo sends a POST request to your registered URL
3. Your server processes the payload and returns 2xx
4. If delivery fails, Whatalo retries with exponential backoffManaging Webhook Endpoints
List Webhooks
GET /v1/webhooksRequired scope: read:webhooks
Create Webhook
POST /v1/webhooks{
"url": "https://your-server.com/webhooks/whatalo",
"events": ["order.created", "order.updated"]
}| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint URL (must be HTTPS) |
events | string[] | Yes | Event types to subscribe to |
secret | string | No | Custom signing secret (auto-generated if omitted) |
Webhook URLs must use HTTPS. HTTP endpoints are rejected.
Required scope: write:webhooks
Update Webhook
PATCH /v1/webhooks/:idRequired scope: write:webhooks
Delete Webhook
DELETE /v1/webhooks/:idRequired scope: write:webhooks
Event Types
| Event | Trigger |
|---|---|
order.created | New order placed |
order.updated | Order status or data changed |
order.cancelled | Order cancelled |
order.completed | Order completed |
product.created | New product created |
product.updated | Product data changed |
product.deleted | Product deleted |
customer.created | New customer registered |
customer.updated | Customer data changed |
checkout.completed | Checkout completed |
checkout.abandoned | Checkout marked abandoned after inactivity |
Webhook Payload
Webhook payloads are JSON objects. The X-Webhook-Event header identifies the event type, and the body contains the event-specific payload.
For order, customer, and product business events, event_id is the public identifier of the affected entity. It matches order.id, customer.id, or product.id in the same payload. Use X-Webhook-Id for per-delivery idempotency; use X-Webhook-Event plus event_id for business-level deduplication.
order.created
{
"event_id": "ord_abc123",
"occurred_at": "2026-03-01T15:00:00.000Z",
"order": {
"id": "ord_abc123",
"order_number": 1042,
"status": "pending",
"payment_status": "pending",
"payment_method": "cash_on_delivery",
"total": 8997,
"subtotal": 8997,
"shipping": 0,
"discount": 0,
"coupon_code": null,
"free_shipping": false,
"currency": "DOP",
"created_at": "2026-03-01T15:00:00.000Z",
"custom_fields": [],
"shipping_address": {
"name": "Ana Perez",
"phone": "8090000000",
"address": "Calle 1 #2",
"city": "Santo Domingo",
"province": "Distrito Nacional",
"country": "República Dominicana",
"postal_code": "10101",
"notes": null
},
"items": [
{
"product_id": "prd_abc123",
"product_name": "Starter Kit",
"variant_name": "Color: Rojo",
"quantity": 2,
"unit_price": 4498.5,
"total_price": 8997
}
]
},
"customer": {
"id": "cus_abc123",
"name": "Ana Perez",
"email": "[email protected]",
"phone": "8090000000"
},
"store": {
"id": "sto_abc123",
"name": "Mi Tienda",
"timezone": "America/Santo_Domingo"
}
}When a checkout includes service-style order bumps that are not persisted as order line items, the webhook also includes order.order_bumps with only those non-line bumps. Linked-product bumps are already represented in order.items and are not duplicated there.
order.updated
Order update payloads can be partial. For status changes, the body includes only the fields that changed:
{
"event_id": "ord_abc123",
"occurred_at": "2026-03-01T15:05:00.000Z",
"order": {
"id": "ord_abc123",
"status": "completed"
},
"store": {
"id": "sto_abc123",
"timezone": "America/Santo_Domingo"
}
}Payment updates use the same partial shape with order.payment_status.
When the order's data changes without a status change — for example, when its line items are edited — the body omits both status and payment_status and carries only the order id, occurred_at, and the store. Treat this as a "data changed" signal and re-fetch the full order via GET /v1/orders/{id}:
{
"event_id": "ord_abc123",
"occurred_at": "2026-03-01T15:05:00.000Z",
"order": {
"id": "ord_abc123"
},
"store": {
"id": "sto_abc123",
"timezone": "America/Santo_Domingo"
}
}Product Events
product.created and product.updated use this shape:
{
"event_id": "prd_abc123",
"occurred_at": "2026-03-01T15:00:00.000Z",
"product": {
"id": "prd_abc123",
"name": "Starter Kit",
"slug": "starter-kit",
"price": 2500,
"product_status": "active",
"archived_at": null,
"main_image": "https://cdn.example.test/product.jpg"
},
"store": {
"id": "sto_abc123",
"timezone": "America/Santo_Domingo"
}
}product.deleted includes the deleted product identifier:
{
"event_id": "prd_abc123",
"occurred_at": "2026-03-01T15:00:00.000Z",
"product": {
"id": "prd_abc123"
},
"store": {
"id": "sto_abc123",
"timezone": "America/Santo_Domingo"
}
}Customer Events
customer.created and customer.updated use this shape:
{
"event_id": "cus_abc123",
"occurred_at": "2026-03-01T15:00:00.000Z",
"customer": {
"id": "cus_abc123",
"name": "Ana Perez",
"email": "[email protected]",
"phone": "8090000000"
},
"store": {
"id": "sto_abc123",
"name": "Mi Tienda",
"timezone": "America/Santo_Domingo"
}
}checkout.abandoned
Fires when a checkout draft is marked abandoned after a period of inactivity (a background job evaluates drafts periodically). Use checkout.recovery_url to bring the shopper back to their cart.
Premium feature: this event requires the store plan to include Abandoned
Checkouts. Subscribing without it returns 403 FEATURE_ACCESS_DENIED, and
deliveries pause automatically while the store is on a plan without the feature
(the plan is re-checked on every firing). Abandoned checkouts can also be read
on demand via GET /v1/checkout-drafts (scope read:checkout_drafts, same
plan requirement).
The event_id is the abandoned checkout's public identifier and matches checkout.id. Unlike order events, customer has no id — an abandoned draft is captured before a customer record exists, so it carries only the contact fields (nullable). Either email or phone is always present.
{
"event_id": "chk_abc123",
"occurred_at": "2026-03-01T15:30:00.000Z",
"checkout": {
"id": "chk_abc123",
"status": "abandoned",
"total": 8997,
"subtotal": 8997,
"shipping": 0,
"discount": 0,
"currency": "DOP",
"recovery_url": "https://mystore.example/r/RECOVERYTOKEN",
"created_at": "2026-03-01T15:00:00.000Z",
"abandoned_at": "2026-03-01T15:30:00.000Z",
"shipping_address": {
"name": "Ana Perez",
"phone": "8090000000",
"address": "Calle 1 #2",
"city": "Santo Domingo",
"province": "Distrito Nacional",
"country": "República Dominicana",
"postal_code": "10101",
"notes": null
},
"items": [
{
"product_id": "prd_abc123",
"product_name": "Starter Kit",
"variant_name": "Color: Rojo",
"quantity": 2,
"unit_price": 4498.5,
"total_price": 8997
}
]
},
"customer": {
"name": "Ana Perez",
"email": "[email protected]",
"phone": "8090000000"
},
"store": {
"id": "sto_abc123",
"name": "Mi Tienda",
"timezone": "America/Santo_Domingo"
}
}When the abandoned draft captured order bumps, the webhook also includes checkout.order_bumps with { total, items: [{ bump_id, title, price }] }. The key is omitted when there were none.
Monetary fields such as total, subtotal, shipping, discount, and price are decimal major units, not minor units or cents. Example: 1500.50 means 1500.50 in the payload currency.
Webhook Security
Every webhook delivery includes these headers:
| Header | Description |
|---|---|
X-Webhook-Id | Unique delivery ID. Store this value for idempotency. |
X-Webhook-Timestamp | Unix epoch timestamp in seconds. |
X-Webhook-Signature | HMAC-SHA256 hex digest. |
X-Webhook-Event | Event name, such as order.created. |
The signature is computed as:
HMAC-SHA256(secret, `${timestamp}.${rawBody}`) -> hexUse the raw request body exactly as received, before JSON parsing. Reject timestamps older than 300 seconds to reduce replay risk.
For idempotency, store X-Webhook-Id and skip duplicate deliveries. For business-level deduplication, combine X-Webhook-Event with the body event_id, which is the public entity ID for order, customer, and product events.
Verification Example
import crypto from "crypto";
function verifyWebhookSignature(rawBody, headers, secret) {
const timestamp = headers["x-webhook-timestamp"];
const signature = headers["x-webhook-signature"];
if (!timestamp || !signature || !/^[a-f0-9]{64}$/i.test(signature)) return false;
const timestampValue = Number(timestamp);
if (!Number.isInteger(timestampValue)) return false;
const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - timestampValue);
if (ageSeconds > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`, "utf8")
.digest("hex");
try {
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
} catch {
return false;
}
}Retry Policy
If your endpoint returns a non-2xx status code, Whatalo retries with exponential backoff:
| Attempt | Delay before attempt |
|---|---|
| 2nd attempt | 1 second |
| 3rd attempt | 2 seconds |
4xx responses are treated as client errors and are not retried. 5xx responses and network errors are retried up to 3 total attempts.