Unified Order Service
Home
Getting Started
  • Core API
  • Drone API
Resources
Home
Getting Started
  • Core API
  • Drone API
Resources
    • Core API Reference
    • Orders API Reference
    • Order Items API
    • Channels API Reference
    • Places API Reference
    • Storage Locations API Reference
    • Webhooks API Reference
      • Order Failed Webhook - Operation Type Specification
    • API Keys Reference

Webhooks API Reference

This page documents the Webhooks endpoints in the Core API.

Update Webhook Configuration

Updates webhook configuration for a specific channel.

URL: /v1/channels/{channelId}/webhook

Method: PUT

Auth required: Yes

Request Body:

FieldTypeRequiredDescription
webhook_urlstringYesURL where notifications will be sent
webhook_secretstringYesSecret key for webhook signature verification
webhook_enabledbooleanNoWhether webhook notifications are enabled (default: true)
webhook_eventsarrayNoArray of event types to receive

Example Request:

curl -X PUT "https://api.uos.example.com/v1/channels/channel-123/webhook" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://your-api.com/webhooks/orders",
    "webhook_secret": "your-secret-key",
    "webhook_enabled": true,
    "webhook_events": ["order:created", "order:updated", "order:status_changed", "order:failed", "webhook:test"]
  }'

Success Response:

  • Code: 200 OK
  • Content:
{
  "id": "channel-123",
  "name": "Amazon US Store",
  "type": "amazon",
  "webhook_url": "https://your-api.com/webhooks/orders",
  "webhook_secret": "masked-for-security",
  "webhook_enabled": true,
  "webhook_events": ["order:created", "order:updated", "order:status_changed", "order:failed", "webhook:test"],
  "webhook_last_sent": null,
  "webhook_failures": 0,
  "updated_at": "2023-06-01T12:00:00Z"
}

Error Responses:

  • Code: 400 Bad Request
    • Content: { "error": "Invalid webhook URL or events format" }
  • Code: 404 Not Found
    • Content: { "error": "Channel not found" }

Test Webhook Configuration

Sends a test webhook to verify the channel's webhook configuration.

URL: /v1/channels/{channelId}/webhook/test

Method: POST

Auth required: Yes

Example Request:

curl -X POST "https://api.uos.example.com/v1/channels/channel-123/webhook/test" \
  -H "Authorization: Bearer YOUR_API_KEY"

Success Response:

  • Code: 200 OK
  • Content:
{
  "message": "Test webhook sent successfully",
  "webhook_id": "webhook-123456",
  "delivery_status": "delivered",
  "delivery_timestamp": "2023-06-01T12:05:00Z"
}

Error Responses:

  • Code: 400 Bad Request
    • Content: { "error": "Webhook not configured for this channel" }
  • Code: 404 Not Found
    • Content: { "error": "Channel not found" }
  • Code: 500 Internal Server Error
    • Content: { "error": "Failed to send test webhook", "details": "Connection timeout" }

Configure OAuth 2.0 Authentication

Configure OAuth 2.0 client credentials flow for authenticating webhook deliveries to third-party endpoints.

URL: /v1/channels/{channelId}/oauth2

Method: PUT

Auth required: Yes

Request Body:

FieldTypeRequiredDescription
oauth2_enabledbooleanYesEnable or disable OAuth 2.0 authentication
oauth2_client_idstringYes*OAuth 2.0 client identifier
oauth2_client_secretstringYes*OAuth 2.0 client secret
oauth2_token_urlstringYes*OAuth 2.0 token endpoint URL
oauth2_scopestringNoOAuth 2.0 scope parameter for token requests
oauth2_additional_paramsobjectNoAdditional parameters for token requests

*Required when oauth2_enabled is true

Example Request:

curl -X PUT "https://api.uos.example.com/v1/channels/channel-123/oauth2" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "oauth2_enabled": true,
    "oauth2_client_id": "your-oauth-client-id",
    "oauth2_client_secret": "your-oauth-client-secret",
    "oauth2_token_url": "https://auth.provider.com/oauth/token",
    "oauth2_scope": "webhook:write orders:read",
    "oauth2_additional_params": {
      "audience": "https://api.example.com"
    }
  }'

