Webhooks

What you'll learn

  • What webhooks are and how they work in the ECOM API
  • Which webhook events are available and what triggers each one
  • How webhook URLs are constructed and authenticated
  • The payload structure for each event type
  • How to configure webhooks for your store
  • Retry behavior and idempotency considerations
  • How to validate webhook authenticity

Prerequisites

  • A store configured with a POS integration (Blaze, Treez, LeafLogix, Greenline, or Cova)
  • The store webhook token (provided during store setup — see Setting Up Webhooks)

Overview

Webhooks are outbound HTTP POST requests sent by the ECOM API to your store's configured webhook endpoints whenever a relevant event occurs in a connected POS or third-party service. Rather than polling the API for changes, webhooks push updates to your system in near-real time.

How it works:

  1. An event occurs in the POS (e.g., an order status changes, a new member registers).
  2. The POS sends an HTTP POST to the ECOM API's inbound webhook endpoint for your store.
  3. The ECOM API processes the payload — updating orders, syncing member data, triggering notifications, etc.
  4. The ECOM API always responds with 200 OK and an empty body {}.

Webhooks in this system are inbound — they are received by the ECOM API from external systems (POS, delivery services, payment services). The ECOM API acts as the webhook consumer.


Authentication: Token-Based URL

Webhook URLs use a store-specific token embedded in the URL path. There are no HTTP headers or signatures to verify — the token is the credential.

URL pattern:

POST /api/v1/hooks/{event_name}/{token}

For events that also carry a service identifier:

POST /api/v1/hooks/{event_name}/{service}/{token}

The {token} is a unique, opaque string tied to a specific store. When a request arrives, the StoreTokenPipeline looks up the store by this token. If no store is found, the request is rejected with 404 Not Found.

Keep your token secret. Anyone with the token URL can post arbitrary payloads to your store's webhook endpoints. Rotate the token if it is ever exposed.


Available Events

Event URL Triggered by
order_updated POST /api/v1/hooks/order_updated/:token Order status change in the POS
new_member POST /api/v1/hooks/new_member/:token New member registered in the POS
member_updated POST /api/v1/hooks/member_updated/:token Member profile updated in the POS
job_assigned POST /api/v1/hooks/job_assigned/:token Delivery job assigned to a driver
job_started POST /api/v1/hooks/job_started/:token Driver picked up the delivery
job_arrived POST /api/v1/hooks/job_arrived/:token Driver arrived at delivery address
job_completed POST /api/v1/hooks/job_completed/:token Delivery job completed
identity-verification-updated POST /api/v1/hooks/identity-verification-updated/:service/:token ID verification status updated
identity-verification-completed POST /api/v1/hooks/identity-verification-completed/:service/:token ID verification completed
payment-auth-completed POST /api/v1/hooks/payment-auth-completed/:service/:token 3DS / payment challenge completed

Event Details

order_updated

URL: POST /api/v1/hooks/order_updated/:token

Fired by the POS whenever an order's status changes (e.g., PENDINGCOMPLETED, CANCELLED). The ECOM API processes the update and syncs the corresponding order record.

The payload is the raw POS order object. Its exact shape depends on the connected POS, but all Blaze-backed stores use the following structure:

{
  "id": "ord_7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "orderNo": "1042",
  "status": "COMPLETED",
  "consumerId": "mbr_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "memo": "Please ring the bell",
  "rewardName": null,
  "publicKey": "cart_pub_9x8y7z",
  "memberGroup": "RECREATIONAL",
  "trackingStatus": "DELIVERED",
  "cart": {
    "subTotal": 45.0,
    "discount": 5.0,
    "totalDiscount": 5.0,
    "deliveryFee": 0.0,
    "creditCardFee": 0.0,
    "total": 43.28,
    "taxTotal": 3.28,
    "totalCalcTax": 3.28,
    "promoCode": null,
    "items": [
      {
        "productId": "prod_c3d4e5f6-a7b8-9012-cdef-345678901234",
        "productName": "Blue Dream Pre-Roll",
        "quantity": 2,
        "price": 22.5,
        "totalPrice": 45.0
      }
    ]
  }
}

