CartAI uses webhooks to push real-time status updates to your server as checkout tasks progress. Instead of polling GET /checkout/{taskId} on a schedule, your application receives an HTTP POST from CartAI the moment anything changes — making webhooks the recommended primary integration pattern for tracking task lifecycle.
How does Webhooks Work for Checkout
When you create a checkout task via POST /checkout, CartAI's AI agent begins executing the purchase in the background. As the agent transitions through each stage — queuing, navigating the merchant site, selecting shipping, processing payment — CartAI fires a POST request to your configured webhook URL carrying the full task state at that moment.
POST /checkout ──► CartAI queues task
│
Agent starts working
│
▼
CartAI POST /your-webhook-url
{ "tid": "...", "status": "STARTED", ... }
│
Agent navigates site
│
▼
CartAI POST /your-webhook-url
{ "tid": "...", "status": "IN-PROGRESS", ... }
│
...
│
▼
CartAI POST /your-webhook-url
{ "tid": "...", "status": "COMPLETED", ... }
Every event carries the same tid (task ID) that was returned when you created the task, so you can correlate events with your own records.
Setting Up Your Webhook Endpoint
Configure your webhook URL in the CartAI Admin Portal under the webhook subscription module. You will provide:
- Endpoint URL — The public HTTPS URL CartAI will POST events to. Must be reachable from the internet.
- Authentication — How CartAI should authenticate its requests to your endpoint (see Authentication).
- Custom headers — Any additional headers CartAI should include on every event request.
Your endpoint must:
- Be accessible over HTTPS — plain HTTP is not recommended in production.
- Accept
POSTrequests with aContent-Type: application/jsonbody. - Return a
2xxHTTP status within a few seconds of receiving the request.
If your endpoint does not respond with2xxpromptly, CartAI may retry delivery. Always acknowledge first, then process.
Authentication
CartAI supports three options for authenticating webhook requests to your endpoint. Configure your preferred method in the Admin Portal.
Basic Authentication
CartAI sends a standard Authorization: Basic <base64(username:password)> header with every event. Validate this header in your webhook receiver before processing the payload.
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
OAuth2
CartAI fetches an access token from your OAuth2 token endpoint before delivering events and includes it as a Bearer token. Suitable for integrations where your webhook endpoint is already part of an OAuth2-protected API.
Authorization: Bearer <access_token>
Custom Headers
Add any arbitrary key-value header pairs that CartAI will include on every event request — for example, a shared secret header your server validates:
X-CartAI-Secret: your-shared-secret-value
No Authentication (testing only)
Webhooks can be configured without any authentication for local development and testing. Do not use this in production — without authentication, any party that discovers your webhook URL can send fake events to your server.
Responding to Webhook Events
Always respond 2xx immediately. CartAI expects a quick acknowledgement. If your handler performs slow work — database writes, downstream API calls, email sends — do that work after you've sent the response.
// Express.js example
app.post("/webhook", (req, res) => {
// ✅ Acknowledge immediately
res.status(200).json({ received: true });
// ✅ Then do your work asynchronously
processEvent(req.body).catch(console.error);
});What happens if you don't respond in time?
CartAI will consider the delivery failed and may retry. Handle retries gracefully — your handler should be idempotent. Use tid + status as a composite key to detect and skip duplicate deliveries:
Event Lifecycle
CartAI emits one webhook event per status transition. The full sequence for a successful checkout:
QUEUED → STARTED → IN-PROGRESS → [CONFIRMED] → PLACED → COMPLETED
Terminal failure paths:
any stage → FAILED
any stage → CANCELLED
| Event | Description | derivedDetails populated? |
|---|---|---|
QUEUED | The checkout task has been accepted and is waiting in the execution queue. No agent has started yet. | No |
STARTED | An AI agent has claimed the task and begun execution. | No |
IN-PROGRESS | The agent is actively navigating the merchant site — adding items to cart, selecting a shipping option, and entering checkout details. | Partially — fields may still be null |
COMPLETED | The checkout was fully completed and the order is confirmed. Terminal success state. | Yes — all fields populated |
CANCELLED | The task was cancelled — either by the customer during PENDING_CONFIRMATION, or by your application via POST /checkout/cancel. Terminal state. | Partial or none |
FAILED | The checkout could not be completed. Terminal failure state. May occur at any stage after STARTED. | Partial or none |
Payload Schema
Every webhook event carries the same JSON structure regardless of the status. Fields that aren't yet known (e.g. pricing before the agent reaches checkout) are null.
Top-level fields
| Field | Type | Description |
|---|---|---|
tid | string (UUID) | The task identifier. Matches the taskId returned by POST /checkout. Use this to look up the task in your system. |
status | string | The lifecycle event that triggered this webhook. See Event Lifecycle for all values. |
createdAt | string (ISO 8601) | Timestamp of when this event was generated. |
groupId | string (UUID) | Groups all tasks belonging to the same logical order. In multi-merchant checkouts, each merchant gets a separate task and webhook stream, but they share a groupId. Use this to show a unified order status across merchants in your UI. |
merchant | string | The merchant domain the agent is operating on (e.g. saltiebeauty.com). |
tasks | array | The products in this checkout task. See tasks[]. |
derivedDetails | object | Pricing, shipping, and other data discovered by the agent. Populated progressively — null in early stages. See derivedDetails. |
customer | object | Echo of the customer object from the original POST /checkout request. See customer. |
tasks[]
tasks[]| Field | Type | Description |
|---|---|---|
url | string | The product page URL the agent is purchasing from. |
quantity | integer | Number of units being purchased. |
selectedVariant.color | string | Color variant selected (empty string if not specified). |
selectedVariant.size | string | Size variant selected (empty string if not specified). |
selectedVariant.fit | string | Fit variant selected (empty string if not specified). |
options | object | null | Task-level execution options. null if not set. |
metadata | object | Your pass-through data (title, description, primaryImage, price). Returned as-is from your original request. |
derivedDetails
derivedDetailsPopulated progressively as the agent advances through the checkout. Fields are null until the agent reaches the relevant step.
| Field | Type | Available from | Description |
|---|---|---|---|
pricing | object | null | PENDING_CONFIRMATION or later | The actual pricing discovered at the merchant's checkout page — subtotal, shipping cost, tax, and grand total as shown to the agent. |
shippingMethod | object | null | PENDING_CONFIRMATION or later | The shipping strategy applied (e.g. { "strategy": "cheapest" }). |
selectedShippingOption | string | null | PENDING_CONFIRMATION or later | The exact shipping option the agent selected (e.g. "Standard Shipping (Free)" or "UPS Ground - $5.99"). |
derivedDetailsis the most useful field for confirmation flows. When you receivePENDING_CONFIRMATION, callGET /checkout/{taskId}to get the fully populated pricing breakdown, then display it to the customer before they confirm.
customer
customerAn echo of the original customer object. All values are returned exactly as submitted in POST /checkout.
| Field path | Type | Description |
|---|---|---|
contact.firstName | string | Buyer's first name. |
contact.lastName | string | Buyer's last name. |
contact.email | string | Buyer's email. |
contact.phone | string | Buyer's phone number. |
shippingAddress | object | Full shipping address (addressLine1, addressLine2, city, province, postalCode, country). |
billingAddress | object | null | Billing address, same schema. null if not separately provided. |
shippingMethod.strategy | string | Shipping strategy used (e.g. "cheapest"). |
payment.provider | string | Payment provider name (e.g. "test"). |
payment.data | string | Payment tokens for real flows or Redacted card details for Test gateway |
Full Payload Example
This is a real IN-PROGRESS event. derivedDetails is not yet populated — the agent is still working through the checkout:
{
"tid": "019af513-ea98-7d21-8f36-3dbe36dd4e05",
"status": "IN_PROGRESS",
"createdAt": "2025-12-06T19:11:54.519Z",
"groupId": "019af513-ea97-7552-91e9-e1f113ea0361",
"merchant": "saltiebeauty.com",
"tasks": [
{
"url": "https://saltiebeauty.com/products/sea-salt-texturizing-spray-enriched-with-sea-salt-marine-botanicals-8-fl-oz-250-ml",
"quantity": 1,
"selectedVariant": {
"color": "",
"size": "",
"fit": ""
},
"options": null,
"metadata": {
"title": "",
"description": "",
"primaryImage": "",
"price": ""
}
}
],
"derivedDetails": {
"pricing": null,
"shippingMethod": null,
"selectedShippingOption": null
},
"customer": {
"contact": {
"firstName": "Carol",
"lastName": "Sturka",
"email": "[email protected]",
"phone": "+16507083507"
},
"shippingAddress": {
"addressLine1": "6104 Plano Parkway",
"addressLine2": "",
"city": "Plano",
"province": "Texas",
"postalCode": "75093",
"country": "United States"
},
"billingAddress": null,
"shippingMethod": {
"strategy": "cheapest"
},
"payment": {
"data": {
"cvv": "***",
"name": "Carol Sturka",
"cardNumber": "************4242",
"expiryYear": "2034",
"expiryMonth": "12"
},
"provider": "test"
}
}
}Handling Each Event
Here is recommended application logic for each webhook event:
app.post("/webhook", (req, res) => {
res.status(200).json({ received: true }); // Always acknowledge first
const { tid, status, groupId, merchant, derivedDetails } = req.body;
switch (status) {
case "QUEUED":
// Optional: mark the task as "pending" in your UI
break;
case "STARTED":
case "IN-PROGRESS":
// Optional: show a "checkout in progress" indicator to the user
break;
case "PENDING_CONFIRMATION":
// Action required — fetch full derivedDetails and show the customer their order summary
// Call GET /checkout/{tid} for fully populated pricing
// Present Confirm / Cancel UI to the customer
notifyCustomerToConfirm(tid, derivedDetails);
break;
case "CONFIRMED":
// Customer confirmed — agent is placing the order
updateOrderStatus(tid, "confirmed");
break;
case "PLACED":
// Order submitted to merchant — update your records
updateOrderStatus(tid, "placed");
break;
case "COMPLETED":
// ✅ Success — order confirmed and complete
finaliseOrder(tid);
sendOrderConfirmationEmail(req.body.customer.contact.email);
break;
case "CANCELLED":
// Task was cancelled — handle cleanup
markOrderCancelled(tid);
break;
case "FAILED":
// ❌ Checkout failed — notify customer, consider retry
handleFailure(tid, merchant);
alertOpsTeam(tid);
break;
}
});Local Development & Testing
Your webhook endpoint must be publicly reachable for CartAI to deliver events. When developing locally, use a tunneling tool to expose your local server.
Using ngrok
# In a separate terminal
npx ngrok http 3000
# Output:
# Forwarding https://abc123.ngrok-free.app → http://localhost:3000Copy the HTTPS URL and set your webhook endpoint in the CartAI Admin Portal to:
https://abc123.ngrok-free.app/webhook
Using localtunnel
npx localtunnel --port 3000
# your url is: https://your-tunnel.loca.ltSecurity Best Practices
- Always use authentication. Configure Basic Auth, OAuth2, or a custom secret header. Unauthenticated webhooks are acceptable for local testing only.
- Validate the authentication header before processing any event payload. Reject requests that don't pass validation with a
401. - Use HTTPS only. Never accept webhook deliveries over plain HTTP in production.
- Make your handler idempotent. CartAI may deliver the same event more than once. Use
tid+statusas a deduplication key. - Respond before processing. Send
200immediately, then do your work asynchronously. This prevents false retry storms if your processing is slow. - Log all incoming events. Store the full raw payload for each event. This gives you an audit trail and makes debugging significantly easier.