Success Response:

  • Code: 200 OK
  • Content: Returns the updated channel object with OAuth configuration (client secret will be masked)

Error Responses:

  • Code: 400 Bad Request
    • Content: { "error": "Missing required OAuth field: oauth2_client_id" }
  • Code: 404 Not Found
    • Content: { "error": "Channel not found" }

Get OAuth 2.0 Configuration

Retrieve the current OAuth 2.0 configuration for a channel.

URL: /v1/channels/{channelId}/oauth2

Method: GET

Auth required: Yes

Example Request:

curl -X GET "https://api.uos.example.com/v1/channels/channel-123/oauth2" \
  -H "Authorization: Bearer YOUR_API_KEY"

Success Response:

  • Code: 200 OK
  • Content:
{
  "oauth2_enabled": true,
  "oauth2_client_id": "your-oauth-client-id",
  "oauth2_client_secret": "masked-for-security",
  "oauth2_token_url": "https://auth.provider.com/oauth/token",
  "oauth2_scope": "webhook:write orders:read",
  "oauth2_additional_params": {
    "audience": "https://api.example.com"
  },
  "oauth2_token_cache": {
    "expires_at": "2025-01-16T16:30:00.000Z"
  }
}

Error Responses:

  • Code: 404 Not Found
    • Content: { "error": "Channel not found" }

Test OAuth 2.0 Configuration

Test the OAuth 2.0 configuration by attempting to acquire an access token.

URL: /v1/channels/{channelId}/oauth2/test

Method: POST

Auth required: Yes

Example Request:

curl -X POST "https://api.uos.example.com/v1/channels/channel-123/oauth2/test" \
  -H "Authorization: Bearer YOUR_API_KEY"

Success Response:

  • Code: 200 OK
  • Content:
{
  "success": true,
  "message": "OAuth 2.0 configuration is valid and working",
  "token_acquired": true,
  "oauth2_enabled": true,
  "channel_id": "channel-123",
  "tested_at": "2025-01-16T15:30:00.000Z"
}

Failure Response:

  • Code: 200 OK (test endpoint always returns 200, check success field)
  • Content:
{
  "success": false,
  "message": "OAuth 2.0 configuration test failed: Invalid client credentials",
  "token_acquired": false,
  "oauth2_enabled": true,
  "channel_id": "channel-123",
  "error": "Invalid client credentials",
  "tested_at": "2025-01-16T15:30:00.000Z"
}

Error Responses:

  • Code: 404 Not Found
    • Content: { "error": "Channel not found" }

Remove OAuth 2.0 Configuration

Delete the OAuth 2.0 configuration for a channel, disabling OAuth authentication.

URL: /v1/channels/{channelId}/oauth2

Method: DELETE

Auth required: Yes

Example Request:

curl -X DELETE "https://api.uos.example.com/v1/channels/channel-123/oauth2" \
  -H "Authorization: Bearer YOUR_API_KEY"

Success Response:

  • Code: 200 OK
  • Content:
{
  "message": "OAuth 2.0 configuration deleted successfully",
  "channel": {
    "id": "channel-123",
    "name": "Example Channel",
    "oauth2_enabled": false,
    "oauth2_client_id": null,
    "oauth2_client_secret": null,
    "oauth2_token_url": null,
    "oauth2_scope": null,
    "oauth2_additional_params": {},
    "oauth2_token_cache": {}
  }
}

Error Responses:

  • Code: 404 Not Found
    • Content: { "error": "Channel not found" }

List Webhook Events

Returns a list of all supported webhook event types.

URL: /v1/webhook-events

Method: GET

Auth required: Yes

Example Request:

curl -X GET "https://api.uos.example.com/v1/webhook-events" \
  -H "Authorization: Bearer YOUR_API_KEY"

