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:
- An event occurs in the POS (e.g., an order status changes, a new member registers).
- The POS sends an HTTP
POSTto the ECOM API's inbound webhook endpoint for your store. - The ECOM API processes the payload — updating orders, syncing member data, triggering notifications, etc.
- The ECOM API always responds with
200 OKand 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., PENDING → COMPLETED, 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/:tokenPOST /api/v1/hooks/job_started/:tokenPOST /api/v1/hooks/job_arrived/:tokenPOST /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/:tokenPOST /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:
-
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. -
Construct your webhook URL. Use the base URL for your environment:
Environment Base URL Production https://ecom-api.blaze.meStaging https://ecom-api.staging.blaze.meDevelopment http://localhost:4000Example for
order_updatedon staging:https://ecom-api.staging.blaze.me/api/v1/hooks/order_updated/YOUR_STORE_TOKEN -
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.
-
Test the endpoint. Most POS systems allow you to send a test event. Verify you receive a
200 OKresponse.
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:
- Extracts the
:tokenfrom the URL path. - Calls
Stores.get_by_token(token). - If no matching store is found, responds
404 Not Foundand halts — no handler runs. - 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 |