What the API does: Dispatches to the POS-specific datasource handler (handle_order_update_hook/2), which syncs the order status and line items. Order update notifications may be sent to the customer.


new_member

URL: POST /api/v1/hooks/new_member/:token

Fired when a new member is created in the POS. The ECOM API checks whether an existing ECOM user maps to this POS member (by consumerUserId) and refreshes their profile. If no user exists and the store has customer imports enabled, an import job is enqueued.

The payload contains the POS member record:

{
  "id": "mbr_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "modified": 1700000000000,
  "email": "jane@example.com",
  "firstName": "Jane",
  "lastName": "Doe",
  "phone": "+15551234567",
  "status": "ACTIVE",
  "type": "RECREATIONAL"
}

What the API does: Triggers a notification log entry and calls PointOfSalesUsers.handle_new_member_hook/2. If the user exists in ECOM, their profile is refreshed from the POS. If not, and imports are enabled, an async import job is scheduled.


member_updated

URL: POST /api/v1/hooks/member_updated/:token

Fired when an existing POS member's profile is updated (name, phone, email, status, etc.). The ECOM API syncs the matching user's profile from POS data.

Important: The Blaze POS fires this webhook when the ECOM API calls the Partner API to update a member. Avoid triggering Partner API member updates inside this handler — doing so creates an infinite loop of webhook calls.

The payload has the same shape as new_member:

{
  "id": "mbr_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "modified": 1700005000000,
  "email": "jane.updated@example.com",
  "firstName": "Jane",
  "lastName": "Smith",
  "phone": "+15551234567",
  "status": "ACTIVE",
  "type": "RECREATIONAL"
}

What the API does: Triggers a notification log entry and calls PointOfSalesUsers.handle_member_updated_hook/2, which refreshes POS-sourced fields on the matching ECOM user record.


job_assigned, job_started, job_arrived, job_completed

URLs:

  • POST /api/v1/hooks/job_assigned/:token
  • POST /api/v1/hooks/job_started/:token
  • POST /api/v1/hooks/job_arrived/:token
  • POST /api/v1/hooks/job_completed/:token

Fired by the delivery service as a driver's job progresses through its lifecycle. The store's delivery datasource (DeliveriesDataSourceSwitcher) handles each event.

Payload structure depends on the delivery integration, but typically includes:

{
  "id": "job_d4e5f6a7-b8c9-0123-def4-567890123456",
  "orderId": "ord_7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "driverId": "drv_1a2b3c4d-5e6f-7890-abcd-ef1234567890",
  "status": "ASSIGNED",
  "estimatedArrival": 1700010000000,
  "driverLocation": {
    "lat": 37.7749,
    "lng": -122.4194
  }
}

What the API does: Updates the DeliveryJob record and triggers any associated order or customer notifications.


identity-verification-updated and identity-verification-completed

URLs:

  • POST /api/v1/hooks/identity-verification-updated/:service/:token
  • POST /api/v1/hooks/identity-verification-completed/:service/:token

