Webhook Event Catalog
Complete reference for all 50 webhook event types the WhaleTools platform can emit, with example payloads.
Event Domains
Events are organized by domain. Each event follows the pattern domain.action and includes a typed payload with the relevant resource data.
Event Envelope
Every webhook delivery wraps the event in a standard envelope. The HTTP headers include the signature, timestamp, event type, and a unique delivery ID for idempotency.
POST https://your-app.com/webhooks/whaletools
Content-Type: application/json
X-Webhook-Signature: sha256=a1b2c3d4e5f6...
X-Webhook-Timestamp: 1741613400
X-Webhook-Event: order.created
X-Webhook-Delivery-Id: 7f3a9c2e-b1d4-4a5f-8c6e-9d0a1b2c3d4e
User-Agent: WhaleTools-Webhook/1.0
{
"id": "evt_...", // Unique event ID
"type": "domain.action", // Event type
"created_at": "ISO 8601", // When the event occurred
"data": { ... } // Domain-specific payload
}Orders
7 events
order.createdA new order was placed in the store.
{
"id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "order.created",
"created_at": "2026-03-10T14:30:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"object": "order",
"order_number": "#10042",
"status": "pending",
"total": 12999,
"currency": "usd",
"customer_id": "cust_9f8e7d6c-5b4a-3210-fedc-ba0987654321",
"line_items": [
{
"product_id": "prod_1a2b3c4d",
"variant_id": "var_5e6f7g8h",
"title": "Classic Tee",
"quantity": 2,
"unit_price": 4999
}
],
"shipping_address": {
"city": "San Francisco",
"state": "CA",
"country": "US"
}
}
}order.updatedOrder details, status, or metadata changed.
{
"id": "evt_b2c3d4e5-f6a7-8901-bcde-f12345678901",
"type": "order.updated",
"created_at": "2026-03-10T14:35:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"object": "order",
"order_number": "#10042",
"status": "processing",
"previous_status": "pending",
"updated_fields": ["status", "notes"]
}
}order.paidPayment was confirmed for an order.
{
"id": "evt_c3d4e5f6-a7b8-9012-cdef-123456789012",
"type": "order.paid",
"created_at": "2026-03-10T14:31:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"object": "order",
"order_number": "#10042",
"total": 12999,
"currency": "usd",
"payment_method": "card",
"payment_id": "pay_x1y2z3w4"
}
}order.fulfilledAll items in the order have been fulfilled.
{
"id": "evt_d4e5f6a7-b8c9-0123-defa-234567890123",
"type": "order.fulfilled",
"created_at": "2026-03-10T16:00:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"object": "order",
"order_number": "#10042",
"status": "fulfilled",
"fulfillment_id": "ful_a1b2c3d4",
"tracking_number": "1Z999AA10123456784",
"carrier": "ups"
}
}order.cancelledOrder was cancelled before fulfillment.
{
"id": "evt_e5f6a7b8-c9d0-1234-efab-345678901234",
"type": "order.cancelled",
"created_at": "2026-03-10T15:00:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"object": "order",
"order_number": "#10042",
"status": "cancelled",
"reason": "customer_request",
"cancelled_by": "customer"
}
}order.refundedA full or partial refund was issued for an order.
{
"id": "evt_f6a7b8c9-d0e1-2345-fabc-456789012345",
"type": "order.refunded",
"created_at": "2026-03-10T17:00:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"object": "order",
"order_number": "#10042",
"refund_amount": 4999,
"currency": "usd",
"refund_reason": "defective_item",
"partial": true
}
}order.voidedOrder was voided before payment was captured.
{
"id": "evt_a7b8c9d0-e1f2-3456-abcd-567890123456",
"type": "order.voided",
"created_at": "2026-03-10T14:32:00.000Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"object": "order",
"order_number": "#10042",
"status": "voided",
"voided_at": "2026-03-10T14:32:00.000Z"
}
}Products
5 events
product.createdA new product was added to the catalog.
{
"id": "evt_p1a2b3c4-d5e6-7890-abcd-ef1234567890",
"type": "product.created",
"created_at": "2026-03-10T10:00:00.000Z",
"data": {
"id": "prod_1a2b3c4d-e5f6-7890-abcd-ef1234567890",
"object": "product",
"title": "Classic Tee",
"handle": "classic-tee",
"status": "active",
"price": 4999,
"currency": "usd",
"variants_count": 3,
"category": "apparel"
}
}product.updatedProduct details, pricing, or metadata changed.
{
"id": "evt_p2b3c4d5-e6f7-8901-bcde-f12345678901",
"type": "product.updated",
"created_at": "2026-03-10T11:00:00.000Z",
"data": {
"id": "prod_1a2b3c4d-e5f6-7890-abcd-ef1234567890",
"object": "product",
"title": "Classic Tee",
"updated_fields": ["price", "description"],
"price": 3999,
"previous_price": 4999
}
}product.deletedProduct was archived or permanently deleted.
{
"id": "evt_p3c4d5e6-f7a8-9012-cdef-123456789012",
"type": "product.deleted",
"created_at": "2026-03-10T12:00:00.000Z",
"data": {
"id": "prod_1a2b3c4d-e5f6-7890-abcd-ef1234567890",
"object": "product",
"title": "Classic Tee",
"deleted_at": "2026-03-10T12:00:00.000Z"
}
}product.out_of_stockAll variants of a product reached zero inventory.
{
"id": "evt_p4d5e6f7-a8b9-0123-defa-234567890123",
"type": "product.out_of_stock",
"created_at": "2026-03-10T13:00:00.000Z",
"data": {
"id": "prod_1a2b3c4d-e5f6-7890-abcd-ef1234567890",
"object": "product",
"title": "Classic Tee",
"variants_out_of_stock": 3,
"total_variants": 3
}
}product.restockedA previously out-of-stock product received new inventory.
{
"id": "evt_p5e6f7a8-b9c0-1234-efab-345678901234",
"type": "product.restocked",
"created_at": "2026-03-10T14:00:00.000Z",
"data": {
"id": "prod_1a2b3c4d-e5f6-7890-abcd-ef1234567890",
"object": "product",
"title": "Classic Tee",
"quantity_added": 50,
"total_available": 50,
"location_id": "loc_a1b2c3d4"
}
}Customers
4 events
customer.createdA new customer account was registered.
{
"id": "evt_cu1a2b3c-d4e5-6789-abcd-ef1234567890",
"type": "customer.created",
"created_at": "2026-03-10T09:00:00.000Z",
"data": {
"id": "cust_9f8e7d6c-5b4a-3210-fedc-ba0987654321",
"object": "customer",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Smith",
"source": "storefront",
"accepts_marketing": true
}
}customer.updatedCustomer profile, address, or preferences changed.
{
"id": "evt_cu2b3c4d-e5f6-7890-bcde-f12345678901",
"type": "customer.updated",
"created_at": "2026-03-10T10:00:00.000Z",
"data": {
"id": "cust_9f8e7d6c-5b4a-3210-fedc-ba0987654321",
"object": "customer",
"updated_fields": ["phone", "default_address"],
"phone": "+14155551234"
}
}customer.deletedCustomer account was deleted or anonymized.
{
"id": "evt_cu3c4d5e-f6a7-8901-cdef-123456789012",
"type": "customer.deleted",
"created_at": "2026-03-10T11:00:00.000Z",
"data": {
"id": "cust_9f8e7d6c-5b4a-3210-fedc-ba0987654321",
"object": "customer",
"deleted_at": "2026-03-10T11:00:00.000Z",
"anonymized": true
}
}customer.mergedTwo customer records were merged into one.
{
"id": "evt_cu4d5e6f-a7b8-9012-defa-234567890123",
"type": "customer.merged",
"created_at": "2026-03-10T12:00:00.000Z",
"data": {
"surviving_id": "cust_9f8e7d6c-5b4a-3210-fedc-ba0987654321",
"merged_id": "cust_1a2b3c4d-e5f6-7890-abcd-ef1234567890",
"object": "customer",
"orders_transferred": 3,
"merged_at": "2026-03-10T12:00:00.000Z"
}
}Inventory
4 events
inventory.adjustedStock level was manually or automatically adjusted.
{
"id": "evt_in1a2b3c-d4e5-6789-abcd-ef1234567890",
"type": "inventory.adjusted",
"created_at": "2026-03-10T08:00:00.000Z",
"data": {
"product_id": "prod_1a2b3c4d",
"variant_id": "var_5e6f7g8h",
"object": "inventory_level",
"location_id": "loc_a1b2c3d4",
"previous_quantity": 100,
"new_quantity": 85,
"adjustment": -15,
"reason": "sale"
}
}inventory.transferredStock was transferred between locations.
{
"id": "evt_in2b3c4d-e5f6-7890-bcde-f12345678901",
"type": "inventory.transferred",
"created_at": "2026-03-10T09:00:00.000Z",
"data": {
"product_id": "prod_1a2b3c4d",
"variant_id": "var_5e6f7g8h",
"object": "inventory_transfer",
"from_location_id": "loc_a1b2c3d4",
"to_location_id": "loc_e5f6g7h8",
"quantity": 25,
"transfer_id": "txfr_x1y2z3w4"
}
}inventory.low_stockStock level fell below the configured low-stock threshold.
{
"id": "evt_in3c4d5e-f6a7-8901-cdef-123456789012",
"type": "inventory.low_stock",
"created_at": "2026-03-10T10:00:00.000Z",
"data": {
"product_id": "prod_1a2b3c4d",
"variant_id": "var_5e6f7g8h",
"object": "inventory_level",
"location_id": "loc_a1b2c3d4",
"current_quantity": 3,
"threshold": 10,
"product_title": "Classic Tee - Medium"
}
}inventory.out_of_stockA variant reached zero available inventory at a location.
{
"id": "evt_in4d5e6f-a7b8-9012-defa-234567890123",
"type": "inventory.out_of_stock",
"created_at": "2026-03-10T11:00:00.000Z",
"data": {
"product_id": "prod_1a2b3c4d",
"variant_id": "var_5e6f7g8h",
"object": "inventory_level",
"location_id": "loc_a1b2c3d4",
"current_quantity": 0,
"product_title": "Classic Tee - Medium"
}
}Checkout
3 events
checkout.createdA new checkout session was initiated.
{
"id": "evt_ch1a2b3c-d4e5-6789-abcd-ef1234567890",
"type": "checkout.created",
"created_at": "2026-03-10T14:00:00.000Z",
"data": {
"id": "chk_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "checkout",
"status": "open",
"total": 12999,
"currency": "usd",
"line_items_count": 2,
"customer_id": null,
"source": "storefront"
}
}checkout.completedCheckout was finalized and converted into an order.
{
"id": "evt_ch2b3c4d-e5f6-7890-bcde-f12345678901",
"type": "checkout.completed",
"created_at": "2026-03-10T14:05:00.000Z",
"data": {
"id": "chk_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "checkout",
"status": "completed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"total": 12999,
"currency": "usd",
"customer_id": "cust_9f8e7d6c"
}
}checkout.abandonedCheckout was not completed within the abandonment window.
{
"id": "evt_ch3c4d5e-f6a7-8901-cdef-123456789012",
"type": "checkout.abandoned",
"created_at": "2026-03-10T16:00:00.000Z",
"data": {
"id": "chk_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "checkout",
"status": "abandoned",
"total": 12999,
"currency": "usd",
"customer_email": "jane@example.com",
"abandoned_at": "2026-03-10T16:00:00.000Z",
"recovery_url": "https://store.example.com/checkout/recover/chk_a1b2c3d4"
}
}Payments
4 events
payment.capturedPayment was successfully captured.
{
"id": "evt_py1a2b3c-d4e5-6789-abcd-ef1234567890",
"type": "payment.captured",
"created_at": "2026-03-10T14:31:00.000Z",
"data": {
"id": "pay_x1y2z3w4-a5b6-7890-cdef-123456789012",
"object": "payment",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"amount": 12999,
"currency": "usd",
"method": "card",
"card_brand": "visa",
"card_last4": "4242",
"captured_at": "2026-03-10T14:31:00.000Z"
}
}payment.failedPayment attempt was declined or failed.
{
"id": "evt_py2b3c4d-e5f6-7890-bcde-f12345678901",
"type": "payment.failed",
"created_at": "2026-03-10T14:31:00.000Z",
"data": {
"id": "pay_x1y2z3w4-a5b6-7890-cdef-123456789012",
"object": "payment",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"amount": 12999,
"currency": "usd",
"method": "card",
"failure_code": "card_declined",
"failure_message": "Your card was declined."
}
}payment.refundedPayment was refunded in full or partially.
{
"id": "evt_py3c4d5e-f6a7-8901-cdef-123456789012",
"type": "payment.refunded",
"created_at": "2026-03-10T17:00:00.000Z",
"data": {
"id": "pay_x1y2z3w4-a5b6-7890-cdef-123456789012",
"object": "payment",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"refund_amount": 4999,
"currency": "usd",
"partial": true,
"refund_id": "ref_m1n2o3p4"
}
}payment.voidedPayment authorization was voided before capture.
{
"id": "evt_py4d5e6f-a7b8-9012-defa-234567890123",
"type": "payment.voided",
"created_at": "2026-03-10T14:35:00.000Z",
"data": {
"id": "pay_x1y2z3w4-a5b6-7890-cdef-123456789012",
"object": "payment",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"amount": 12999,
"currency": "usd",
"voided_at": "2026-03-10T14:35:00.000Z"
}
}Fulfillment
4 events
fulfillment.createdA new fulfillment was created for an order.
{
"id": "evt_fu1a2b3c-d4e5-6789-abcd-ef1234567890",
"type": "fulfillment.created",
"created_at": "2026-03-10T15:00:00.000Z",
"data": {
"id": "ful_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "fulfillment",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"line_items": [
{ "product_id": "prod_1a2b3c4d", "quantity": 2 }
],
"location_id": "loc_a1b2c3d4"
}
}fulfillment.shippedFulfillment was handed off to the carrier.
{
"id": "evt_fu2b3c4d-e5f6-7890-bcde-f12345678901",
"type": "fulfillment.shipped",
"created_at": "2026-03-10T15:30:00.000Z",
"data": {
"id": "ful_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "fulfillment",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "shipped",
"tracking_number": "1Z999AA10123456784",
"tracking_url": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
"carrier": "ups",
"shipped_at": "2026-03-10T15:30:00.000Z"
}
}fulfillment.deliveredCarrier confirmed delivery to the customer.
{
"id": "evt_fu3c4d5e-f6a7-8901-cdef-123456789012",
"type": "fulfillment.delivered",
"created_at": "2026-03-12T10:00:00.000Z",
"data": {
"id": "ful_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "fulfillment",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "delivered",
"delivered_at": "2026-03-12T10:00:00.000Z",
"signed_by": "J. SMITH"
}
}fulfillment.cancelledFulfillment was cancelled before delivery.
{
"id": "evt_fu4d5e6f-a7b8-9012-defa-234567890123",
"type": "fulfillment.cancelled",
"created_at": "2026-03-10T16:00:00.000Z",
"data": {
"id": "ful_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "fulfillment",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "cancelled",
"reason": "out_of_stock",
"cancelled_at": "2026-03-10T16:00:00.000Z"
}
}AI Agents
4 events
agent.conversation.createdA new conversation was started with an AI agent.
{
"id": "evt_ag1a2b3c-d4e5-6789-abcd-ef1234567890",
"type": "agent.conversation.created",
"created_at": "2026-03-10T09:00:00.000Z",
"data": {
"conversation_id": "conv_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "agent_conversation",
"agent_id": "agt_72d7aaa8-2e8b-48a0",
"agent_name": "Sales Assistant",
"channel": "webchat",
"customer_id": "cust_9f8e7d6c",
"started_at": "2026-03-10T09:00:00.000Z"
}
}agent.conversation.completedAn AI agent conversation was closed or resolved.
{
"id": "evt_ag2b3c4d-e5f6-7890-bcde-f12345678901",
"type": "agent.conversation.completed",
"created_at": "2026-03-10T09:15:00.000Z",
"data": {
"conversation_id": "conv_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "agent_conversation",
"agent_id": "agt_72d7aaa8-2e8b-48a0",
"duration_seconds": 900,
"messages_count": 12,
"resolution": "resolved",
"completed_at": "2026-03-10T09:15:00.000Z"
}
}agent.tool.executedAn AI agent executed a tool during a conversation.
{
"id": "evt_ag3c4d5e-f6a7-8901-cdef-123456789012",
"type": "agent.tool.executed",
"created_at": "2026-03-10T09:05:00.000Z",
"data": {
"conversation_id": "conv_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "agent_tool_execution",
"agent_id": "agt_72d7aaa8-2e8b-48a0",
"tool_name": "check_order_status",
"tool_input": { "order_number": "#10042" },
"success": true,
"duration_ms": 230
}
}agent.errorAn AI agent encountered an error during processing.
{
"id": "evt_ag4d5e6f-a7b8-9012-defa-234567890123",
"type": "agent.error",
"created_at": "2026-03-10T09:06:00.000Z",
"data": {
"conversation_id": "conv_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "agent_error",
"agent_id": "agt_72d7aaa8-2e8b-48a0",
"error_type": "tool_timeout",
"error_message": "Tool execution timed out after 30s",
"tool_name": "fetch_product_catalog",
"recoverable": true
}
}6 events
email.sentAn email was sent from the platform.
{
"id": "evt_em1a2b3c-d4e5-6789-abcd-ef1234567890",
"type": "email.sent",
"created_at": "2026-03-10T10:00:00.000Z",
"data": {
"id": "msg_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "email",
"to": "jane@example.com",
"subject": "Your order #10042 has shipped",
"template": "order_shipped",
"sent_at": "2026-03-10T10:00:00.000Z"
}
}email.deliveredEmail was successfully delivered to the recipient.
{
"id": "evt_em2b3c4d-e5f6-7890-bcde-f12345678901",
"type": "email.delivered",
"created_at": "2026-03-10T10:00:05.000Z",
"data": {
"id": "msg_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "email",
"to": "jane@example.com",
"delivered_at": "2026-03-10T10:00:05.000Z"
}
}email.openedRecipient opened the email.
{
"id": "evt_em3c4d5e-f6a7-8901-cdef-123456789012",
"type": "email.opened",
"created_at": "2026-03-10T10:30:00.000Z",
"data": {
"id": "msg_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "email",
"to": "jane@example.com",
"opened_at": "2026-03-10T10:30:00.000Z",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
"open_count": 1
}
}email.clickedRecipient clicked a link in the email.
{
"id": "evt_em4d5e6f-a7b8-9012-defa-234567890123",
"type": "email.clicked",
"created_at": "2026-03-10T10:31:00.000Z",
"data": {
"id": "msg_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "email",
"to": "jane@example.com",
"url": "https://store.example.com/orders/10042/tracking",
"clicked_at": "2026-03-10T10:31:00.000Z"
}
}email.bouncedEmail delivery failed due to a bounce.
{
"id": "evt_em5e6f7a-b8c9-0123-efab-345678901234",
"type": "email.bounced",
"created_at": "2026-03-10T10:00:02.000Z",
"data": {
"id": "msg_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "email",
"to": "invalid@example.com",
"bounce_type": "hard",
"bounce_reason": "550 User not found",
"bounced_at": "2026-03-10T10:00:02.000Z"
}
}email.complainedRecipient marked the email as spam.
{
"id": "evt_em6f7a8b-c9d0-1234-fabc-456789012345",
"type": "email.complained",
"created_at": "2026-03-10T12:00:00.000Z",
"data": {
"id": "msg_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "email",
"to": "jane@example.com",
"complained_at": "2026-03-10T12:00:00.000Z",
"feedback_type": "abuse"
}
}Direct Mail
3 events
direct_mail.sentA direct mail piece was sent to print and fulfillment.
{
"id": "evt_dm1a2b3c-d4e5-6789-abcd-ef1234567890",
"type": "direct_mail.sent",
"created_at": "2026-03-10T08:00:00.000Z",
"data": {
"id": "dm_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "direct_mail",
"type": "postcard",
"customer_id": "cust_9f8e7d6c",
"address": {
"city": "San Francisco",
"state": "CA",
"country": "US"
},
"sent_at": "2026-03-10T08:00:00.000Z",
"expected_delivery": "2026-03-15"
}
}direct_mail.deliveredDirect mail piece was confirmed delivered by the carrier.
{
"id": "evt_dm2b3c4d-e5f6-7890-bcde-f12345678901",
"type": "direct_mail.delivered",
"created_at": "2026-03-14T12:00:00.000Z",
"data": {
"id": "dm_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "direct_mail",
"type": "postcard",
"customer_id": "cust_9f8e7d6c",
"delivered_at": "2026-03-14T12:00:00.000Z"
}
}direct_mail.returnedDirect mail piece was returned to sender as undeliverable.
{
"id": "evt_dm3c4d5e-f6a7-8901-cdef-123456789012",
"type": "direct_mail.returned",
"created_at": "2026-03-17T10:00:00.000Z",
"data": {
"id": "dm_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "direct_mail",
"type": "postcard",
"customer_id": "cust_9f8e7d6c",
"return_reason": "address_not_found",
"returned_at": "2026-03-17T10:00:00.000Z"
}
}Loyalty
3 events
loyalty.points_earnedCustomer earned loyalty points from a purchase or activity.
{
"id": "evt_lo1a2b3c-d4e5-6789-abcd-ef1234567890",
"type": "loyalty.points_earned",
"created_at": "2026-03-10T14:31:00.000Z",
"data": {
"customer_id": "cust_9f8e7d6c",
"object": "loyalty_transaction",
"points_earned": 130,
"source": "order",
"source_id": "550e8400-e29b-41d4-a716-446655440000",
"balance": 1250
}
}loyalty.points_redeemedCustomer redeemed loyalty points for a reward or discount.
{
"id": "evt_lo2b3c4d-e5f6-7890-bcde-f12345678901",
"type": "loyalty.points_redeemed",
"created_at": "2026-03-10T15:00:00.000Z",
"data": {
"customer_id": "cust_9f8e7d6c",
"object": "loyalty_transaction",
"points_redeemed": 500,
"reward": "$5 off next order",
"discount_code": "LOYAL5OFF",
"balance": 750
}
}loyalty.tier_changedCustomer moved to a different loyalty tier.
{
"id": "evt_lo3c4d5e-f6a7-8901-cdef-123456789012",
"type": "loyalty.tier_changed",
"created_at": "2026-03-10T00:00:00.000Z",
"data": {
"customer_id": "cust_9f8e7d6c",
"object": "loyalty_tier",
"previous_tier": "silver",
"new_tier": "gold",
"direction": "upgraded",
"lifetime_points": 5000
}
}Workflows
3 events
workflow.triggeredAn automation workflow was triggered.
{
"id": "evt_wf1a2b3c-d4e5-6789-abcd-ef1234567890",
"type": "workflow.triggered",
"created_at": "2026-03-10T14:31:00.000Z",
"data": {
"workflow_id": "wf_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "workflow_execution",
"workflow_name": "Low Stock Alert",
"trigger": "inventory.low_stock",
"execution_id": "exec_x1y2z3w4",
"status": "running"
}
}workflow.completedA workflow execution finished successfully.
{
"id": "evt_wf2b3c4d-e5f6-7890-bcde-f12345678901",
"type": "workflow.completed",
"created_at": "2026-03-10T14:31:05.000Z",
"data": {
"workflow_id": "wf_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "workflow_execution",
"workflow_name": "Low Stock Alert",
"execution_id": "exec_x1y2z3w4",
"status": "completed",
"steps_executed": 3,
"duration_ms": 4800
}
}workflow.failedA workflow execution failed due to an error.
{
"id": "evt_wf3c4d5e-f6a7-8901-cdef-123456789012",
"type": "workflow.failed",
"created_at": "2026-03-10T14:31:10.000Z",
"data": {
"workflow_id": "wf_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"object": "workflow_execution",
"workflow_name": "Low Stock Alert",
"execution_id": "exec_x1y2z3w4",
"status": "failed",
"error": "Step 2 failed: email service unavailable",
"failed_step": 2,
"duration_ms": 3200
}
}Signature Verification
Every webhook delivery is signed with HMAC-SHA256 using your endpoint's signing secret. Always verify the signature before processing the payload to ensure it originated from WhaleTools and was not tampered with in transit.
The signature is computed over the raw request body and sent in the X-Webhook-Signature header as sha256=<hex digest>. Use timing-safe comparison to prevent timing attacks.
import crypto from 'crypto';
function verifyWebhookSignature(
rawBody: string,
signature: string,
secret: string
): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Example: Express.js handler
app.post('/webhooks/whaletools', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const timestamp = parseInt(req.headers['x-webhook-timestamp'] as string, 10);
const body = req.body.toString('utf8');
// 1. Verify signature
if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Reject stale events (older than 5 minutes)
if (Date.now() / 1000 - timestamp > 300) {
return res.status(401).json({ error: 'Timestamp too old' });
}
// 3. Process the event
const event = JSON.parse(body);
console.log('Received:', event.type, event.id);
res.status(200).json({ received: true });
});Python
import hmac, hashlib, time, json
from flask import Flask, request, abort
app = Flask(__name__)
@app.post('/webhooks/whaletools')
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature', '')
timestamp = int(request.headers.get('X-Webhook-Timestamp', '0'))
body = request.get_data(as_text=True)
# Verify signature
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(), body.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401, 'Invalid signature')
# Reject stale events
if time.time() - timestamp > 300:
abort(401, 'Timestamp too old')
event = json.loads(body)
print(f"Received: {event['type']} {event['id']}")
return {'received': True}Retry Policy
If your endpoint returns a non-2xx status code or times out (30 second limit), WhaleTools retries the delivery up to 3 times with exponential backoff.
| Attempt | Delay | Cumulative |
|---|---|---|
| 1st retry | 1 minute | ~1 min after initial |
| 2nd retry | 10 minutes | ~11 min after initial |
| 3rd retry | 1 hour | ~1 hr 11 min after initial |
After all retries are exhausted, the delivery is marked as failed. Consistently failing endpoints are automatically disabled after 50 consecutive failures, and you will receive an email notification.
Delivery Logs
Every webhook delivery is logged with the request, response, and timing information. Query delivery logs to debug failures or confirm receipt.
curl https://vm.whaletools.cloud/v1/stores/{store_id}/webhooks/{webhook_id}/deliveries \
-H "x-api-key: wk_live_..."
// Response
{
"object": "list",
"data": [
{
"id": "del_a1b2c3d4",
"event_type": "order.created",
"event_id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "delivered",
"http_status": 200,
"attempt": 1,
"response_time_ms": 145,
"created_at": "2026-03-10T14:30:01.000Z"
},
{
"id": "del_b2c3d4e5",
"event_type": "order.paid",
"event_id": "evt_c3d4e5f6-a7b8-9012-cdef-123456789012",
"status": "failed",
"http_status": 500,
"attempt": 3,
"response_time_ms": 2340,
"error": "Internal Server Error",
"created_at": "2026-03-10T14:31:05.000Z",
"next_retry_at": "2026-03-10T15:31:05.000Z"
}
]
}Replay a Delivery
Replay a specific delivery to re-send the original payload to your endpoint. This is useful for recovering from transient failures.
curl -X POST https://vm.whaletools.cloud/v1/stores/{store_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/replay \
-H "x-api-key: wk_live_..."
// Response
{
"id": "del_c3d4e5f6",
"status": "delivered",
"http_status": 200,
"attempt": 1,
"response_time_ms": 98,
"replayed_from": "del_b2c3d4e5"
}Testing Webhooks
Send a test event to your webhook endpoint to verify connectivity and signature validation before going live.
Send a Test Ping
The test endpoint sends a webhook.test event with a sample payload to your URL.
curl -X POST https://vm.whaletools.cloud/v1/stores/{store_id}/webhooks/{webhook_id}/test \
-H "x-api-key: wk_live_..."
// Response
{
"status": "delivered",
"http_status": 200,
"response_time_ms": 112,
"event": {
"id": "evt_test_a1b2c3d4",
"type": "webhook.test",
"created_at": "2026-03-10T14:00:00.000Z",
"data": {
"message": "This is a test webhook delivery.",
"endpoint_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
}Send a Specific Event Type
Test with a specific event type to receive a realistic sample payload for that event.
curl -X POST https://vm.whaletools.cloud/v1/stores/{store_id}/webhooks/{webhook_id}/test \
-H "x-api-key: wk_live_..." \
-H "Content-Type: application/json" \
-d '{ "event_type": "order.created" }'
// Response
{
"status": "delivered",
"http_status": 200,
"response_time_ms": 98,
"event": {
"id": "evt_test_b2c3d4e5",
"type": "order.created",
"created_at": "2026-03-10T14:00:00.000Z",
"data": {
"id": "ord_sample_a1b2c3d4",
"object": "order",
"order_number": "#TEST-001",
"status": "pending",
"total": 9999,
"currency": "usd"
}
}
}Local Development
Use a tunnel service to expose your local server and test webhooks during development.
# Start your local server
node server.js # listening on port 3000
# Expose it via a tunnel (e.g., ngrok, Cloudflare Tunnel)
ngrok http 3000
# Forwarding: https://abc123.ngrok.io -> http://localhost:3000
# Register the tunnel URL as your webhook endpoint
curl -X POST https://vm.whaletools.cloud/v1/stores/{store_id}/webhooks \
-H "x-api-key: wk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://abc123.ngrok.io/webhooks/whaletools",
"events": ["order.created", "order.paid"],
"description": "Local development"
}'