Success Response:

  • Code: 200 OK
  • Content:
{
  "events": [
    {
      "name": "order:created",
      "description": "Triggered when a new order is created"
    },
    {
      "name": "order:updated",
      "description": "Triggered when an order is updated"
    },
    {
      "name": "order:status_changed",
      "description": "Triggered when an order's status changes"
    },
    {
      "name": "order:failed",
      "description": "Triggered when an order fails processing or validation"
    },
    {
      "name": "webhook:test",
      "description": "Used for testing webhook configuration"
    }
  ]
}

View Webhook Delivery History

Returns the delivery history for a channel's webhooks.

URL: /v1/channels/{channelId}/webhook/history

Method: GET

Auth required: Yes

Parameters:

ParameterTypeRequiredDescription
limitintegerNoMaximum number of records to return (default: 20, max: 100)
offsetintegerNoNumber of records to skip (for pagination)
event_typestringNoFilter by event type
statusstringNoFilter by delivery status (delivered, failed)

Example Request:

curl -X GET "https://api.uos.example.com/v1/channels/channel-123/webhook/history?limit=10&event_type=order:created" \
  -H "Authorization: Bearer YOUR_API_KEY"

Success Response:

  • Code: 200 OK
  • Content:
{
  "data": [
    {
      "id": "webhook-123456",
      "event_type": "order:created",
      "delivery_status": "delivered",
      "delivery_timestamp": "2023-06-01T12:05:00Z",
      "request_url": "https://your-api.com/webhooks/orders",
      "request_headers": {
        "X-Signature": "masked-for-security",
        "Content-Type": "application/json"
      },
      "response_status": 200,
      "response_body": "{\"received\":true}"
    },
    {
      "id": "webhook-123457",
      "event_type": "order:created",
      "delivery_status": "failed",
      "delivery_timestamp": "2023-06-01T11:05:00Z",
      "request_url": "https://your-api.com/webhooks/orders",
      "request_headers": {
        "X-Signature": "masked-for-security",
        "Content-Type": "application/json"
      },
      "response_status": 500,
      "response_body": "{\"error\":\"Internal server error\"}",
      "retry_count": 3,
      "next_retry": "2023-06-01T11:20:00Z"
    }
  ],
  "meta": {
    "total": 2,
    "limit": 10,
    "offset": 0
  }
}

Error Responses:

  • Code: 404 Not Found
    • Content: { "error": "Channel not found" }

Webhook Payload Format

Important: Webhook payloads include the complete order data, not just summary information. This includes the full items array with all product details including image URLs.

Event Envelope Structure

All webhook events use a standardized envelope structure with strict validation requirements:

{
  "event_type": "string (required, minLength: 1)",
  "event_id": "string (required, minLength: 1)", 
  "timestamp": "string (required, ISO 8601 date-time format)",
  "data": "object (required)"
}

Validation Rules:

  • No additional properties allowed at envelope level
  • All envelope fields are required
  • timestamp must be valid ISO 8601 format
  • event_type must be one of the supported event types

Webhook Authentication Headers

When delivering webhooks, UOS includes authentication headers based on your channel configuration:

HMAC Signature (when webhook_secret is configured):

X-Webhook-Signature: sha256=abc123...

OAuth 2.0 Authentication (when OAuth 2.0 is enabled):

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

Both Authentication Methods: When both HMAC and OAuth 2.0 are configured, UOS includes both headers for maximum security:

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
X-Webhook-Signature: sha256=abc123...

This dual authentication approach allows receiving systems to verify webhooks using either method or both methods for enhanced security.

Order Created Event

Example payload for order:created event:

{
  "event_type": "order:created",
  "event_id": "evt-123456",
  "timestamp": "2023-06-01T12:00:00Z",
  "data": {
    "order_id": "order-123",
    "channel_id": "channel-123",
    "order_number": "ORD-001",
    "status": "pending",
    "created_at": "2023-06-01T12:00:00Z",
    "updated_at": "2023-06-01T12:00:00Z",
    "customer_info": {
      "name": "John Smith",
      "email": "john.smith@example.com",
      "phone": "+1234567890"
    },
    "shipping_info": {
      "method": "Home Delivery",
      "address": {
        "street": "123 Main St",
        "city": "London",
        "postal_code": "SW1A 1AA",
        "country": "GB"
      }
    },
    "payment_info": {
      "method": "card",
      "amount": 45.97,
      "currency": "GBP",
      "status": "pending"
    },
    "items": [
      {
        "item_id": "PROD123",
        "sku": "PROD123-VAR1",
        "name": "Premium Widget",
        "quantity": 2,
        "price": 19.99,
        "status": "pending",
        "image_url": "https://example.com/images/premium-widget.jpg",
        "properties": {
          "color": "blue",
          "size": "medium"
        },
        "created_at": "2023-06-01T12:00:00Z",
        "updated_at": "2023-06-01T12:00:00Z"
      },
      {
        "item_id": "PROD456",
        "sku": "PROD456",
        "name": "Basic Gadget",
        "quantity": 1,
        "price": 5.99,
        "status": "pending",
        "image_url": null,
        "properties": {},
        "created_at": "2023-06-01T12:00:00Z",
        "updated_at": "2023-06-01T12:00:00Z"
      }
    ],
    "place_info": {
      "place_id": "place-456",
      "place_external_id": "store-123",
      "name": "Downtown Store",
      "address": "456 High Street, London"
    },
    "totals": {
      "subtotal": 45.97,
      "tax": 0.00,
      "shipping": 0.00,
      "total": 45.97
    }
  }
}

Order Updated Event

Example payload for order:updated event:

{
  "event_type": "order:updated",
  "event_id": "evt-123457",
  "timestamp": "2023-06-01T12:30:00Z",
  "data": {
    "order_id": "order-123",
    "channel_id": "channel-123",
    "order_number": "ORD-001",
    "status": "processing",
    "created_at": "2023-06-01T12:00:00Z",
    "updated_at": "2023-06-01T12:30:00Z",
    "customer_info": {
      "name": "John Smith",
      "email": "john.smith@example.com",
      "phone": "+1234567890"
    },
    "shipping_info": {
      "method": "Home Delivery",
      "address": {
        "street": "123 Main St",
        "city": "London",
        "postal_code": "SW1A 1AA",
        "country": "GB"
      }
    },
    "payment_info": {
      "method": "card",
      "amount": 45.97,
      "currency": "GBP",
      "status": "authorized"
    },
    "items": [
      {
        "item_id": "PROD123",
        "sku": "PROD123-VAR1",
        "name": "Premium Widget",
        "quantity": 2,
        "price": 19.99,
        "status": "picked",
        "image_url": "https://example.com/images/premium-widget.jpg",
        "properties": {
          "color": "blue",
          "size": "medium"
        },
        "created_at": "2023-06-01T12:00:00Z",
        "updated_at": "2023-06-01T12:30:00Z"
      },
      {
        "item_id": "PROD456",
        "sku": "PROD456",
        "name": "Basic Gadget",
        "quantity": 1,
        "price": 5.99,
        "status": "pending",
        "image_url": null,
        "properties": {},
        "created_at": "2023-06-01T12:00:00Z",
        "updated_at": "2023-06-01T12:00:00Z"
      }
    ],
    "place_info": {
      "place_id": "place-456",
      "place_external_id": "store-123",
      "name": "Downtown Store",
      "address": "456 High Street, London"
    },
    "totals": {
      "subtotal": 45.97,
      "tax": 0.00,
      "shipping": 0.00,
      "total": 45.97
    },
    "changes": {
      "status": {
        "from": "pending",
        "to": "processing"
      },
      "items": [
        {
          "item_id": "PROD123",
          "field": "status",
          "from": "pending",
          "to": "picked"
        }
      ]
    }
  }
}

Order Status Changed Event

