Webhooks

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:

  1. Endpoint URL — The public HTTPS URL CartAI will POST events to. Must be reachable from the internet.
  2. Authentication — How CartAI should authenticate its requests to your endpoint (see Authentication).
  3. 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 POST requests with a Content-Type: application/json body.
  • Return a 2xx HTTP status within a few seconds of receiving the request.
⚠️

If your endpoint does not respond with 2xx promptly, 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
EventDescriptionderivedDetails populated?
QUEUEDThe checkout task has been accepted and is waiting in the execution queue. No agent has started yet.No
STARTEDAn AI agent has claimed the task and begun execution.No
IN-PROGRESSThe 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
COMPLETEDThe checkout was fully completed and the order is confirmed. Terminal success state.Yes — all fields populated
CANCELLEDThe task was cancelled — either by the customer during PENDING_CONFIRMATION, or by your application via POST /checkout/cancel. Terminal state.Partial or none
FAILEDThe 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

FieldTypeDescription
tidstring (UUID)The task identifier. Matches the taskId returned by POST /checkout. Use this to look up the task in your system.
statusstringThe lifecycle event that triggered this webhook. See Event Lifecycle for all values.
createdAtstring (ISO 8601)Timestamp of when this event was generated.
groupIdstring (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.
merchantstringThe merchant domain the agent is operating on (e.g. saltiebeauty.com).
tasksarrayThe products in this checkout task. See tasks[].
derivedDetailsobjectPricing, shipping, and other data discovered by the agent. Populated progressively — null in early stages. See derivedDetails.
customerobjectEcho of the customer object from the original POST /checkout request. See customer.

tasks[]

FieldTypeDescription
urlstringThe product page URL the agent is purchasing from.
quantityintegerNumber of units being purchased.
selectedVariant.colorstringColor variant selected (empty string if not specified).
selectedVariant.sizestringSize variant selected (empty string if not specified).
selectedVariant.fitstringFit variant selected (empty string if not specified).
optionsobject | nullTask-level execution options. null if not set.
metadataobjectYour pass-through data (title, description, primaryImage, price). Returned as-is from your original request.

derivedDetails

Populated progressively as the agent advances through the checkout. Fields are null until the agent reaches the relevant step.

FieldTypeAvailable fromDescription
pricingobject | nullPENDING_CONFIRMATION or laterThe actual pricing discovered at the merchant's checkout page — subtotal, shipping cost, tax, and grand total as shown to the agent.
shippingMethodobject | nullPENDING_CONFIRMATION or laterThe shipping strategy applied (e.g. { "strategy": "cheapest" }).
selectedShippingOptionstring | nullPENDING_CONFIRMATION or laterThe exact shipping option the agent selected (e.g. "Standard Shipping (Free)" or "UPS Ground - $5.99").

derivedDetails is the most useful field for confirmation flows. When you receive PENDING_CONFIRMATION, call GET /checkout/{taskId} to get the fully populated pricing breakdown, then display it to the customer before they confirm.


customer

An echo of the original customer object. All values are returned exactly as submitted in POST /checkout.

Field pathTypeDescription
contact.firstNamestringBuyer's first name.
contact.lastNamestringBuyer's last name.
contact.emailstringBuyer's email.
contact.phonestringBuyer's phone number.
shippingAddressobjectFull shipping address (addressLine1, addressLine2, city, province, postalCode, country).
billingAddressobject | nullBilling address, same schema. null if not separately provided.
shippingMethod.strategystringShipping strategy used (e.g. "cheapest").
payment.providerstringPayment provider name (e.g. "test").
payment.datastringPayment 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:3000

Copy 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.lt


Security 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 + status as a deduplication key.
  • Respond before processing. Send 200 immediately, 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.