Back to Developer

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.created

A 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.updated

Order 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.paid

Payment 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.fulfilled

All 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.cancelled

Order 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.refunded

A 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.voided

Order 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.created

A 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.updated

Product 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.deleted

Product 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_stock

All 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.restocked

A 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.created

A 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.updated

Customer 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.deleted

Customer 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.merged

Two 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.adjusted

Stock 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.transferred

Stock 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_stock

Stock 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_stock

A 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.created

A 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.completed

Checkout 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.abandoned

Checkout 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.captured

Payment 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.failed

Payment 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.refunded

Payment 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.voided

Payment 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.created

A 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.shipped

Fulfillment 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.delivered

Carrier 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.cancelled

Fulfillment 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.created

A 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.completed

An 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.executed

An 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.error

An 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
  }
}

Email

6 events

email.sent

An 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.delivered

Email 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.opened

Recipient 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.clicked

Recipient 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.bounced

Email 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.complained

Recipient 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.sent

A 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.delivered

Direct 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.returned

Direct 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_earned

Customer 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_redeemed

Customer 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_changed

Customer 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.triggered

An 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.completed

A 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.failed

A 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.

AttemptDelayCumulative
1st retry1 minute~1 min after initial
2nd retry10 minutes~11 min after initial
3rd retry1 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"
  }'