The order:status_changed event is triggered when an order's status changes. UOS supports various status transitions including the fulfillment workflow.

Supported Status Transitions

UOS supports the following key status transitions for the fulfillment workflow:

  • processing → picking (Order Picking Started)
  • picking → picked (Order Picked)
  • picked → retrieving (Order Being Retrieved)
  • retrieving → collected (Order Collected)
  • retrieving → shipped (Order Shipped)
  • Any status → cancelled (Order Cancelled)

Note: processing status transitions are not sent via webhooks as this is an internal system status. Webhooks begin from the picking status onwards.

Order Picking Started (processing → picking)

Triggered when an order moves from processing to picking status:

{
  "event_type": "order:status_changed",
  "event_id": "evt-456789",
  "timestamp": "2023-06-01T12:00:00Z",
  "data": {
    "order_id": "12e2a812-87a1-4174-9064-4059ac33f7d2",
    "channel_id": "channel-123",
    "order_number": "ORD-001",
    "status": {
      "from": "processing",
      "to": "picking"
    },
    "metadata": {
      "updated_by": "picker-app",
      "user_id": "picker-042",
      "user_name": "Sarah Johnson"
    }
  }
}

Order Picked (picking → picked)

Triggered when an order is picked. Includes detailed item picking information:

{
  "event_type": "order:status_changed",
  "event_id": "evt-456790",
  "timestamp": "2023-06-01T12:10:00Z",
  "data": {
    "order_id": "12e2a812-87a1-4174-9064-4059ac33f7d2",
    "channel_id": "channel-123",
    "order_number": "ORD-001",
    "status": {
      "from": "picking",
      "to": "picked"
    },
    "metadata": {
      "updated_by": "picker-app",
      "user_id": "picker-042",
      "items": [
        {
          "item_id": "PROD123",
          "quantity": 2,
          "properties": {
            "picked_quantity": 2,
            "unpicked_quantity": 0,
            "sub_item_id": null
          }
        },
        {
          "item_id": "PROD456",
          "quantity": 1,
          "properties": {
            "picked_quantity": 0,
            "unpicked_quantity": 1,
            "sub_item_id": "PROD456-ALT"
          }
        }
      ]
    }
  }
}