Fired by the identity verification service (e.g., as a user's document scan progresses). The :service path segment identifies which provider sent the event (e.g., "alpharoot", "onfido").

Payload varies by provider. The service field must be present:

{
  "service": "alpharoot",
  "transactionId": "txn_e5f6a7b8-c9d0-1234-ef56-789012345678",
  "status": "PENDING_REVIEW",
  "userId": "mbr_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "updatedAt": 1700010000000
}

What the API does: Routes to the appropriate IdentityVerificationServiceSwitcher datasource, which updates the user's identity verification record.


payment-auth-completed

URL: POST /api/v1/hooks/payment-auth-completed/:service/:token

Fired by the payment provider after a 3D Secure (3DS) challenge completes. The :service path segment identifies the payment provider. Unlike other webhooks, this handler performs a CAVV lookup and redirects the browser to the storefront's success or failure URL.

Payload must include a cres (challenge response) field:

{
  "service": "adyen",
  "cres": "eyJhbGciOiJSUzI1Ni...",
  "transactionId": "txn_e5f6a7b8-c9d0-1234-ef56-789012345678"
}

What the API does: Looks up the CAVV and ECI values via PaymentsSwitcher, then redirects to:

  • {store_url}checkout/payment-verification/success?cavv=...&eci=... on success
  • {store_url}checkout/payment-verification/failure?msg=... on failure

Setting Up Webhooks

Webhook URLs are configured per store within the POS integration settings. The setup process differs by POS provider, but the general steps are:

  1. Retrieve your store's webhook token. This is available via store configuration — contact your integration team or check the POS integration settings in the ECOM admin. The token is stored in the store record and looked up by Stores.get_by_token/1.

  2. Construct your webhook URL. Use the base URL for your environment:

    Environment Base URL
    Production https://ecom-api.blaze.me
    Staging https://ecom-api.staging.blaze.me
    Development http://localhost:4000

    Example for order_updated on staging:

    https://ecom-api.staging.blaze.me/api/v1/hooks/order_updated/YOUR_STORE_TOKEN
    
  3. Register the URL in your POS. In the Blaze POS (or your configured POS), navigate to the webhook or integrations settings and add each webhook URL for the events you want to receive.

  4. Test the endpoint. Most POS systems allow you to send a test event. Verify you receive a 200 OK response.


Response Format

All webhook endpoints return 200 OK with an empty JSON object body, regardless of whether processing succeeds or fails internally:

{}

The ECOM API is designed to always acknowledge receipt. Internal processing errors are captured via Sentry and logged — they do not result in non-2xx responses.


Retry Behavior and Idempotency

Because the ECOM API always returns 200 OK, the POS or external service is responsible for retry logic. If the POS does not receive a 200 (e.g., due to a network error), it may re-deliver the same event.

Idempotency considerations:

  • order_updated — Safe to replay. The handler re-syncs order state from the POS; delivering the same payload twice results in the same final state.
  • new_member / member_updated — Safe to replay. The handler refreshes the user profile from POS data; duplicate deliveries do not create duplicate records.
  • job_* — Safe to replay. Delivery job records are upserted based on their POS job ID.
  • payment-auth-completed — Replaying this event redirects the browser again. In practice, replays are unlikely because this is a user-facing browser redirect flow.
  • identity-verification-* — Safe to replay. Verification state is updated based on the transaction ID from the provider.

Security Considerations

Token Confidentiality

The webhook token is the only authentication mechanism. Treat it like a password:

  • Never log full webhook URLs in browser-accessible logs.
  • Rotate the token via store configuration if it is ever exposed.
  • Use HTTPS for all webhook URLs (all production and staging URLs use TLS).

Token Validation

The StoreTokenPipeline validates each inbound request before it reaches the handler:

  1. Extracts the :token from the URL path.
  2. Calls Stores.get_by_token(token).
  3. If no matching store is found, responds 404 Not Found and halts — no handler runs.
  4. If found, assigns the store to the connection context for use by the controller.

This means an invalid or guessed token cannot trigger any processing.

Source IP Allowlisting

For additional security, consider allowlisting the IP ranges of your POS provider at your network or load balancer level. Contact your POS provider for their current egress IP list.


Error Codes

Webhook endpoints do not return application-level error codes to callers — they always respond 200 OK. However, these internal conditions can prevent a webhook from being processed correctly:

Condition Behavior
Invalid or unknown token 404 Not Found returned; processing halted
Store not found for token 404 Not Found returned; processing halted
Unknown service in identity verification events Handler receives unsupported service; payload is ignored silently
Unknown service in payment auth events Falls through to error redirect
Missing service field in identity/payment payloads Pattern match fails; Elixir function clause error captured by Sentry
PlasticPay payment with no related order Error logged to Sentry; 200 OK returned