Item Picking Properties:

  • quantity: Originally ordered quantity (always the customer's original order)
  • picked_quantity: Actual quantity picked (≥ 0)
  • unpicked_quantity: Quantity not picked (≥ 0)
  • sub_item_id: Product ID of substitute item (if substitution occurred)

Understanding Picking Scenarios:

  1. Fully Picked (No Substitution)

    {
      "item_id": "PROD123",
      "quantity": 2,
      "properties": {
        "picked_quantity": 2,
        "unpicked_quantity": 0,
        "sub_item_id": null
      }
    }
    

    The original item was fully picked.

  2. Substitution Occurred

    {
      "item_id": "PROD456",
      "quantity": 1,
      "properties": {
        "picked_quantity": 1,
        "unpicked_quantity": 0,
        "sub_item_id": "PROD789"
      }
    }
    

    The original item (PROD456) was out of stock. A substitute item (PROD789) was picked instead. The picked_quantity refers to the quantity of the substitute item that was picked.

    Note: To get full details about the substitute item (name, price, barcode, etc.), you need to fetch the complete order using the GET order endpoint and look for the item with item_id: "PROD789" in the order's items array.

  3. Out of Stock (No Substitution)

    {
      "item_id": "PROD456",
      "quantity": 1,
      "properties": {
        "picked_quantity": 0,
        "unpicked_quantity": 1,
        "sub_item_id": null
      }
    }
    

    Item was not available and no substitute was selected.

  4. Short Pick (Partial Fulfillment)

    {
      "item_id": "PROD789",
      "quantity": 5,
      "properties": {
        "picked_quantity": 3,
        "unpicked_quantity": 2,
        "sub_item_id": null
      }
    }
    

    Only 3 out of 5 requested items were available and picked.

  5. Partial Substitution

    {
      "item_id": "PROD101",
      "quantity": 5,
      "properties": {
        "picked_quantity": 3,
        "unpicked_quantity": 2,
        "sub_item_id": "PROD202"
      }
    }
    

    The original item was out of stock. A substitute was used, but only 3 units of the substitute were available (customer ordered 5).

Business Rules:

  • metadata.items array is required and must have at least 1 item
  • When sub_item_id is non-null, picked_quantity refers to the substitute item quantity
  • Short/nil picks are indicated by picked_quantity < quantity
  • quantity always represents the original order quantity and never changes
  • picked_quantity + unpicked_quantity should equal quantity

How to Get Substitute Item Details:

When a substitution occurs (sub_item_id is not null), the webhook only includes the substitute item's ID. To get the full details of the substitute item (name, price, barcode, etc.), you have two options:

  1. Fetch the complete order using GET /v1/orders/{orderId} and find the item with item_id matching the sub_item_id value
  2. Use the Drone API /dts/drone/order/products endpoint which includes allowed_substitutions with full product details

Example: Processing a Substitution

// Webhook received indicates substitution
const webhookItem = {
  "item_id": "PROD456",
  "quantity": 1,
  "properties": {
    "picked_quantity": 1,
    "unpicked_quantity": 0,
    "sub_item_id": "PROD789"
  }
};

// Fetch complete order to get substitute details
const order = await fetch(`/v1/orders/${orderId}`);
const substituteItem = order.items.find(item => item.item_id === "PROD789");

// Now you have full details:
console.log(substituteItem.name);     // "Alternative Brand Product"
console.log(substituteItem.price);    // 2.99
console.log(substituteItem.barcode);  // "1234567890123"

Order Being Retrieved (picked → retrieving)

Triggered when an order moves to retrieving status (ready for pickup or delivery):

{
  "event_type": "order:status_changed",
  "event_id": "evt-456791",
  "timestamp": "2023-06-01T14:00:00Z",
  "data": {
    "order_id": "12e2a812-87a1-4174-9064-4059ac33f7d2",
    "channel_id": "channel-123",
    "order_number": "ORD-001",
    "status": {
      "from": "picked",
      "to": "retrieving"
    },
    "metadata": {
      "updated_by": "fulfillment-system",
      "user_id": "system"
    }
  }
}

Order Collected (retrieving → collected)

Triggered when a Click & Collect order is collected by the customer:

{
  "event_type": "order:status_changed",
  "event_id": "evt-456792",
  "timestamp": "2023-06-01T15:33:12Z",
  "data": {
    "order_id": "12e2a812-87a1-4174-9064-4059ac33f7d2",
    "channel_id": "channel-123",
    "order_number": "ORD-001",
    "status": {
      "from": "retrieving",
      "to": "collected"
    },
    "metadata": {
      "updated_by": "picker-app",
      "user_id": "picker-042",
      "user_name": "Sarah Johnson",
      "collected_by": "John Smith",
      "notes": "ID verified at pickup",
      "rejected_items": [
        {
          "item_id": "PROD456",
          "quantity": 1,
          "reason": "collection_rejected_by_customer"
        }
      ]
    }
  }
}

Collection Metadata:

  • collected_by: Name of person who collected the order
  • notes: Additional collection notes
  • rejected_items: Array of items rejected during collection (optional)

Rejection Reasons:

  • collection_rejected_by_customer: Customer refused the item
  • collection_rejected_by_store: Store couldn't provide the item

Generic Status Change Example

For other status transitions, the basic format applies:

{
  "event_type": "order:status_changed",
  "event_id": "evt-456789",
  "timestamp": "2023-06-01T12:10:00Z",
  "data": {
    "order_id": "12e2a812-87a1-4174-9064-4059ac33f7d2",
    "channel_id": "channel-123",
    "order_number": "ORD-001",
    "status": {
      "from": "processing",
      "to": "shipped"
    },
    "metadata": {
      "updated_by": "fulfillment-system",
      "user_id": "system",
      "tracking_number": "TRK123456789"
    }
  }
}

Cancelled Status Webhook

When an order status changes to cancelled, additional cancellation metadata is included:

{
  "event_type": "order:status_changed",
  "event_id": "evt-123459",
  "timestamp": "2023-06-01T12:35:00Z",
  "data": {
    "order_id": "order-123",
    "channel_id": "channel-123",
    "order_number": "ORD-001",
    "status": {
      "from": "processing",
      "to": "cancelled"
    },
    "metadata": {
      "updated_by": "system",
      "cancellation_reason": "customer_request",
      "cancellation_category": "customer",
      "notes": "Customer requested cancellation via support ticket #12345",
      "user_id": "user-123",
      "user_name": "John Smith"
    }
  }
}

Cancellation Metadata Fields:

  • cancellation_reason (string): Standardized cancellation reason code (e.g., customer_request, picking_rejected_by_store)
  • cancellation_category (string): Cancellation category (e.g., customer, store, system)
  • notes (string): Human-readable description or notes about the cancellation. For system-generated cancellations (e.g., items unavailable during picking), this contains a descriptive explanation. For manual cancellations, this contains user-provided notes.
  • user_id (string, optional): ID of the user who cancelled the order
  • user_name (string, optional): Name of the user who cancelled the order

Example: Store Rejection During Picking

When an order is cancelled due to items being unavailable during the picking process:

{
  "event_type": "order:status_changed",
  "event_id": "evt-890456",
  "timestamp": "2023-06-01T14:22:00Z",
  "data": {
    "order_id": "order-456",
    "channel_id": "channel-123",
    "order_number": "ORD-456",
    "status": {
      "from": "picking",
      "to": "cancelled"
    },
    "metadata": {
      "updated_by": "drone_api",
      "cancellation_reason": "picking_rejected_by_store",
      "cancellation_category": "store",
      "notes": "All items unavailable during picking - rejected by store"
    }
  }
}

Order Failed Event

The order:failed event is triggered when an order operation fails. It uses an operation-type oriented approach that provides specific information about what operation failed.

Base Structure

All order:failed events follow this structure:

{
  "event_type": "order:failed",
  "event_id": "evt-{operation}-{unique_id}",
  "timestamp": "2023-06-01T12:15:00Z",
  "data": {
    "channel_id": "channel-123",
    "order_number": "ORD-002",
    "operation_type": "order_status_update",
    "error_count": 1,
    "metadata": {
      "errors": ["Array of error messages"],
      "error_code": "SPECIFIC_ERROR_CODE",
      // Operation-specific fields...
    }
  }
}

Operation Types

The webhook uses operation_type as the primary identifier for what operation failed:

  • order_create - Order creation failed
  • order_status_update - Order status update failed
  • order_validation - Order validation failed
  • place_resolution - Place ID resolution failed

Operation-Specific Metadata

Each operation type includes specific metadata fields in addition to the base errors array:

order_status_update

Used when an order status update operation fails:

{
  "operation_type": "order_status_update",
  "metadata": {
    "errors": ["Invalid status transition from pending to shipped"],
    "error_code": "INVALID_STATUS_TRANSITION",
    "requested_status": "shipped",
    "current_status": "pending",
    "valid_transitions": ["processing", "cancelled"],
    "processor": "order-service",
    "updated_by": "api-user",
    "failure_type": "processing"
  }
}

Key Fields:

  • requested_status - The status that was requested
  • current_status - The current status of the order
  • valid_transitions - Array of valid status transitions from current status
  • processor - Which component failed
  • updated_by - Who/what requested the status change
order_validation

Used when order data fails validation:

{
  "operation_type": "order_validation",
  "metadata": {
    "validation_errors": [
      "Missing required field: customer_info.name",
      "Invalid shipping_info.method: must be 'Home Delivery' or 'Click & Collect'"
    ],
    "validator": "order-validator",
    "order_data": {
      "order_number": "ORD-101",
      "has_items": true,
      "channel_id": "channel-123"
    }
  }
}

Key Fields:

  • validation_errors - Array of validation error messages (required, minItems: 1)
  • validator - Which validator detected the errors
  • order_data - Relevant order data context
order_create

Used when order creation fails:

{
  "operation_type": "order_create",
  "metadata": {
    "errors": ["Failed to create order in backend system"],
    "retry_count": 3,
    "order_data": {
      "order_number": "ORD-003",
      "channel_id": "channel-123"
    }
  }
}

Key Fields:

  • errors - Array of error messages (required, minItems: 1)
  • retry_count - Number of retry attempts made
  • order_data - Relevant order data context
place_resolution

Used when a place external ID cannot be resolved:

{
  "operation_type": "place_resolution",
  "metadata": {
    "errors": ["Failed to resolve place for external_id: store-999"],
    "place_external_id": "store-999"
  }
}

Key Fields:

  • errors - Array of error messages (required, minItems: 1)
  • place_external_id - The external place ID that couldn't be resolved

Complete Examples

Order Status Update Failure:

{
  "event_type": "order:failed", 
  "event_id": "evt-ordsta-xyz789",
  "timestamp": "2023-06-01T12:30:00Z",
  "data": {
    "channel_id": "channel-123",
    "order_number": "ORD-456", 
    "operation_type": "order_status_update",
    "error_count": 1,
    "metadata": {
      "errors": ["Cannot cancel order that is already shipped"],
      "error_code": "INVALID_STATUS_TRANSITION",
      "requested_status": "cancelled",
      "current_status": "shipped",
      "valid_transitions": [],
      "processor": "order-service",
      "updated_by": "api-user",
      "failure_type": "processing"
    }
  }
}

Order Validation Failure:

{
  "event_type": "order:failed",
  "event_id": "evt-ordval-456def",
  "timestamp": "2023-06-01T12:40:00Z",
  "data": {
    "channel_id": "channel-123",
    "order_number": "ORD-101",
    "operation_type": "order_validation",
    "error_count": 2,
    "metadata": {
      "validation_errors": [
        "Missing required field: customer_info.name",
        "Invalid shipping_info.method: must be 'Home Delivery' or 'Click & Collect'"
      ],
      "validator": "order-validator",
      "order_data": {
        "order_number": "ORD-101",
        "has_items": true,
        "channel_id": "channel-123",
        "place_external_id": "4576"
      }
    }
  }
}

Backward Compatibility

The failure_type field is still included for backward compatibility but is now optional. The primary field to use is operation_type.

Migration from Failure-Type to Operation-Type

Legacy Format (deprecated):

{
  "failure_type": "validation", 
  "validation_errors": ["Error message"]
}

New Format (recommended):

{
  "operation_type": "order_validation",
  "validation_errors": ["Error message"],
  "failure_type": "validation"  // Still included for compatibility
}

Verifying Webhook Signatures

To ensure webhook payloads are sent by the Unified Order Service, verify the signature in each webhook request. The signature is included in the X-Signature header.

Example verification code (Node.js):

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
    
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// In your webhook handler:
app.post('/webhooks/orders', (req, res) => {
  const signature = req.headers['x-signature'];
  const webhookSecret = 'your-webhook-secret';
  
  if (verifyWebhookSignature(req.body, signature, webhookSecret)) {
    // Signature is valid, process the webhook
    console.log('Webhook received:', req.body);
    res.status(200).json({ received: true });
  } else {
    // Invalid signature
    console.error('Invalid webhook signature');
    res.status(401).json({ error: 'Invalid signature' });
  }
});

Test Event

Example payload for webhook:test event:

{
  "event_type": "webhook:test",
  "event_id": "evt-123459",
  "timestamp": "2023-06-01T12:05:00Z",
  "data": {
    "message": "This is a test webhook event",
    "channel_id": "channel-123"
  }
}
Last Updated: 12/1/25, 11:31 AM
Prev
Storage Locations API Reference