Introduction
Read and write store data from your own systems: products, variants, inventory, orders and webhooks.
This API is served over HTTPS and returns JSON. Every request needs an Authorization: Bearer <token> header.
Keep the token on your server
A token is scoped to the whole store, not to one shopper or one record. Anything its scopes allow, it allows across every order, product and customer the store has. Treat it as a server-side credential: never ship it to a browser, a mobile app or any other client you do not control.
This matters most for shopper-facing pages such as order tracking. Call this API from your own backend, verify there that the requester owns the record, and return only that record to the page.
Pagination
List endpoints are cursor-paginated. Pass limit (1–250, default 50) and read meta.next_cursor from the response; request the next page by sending that value back as cursor. When meta.has_more is false you have reached the end. Cursors are opaque — do not construct or modify them.
Keeping data in sync
products, variants and orders accept updated_since (ISO-8601). Combined with cursor pagination this gives you an incremental feed of everything that changed since your last poll.
The product feed also reports removals, and deleted_at is the signal: drop any record that carries one, whatever its status. Archiving a product in the admin unpublishes it, so it arrives here with deleted_at set and belongs out of your copy too — a shopper cannot buy it. A product removed permanently arrives collapsed to "status": "deleted" and six fields.
Webhooks and this feed cover different ground. After a missed delivery the incremental feed is the first place to look, and for most changes it returns the record. It is not a complete substitute: some admin actions, and some values that are derived rather than stored, raise an event without moving the timestamp the feed filters on, so a missed delivery can fail to resurface. Reconcile against a full listing on a schedule rather than treating updated_since as complete.
A bundle variant's stock is one of those derived values — it is computed from its components rather than stored on the bundle row — so read it from available_stock on GET /variants. The stock field carries a placeholder for a bundle.
The events for customers, subscriptions, subscription charges, discounts, categories and abandoned carts have no endpoint behind them, so no polling reconciles one you never received and the loss is permanent. Those are the deliveries to alert on.
Deliveries are retried on failure, so the same event can reach you more than once. The envelope carries no event or delivery id, so make your receiver idempotent on the payload itself. What identifies a record differs by family: a product payload carries id; an order payload name and guid; a customer payload customer_email; a category payload slug; a subscription or subscription-charge payload name together with order_id; an abandoned-cart payload guid; and a discount payload name, since its code is null on an automatic discount. An inventory_updated payload carries no identifier at all — it names the variant by sku and product_name, and a SKU is not unique within a store — so treat one as a signal to re-read GET /variants?sku= rather than a value to apply directly.
An event whose retries are all exhausted is dropped, and nothing here reports that: there is no delivery-log endpoint, and a webhook's last_dispatched_at records the moment CartGenie queued the delivery, not that you received it. Alert on missing deliveries from your own side.
Rate limits
120 requests per minute and 4 per second, counted per token. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; exceeding either limit returns 429.
Errors
Failures return a JSON body with a message field. 401 means the token is missing or invalid, 403 means the token or its owner lacks the required scope or permission, 404 means the record does not exist, the store is archived, or the Public API is not enabled for the store, 422 means validation failed (with an errors object), and 409 means the request conflicts with the current state — a stale expected_stock, or an order status that does not allow the action.
Three causes share the 404, and the message tells them apart. A record that does not exist, or belongs to another store, carries Resource not found. as its message. An empty message means the Public API is not enabled for the store; every endpoint answers that way, including GET /store, so it is the first thing to check when a brand-new token gets 404 everywhere. An archived store answers 404 on every endpoint too, with This store is archived. as the message.
Requests are validated strictly: any parameter that an endpoint does not define is rejected with 422 and The <field> field is not supported. rather than ignored. Send only the documented fields — echoing an object back with extra keys will fail.
Authenticating requests
To authenticate requests, include an Authorization header with the value "Bearer {YOUR_API_TOKEN}".
All authenticated endpoints are marked with a requires authentication badge in the documentation below.
Tokens are created in the CartGenie dashboard under Settings → Public API by a team member with the Manage store settings permission.
Each token carries a fixed set of scopes and inherits the permissions of the member who created it, so a request succeeds only when the token scope and that member's store role both allow it. Revoking the token, or removing the member from the store, immediately ends access.
The Public API is enabled per store. Until it is switched on for a store, every endpoint answers 404.
Store
Get store information
requires authentication
Returns the store the token belongs to. Useful as a connection check: a
200 here means the token is valid, the Public API is enabled for the
store, and the token's owner still has access to it.
A 404 with an empty message has one cause here: the Public API is not
enabled for the store. Reach for the store settings before suspecting the
token. An archived store answers 404 as well, carrying
This store is archived. as the message.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/store" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/store"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/store';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"name": "Example Store",
"domain": "https://example.webflow.io",
"currency": "usd",
"timezone": "UTC",
"created_at": "2025-11-02T08:31:00.000000Z"
}
Example response (404, Public API is not enabled for this store):
{
"message": ""
}
Example response (404, Store is archived):
{
"message": "This store is archived."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Products
List products
requires authentication
Returns the store's products, oldest update first. Pass updated_since to
receive an incremental feed: the result then also carries the products the
store has removed, so a mirrored catalog can drop them.
deleted_at is the removal signal — drop any record that carries one,
whatever its status. Removal reaches the feed in two shapes. A product
archived in the admin keeps the full product shape and its own status. A
product removed permanently collapses to "status": "deleted" and carries
only id, name, slug, status, deleted_at and updated_at — no
price, stock, variants, images, url or created_at. Read
deleted_at and status before any other field.
Archiving is a removal here by design: it unpublishes the product, so a
shopper cannot buy it and it belongs out of any catalog you mirror from
this feed. Restoring the product in the admin returns it to a later page
with deleted_at back to null, carrying whatever status the restoring
action set: un-archiving returns it as draft, publishing straight out of
the archive returns it as published. It is buyable again only once it is
published.
Reconcile against a full listing on a schedule: a permanent removal is not guaranteed to reach the feed.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/products?limit=50&cursor=eyJpZCI6MzY0fQ&search=tee&updated_since=2026-08-01T00%3A00%3A00Z" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/products"
);
const params = {
"limit": "50",
"cursor": "eyJpZCI6MzY0fQ",
"search": "tee",
"updated_since": "2026-08-01T00:00:00Z",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'limit' => '50',
'cursor' => 'eyJpZCI6MzY0fQ',
'search' => 'tee',
'updated_since' => '2026-08-01T00:00:00Z',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Catalog page):
{
"data": [
{
"id": 364,
"name": "Classic Tee",
"slug": "classic-tee",
"status": "published",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"stock": 120,
"available_stock": 118,
"track_inventory": true,
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg"
},
"url": "https://store.example.com/product/classic-tee",
"variants": [
{
"id": 2301,
"product_id": 364,
"sku": "TEE-BLK-M",
"title": "black-medium",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 2,
"length": 30,
"width": 20,
"weight": 180
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-01T11:20:31.000000Z"
}
],
"created_at": "2026-01-14T09:00:00.000000Z",
"deleted_at": null,
"updated_at": "2026-08-01T11:20:31.000000Z"
}
],
"meta": {
"per_page": 50,
"has_more": true,
"next_cursor": "eyJpZCI6MzY0fQ"
}
}
Example response (200, Incremental feed carrying both removal shapes):
{
"data": [
{
"id": 351,
"name": "Summer Cap",
"slug": "summer-cap",
"status": "deleted",
"deleted_at": "2026-08-03T14:22:09.000000Z",
"updated_at": "2026-08-03T14:22:09.000000Z"
},
{
"id": 358,
"name": "Winter Scarf",
"slug": "winter-scarf",
"status": "archived",
"published": false,
"price": 2900,
"formatted_price": "$29.00",
"stock": 0,
"available_stock": 0,
"track_inventory": true,
"images": {
"main_image": "https://cdn.example.com/scarf.jpg",
"main_image_large": "https://cdn.example.com/scarf-large.jpg"
},
"url": "https://store.example.com/product/winter-scarf",
"variants": [
{
"id": 2288,
"product_id": 358,
"sku": "SCARF-GRY",
"title": "grey",
"published": false,
"price": 2900,
"formatted_price": "$29.00",
"stock": 0,
"reserved_stock": 0,
"available_stock": 0,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 1,
"length": 25,
"width": 15,
"weight": 90
},
"created_at": "2026-02-02T08:12:00.000000Z",
"updated_at": "2026-08-04T09:15:44.000000Z"
}
],
"created_at": "2026-02-02T08:12:00.000000Z",
"deleted_at": "2026-08-04T09:15:44.000000Z",
"updated_at": "2026-08-04T09:15:44.000000Z"
}
],
"meta": {
"per_page": 50,
"has_more": false,
"next_cursor": null
}
}
Example response (422, Malformed cursor):
{
"message": "The cursor is invalid.",
"errors": {
"cursor": [
"The cursor is invalid."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get a product
requires authentication
Includes the product's variants.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/products/364" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/products/364"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 364,
"name": "Classic Tee",
"slug": "classic-tee",
"status": "published",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"stock": 120,
"available_stock": 118,
"track_inventory": true,
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg"
},
"url": "https://store.example.com/product/classic-tee",
"variants": [
{
"id": 2301,
"product_id": 364,
"sku": "TEE-BLK-M",
"title": "black-medium",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 2,
"length": 30,
"width": 20,
"weight": 180
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-01T11:20:31.000000Z"
}
],
"created_at": "2026-01-14T09:00:00.000000Z",
"deleted_at": null,
"updated_at": "2026-08-01T11:20:31.000000Z"
}
Example response (404, Product belongs to another store or does not exist):
{
"message": "Resource not found."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Variants
List variants
requires authentication
Returns every variant in the store, oldest update first. Filter by sku to
resolve your own identifiers into variant ids. SKUs are not unique within a
store, so this always returns an array — match on the fields you need rather
than assuming a single result.
stock is the persisted level, reserved_stock is held by checkouts in
flight, and available_stock is what a shopper can still buy.
A bundle variant holds no stock of its own: stock carries a placeholder
and available_stock is derived from its components, so read that one.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/variants?limit=50&cursor=eyJpZCI6MjMwMX0&sku=TEE-BLK-M&updated_since=2026-08-01T00%3A00%3A00Z" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/variants"
);
const params = {
"limit": "50",
"cursor": "eyJpZCI6MjMwMX0",
"sku": "TEE-BLK-M",
"updated_since": "2026-08-01T00:00:00Z",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/variants';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'limit' => '50',
'cursor' => 'eyJpZCI6MjMwMX0',
'sku' => 'TEE-BLK-M',
'updated_since' => '2026-08-01T00:00:00Z',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 2301,
"product_id": 364,
"sku": "TEE-BLK-M",
"title": "black-medium",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 2,
"length": 30,
"width": 20,
"weight": 180
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-01T11:20:31.000000Z"
}
],
"meta": {
"per_page": 50,
"has_more": false,
"next_cursor": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Inventory
Set variant inventory
requires authentication
Sets the stock level to an absolute value — this is not an increment. The variant must have inventory tracking enabled and must be published.
Send expected_stock to make the write conditional: the update is applied
only while the stored level still matches, and a mismatch answers 409
without changing anything. The conflict response carries actual_stock, so
a retry needs no extra read.
The comparison is against the persisted stock level, not the available one, so checkouts holding reserved stock will not cause spurious conflicts.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/variants/2301/inventory" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"stock\": 120,
\"expected_stock\": 118
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/variants/2301/inventory"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"stock": 120,
"expected_stock": 118
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/variants/2301/inventory';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'stock' => 120,
'expected_stock' => 118,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 2301,
"product_id": 364,
"sku": "TEE-BLK-M",
"title": "black-medium",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 2,
"length": 30,
"width": 20,
"weight": 180
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (409, expected_stock no longer matches):
{
"message": "Inventory changed before the update could be applied.",
"expected_stock": 118,
"actual_stock": 120
}
Example response (409, Variant is not published):
{
"message": "Variant must be published before inventory can be updated."
}
Example response (422, Inventory tracking is off for this variant):
{
"message": "Inventory tracking is not enabled for this variant."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Orders
List orders
requires authentication
Returns the store's orders, oldest update first. Pass updated_since for an
incremental feed. Orders are never removed, so a cancelled order appears as
a status change rather than a deletion.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/orders?limit=50&cursor=eyJpZCI6MTAwMn0&updated_since=2026-08-01T00%3A00%3A00Z" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/orders"
);
const params = {
"limit": "50",
"cursor": "eyJpZCI6MTAwMn0",
"updated_since": "2026-08-01T00:00:00Z",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/orders';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'limit' => '50',
'cursor' => 'eyJpZCI6MTAwMn0',
'updated_since' => '2026-08-01T00:00:00Z',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"order_id": "1002",
"name": "1002",
"guid": "01KSNP2NFV6DJWW6MXME27Q0KE",
"status": "unfulfilled",
"currency_code": "usd",
"coupon_code": null,
"customer_email": "[email protected]",
"customer_name": "John Doe",
"shipment": {
"tracking_url": null,
"tracking_number": null,
"provider": null,
"address": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
},
"payment": {
"label": "Visa ending in 4242",
"status": "paid",
"method": "credit_card",
"transaction_id": "pi_3Q1example"
},
"details": {
"custom_fields": {
"Gift Message": "Happy birthday"
},
"checkout_type": ""
},
"items": [
{
"sku": "TEE-BLK-M",
"quantity": 1,
"price": 4900,
"name": "Classic Tee",
"slug": "classic-tee",
"url": "https://store.example.com/product/classic-tee",
"image_url": "https://cdn.example.com/tee.jpg",
"subtotal": 4900,
"options": [
{
"name": "Size",
"slug": "size",
"value": "M",
"value_slug": "m"
}
],
"weight": 180,
"weight_unit": "g",
"height": 2,
"width": 20,
"length": 30,
"downloads": [],
"has_subscription": false,
"availability": "available",
"estimated_shipping_date": null,
"bundle_components": [],
"fulfilled_quantity": 0,
"remaining_fulfillment_quantity": 1,
"fulfillment_state": "unfulfilled"
}
],
"subtotal": 4900,
"formatted_subtotal": "$49.00",
"total": 5391,
"formatted_total": "$53.91",
"discount_total": 0,
"formatted_discount_total": "$0.00",
"shipping_total": 0,
"formatted_shipping_total": "$0.00",
"shipping_method_label": "Standard",
"tax_total": 491,
"formatted_tax_total": "$4.91",
"created_at": "2026-08-01T10:12:00.000000Z",
"updated_at": "2026-08-01T10:12:00.000000Z",
"placed_at": "2026-08-01T10:12:00.000000Z",
"fulfilled_at": null,
"disputed_at": null,
"discounts": []
}
],
"meta": {
"per_page": 50,
"has_more": true,
"next_cursor": "eyJpZCI6MTAwMn0"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get an order
requires authentication
Orders are addressed by the name the merchant sees, not by a database id.
A refunds array is present only when payment.status is refunded or
partially_refunded; each entry carries id, amount, formatted_amount,
quantity, status, reason and created_at. Treat its absence as
"nothing refunded" rather than as an error.
Some fields are only as complete as the order is. shipment is an object
when the order has at least one shippable item and an empty array [] when
it has none, as on a digital-only order.
Under payment, status always carries a value and reads unpaid before
the charge is captured, while method and transaction_id are null until
the gateway records them. billing and payment.label are null together,
and only on an order whose billing record was never written.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/orders/1002" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/orders/1002"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/orders/1002';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"order_id": "1002",
"name": "1002",
"guid": "01KSNP2NFV6DJWW6MXME27Q0KE",
"status": "unfulfilled",
"currency_code": "usd",
"coupon_code": null,
"customer_email": "[email protected]",
"customer_name": "John Doe",
"shipment": {
"tracking_url": null,
"tracking_number": null,
"provider": null,
"address": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
},
"payment": {
"label": "Visa ending in 4242",
"status": "paid",
"method": "credit_card",
"transaction_id": "pi_3Q1example"
},
"details": {
"custom_fields": {
"Gift Message": "Happy birthday"
},
"checkout_type": ""
},
"items": [
{
"sku": "TEE-BLK-M",
"quantity": 1,
"price": 4900,
"name": "Classic Tee",
"slug": "classic-tee",
"url": "https://store.example.com/product/classic-tee",
"image_url": "https://cdn.example.com/tee.jpg",
"subtotal": 4900,
"options": [
{
"name": "Size",
"slug": "size",
"value": "M",
"value_slug": "m"
}
],
"weight": 180,
"weight_unit": "g",
"height": 2,
"width": 20,
"length": 30,
"downloads": [],
"has_subscription": false,
"availability": "available",
"estimated_shipping_date": null,
"bundle_components": [],
"fulfilled_quantity": 0,
"remaining_fulfillment_quantity": 1,
"fulfillment_state": "unfulfilled"
}
],
"subtotal": 4900,
"formatted_subtotal": "$49.00",
"total": 5391,
"formatted_total": "$53.91",
"discount_total": 0,
"formatted_discount_total": "$0.00",
"shipping_total": 0,
"formatted_shipping_total": "$0.00",
"shipping_method_label": "Standard",
"tax_total": 491,
"formatted_tax_total": "$4.91",
"created_at": "2026-08-01T10:12:00.000000Z",
"updated_at": "2026-08-01T10:12:00.000000Z",
"placed_at": "2026-08-01T10:12:00.000000Z",
"fulfilled_at": null,
"disputed_at": null,
"discounts": []
}
Example response (404, Unknown order):
{
"message": "Resource not found."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update tracking
requires authentication
Adds or replaces the shipping carrier and tracking details on an order. Send
a field as null to clear it. The order must contain at least one shippable
item, and at least one of shipping_carrier, tracking_number or
tracking_url must be present.
Where the details land depends on how many fulfillments the order has. With
more than one, or when the order is partially fulfilled, fulfillment_id is
required and targets that fulfillment. With exactly one, omit it and the
details go to that fulfillment. With none, they go to the order's shipment.
Read the tracking back from where it was written: anything stored on a
fulfillment comes back under the matching entry in fulfillments[], and
shipment.tracking_url, shipment.tracking_number and shipment.provider
are always null once the order has any fulfillment.
Example request:
curl --request PATCH \
"https://api.cartgenie.com/public/v1/orders/1002/tracking" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"shipping_carrier\": \"DHL\",
\"tracking_number\": \"JD014600006281230542\",
\"tracking_url\": \"https:\\/\\/www.dhl.com\\/track?id=JD014600006281230542\",
\"fulfillment_id\": 91
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/orders/1002/tracking"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"shipping_carrier": "DHL",
"tracking_number": "JD014600006281230542",
"tracking_url": "https:\/\/www.dhl.com\/track?id=JD014600006281230542",
"fulfillment_id": 91
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/orders/1002/tracking';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'shipping_carrier' => 'DHL',
'tracking_number' => 'JD014600006281230542',
'tracking_url' => 'https://www.dhl.com/track?id=JD014600006281230542',
'fulfillment_id' => 91,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"order_id": "1002",
"name": "1002",
"guid": "01KSNP2NFV6DJWW6MXME27Q0KE",
"status": "unfulfilled",
"currency_code": "usd",
"coupon_code": null,
"customer_email": "[email protected]",
"customer_name": "John Doe",
"shipment": {
"provider": "DHL",
"tracking_number": "JD014600006281230542",
"tracking_url": "https://www.dhl.com/track?id=JD014600006281230542",
"address": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
},
"payment": {
"label": "Visa ending in 4242",
"status": "paid",
"method": "credit_card",
"transaction_id": "pi_3Q1example"
},
"details": {
"custom_fields": {
"Gift Message": "Happy birthday"
},
"checkout_type": ""
},
"items": [
{
"sku": "TEE-BLK-M",
"quantity": 1,
"price": 4900,
"name": "Classic Tee",
"slug": "classic-tee",
"url": "https://store.example.com/product/classic-tee",
"image_url": "https://cdn.example.com/tee.jpg",
"subtotal": 4900,
"options": [
{
"name": "Size",
"slug": "size",
"value": "M",
"value_slug": "m"
}
],
"weight": 180,
"weight_unit": "g",
"height": 2,
"width": 20,
"length": 30,
"downloads": [],
"has_subscription": false,
"availability": "available",
"estimated_shipping_date": null,
"bundle_components": [],
"fulfilled_quantity": 0,
"remaining_fulfillment_quantity": 1,
"fulfillment_state": "unfulfilled"
}
],
"subtotal": 4900,
"formatted_subtotal": "$49.00",
"total": 5391,
"formatted_total": "$53.91",
"discount_total": 0,
"formatted_discount_total": "$0.00",
"shipping_total": 0,
"formatted_shipping_total": "$0.00",
"shipping_method_label": "Standard",
"tax_total": 491,
"formatted_tax_total": "$4.91",
"created_at": "2026-08-01T10:12:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z",
"placed_at": "2026-08-01T10:12:00.000000Z",
"fulfilled_at": null,
"disputed_at": null,
"discounts": []
}
Example response (422, Nothing to ship):
{
"message": "Order has no shippable items."
}
Example response (422, No tracking field sent):
{
"message": "At least one supported field is required.",
"errors": {
"request": [
"At least one supported field is required."
]
}
}
Example response (422, fulfillment_id omitted on a multi-fulfillment order):
{
"message": "The fulfillment id field is required.",
"errors": {
"fulfillment_id": [
"The fulfillment id field is required."
]
}
}
Example response (422, Fulfillment belongs to another order):
{
"message": "The fulfillment_id does not belong to this order.",
"errors": {
"fulfillment_id": [
"The fulfillment_id does not belong to this order."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Fulfill an order
requires authentication
Marks the whole order fulfilled and, when send_notification is true, emails
the customer. Tracking details sent here are stored alongside the fulfillment,
so a shipment can be recorded in one call.
Only whole-order fulfillment is supported; item-level fulfillment is not part of this version.
Each fulfillments[].items[].order_item_id is an internal identifier for the
line it covers; it is not repeated in items[], which carries no id. A line
covering a bundle carries a fourth key, components[], whose entries hold
live_product_variant_id, name and quantity for each bundled variant.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/orders/1002/fulfill" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"shipping_carrier\": \"DHL\",
\"tracking_number\": \"JD014600006281230542\",
\"tracking_url\": \"https:\\/\\/www.dhl.com\\/track?id=JD014600006281230542\",
\"send_notification\": true
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/orders/1002/fulfill"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"shipping_carrier": "DHL",
"tracking_number": "JD014600006281230542",
"tracking_url": "https:\/\/www.dhl.com\/track?id=JD014600006281230542",
"send_notification": true
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/orders/1002/fulfill';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'shipping_carrier' => 'DHL',
'tracking_number' => 'JD014600006281230542',
'tracking_url' => 'https://www.dhl.com/track?id=JD014600006281230542',
'send_notification' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"order_id": "1002",
"name": "1002",
"guid": "01KSNP2NFV6DJWW6MXME27Q0KE",
"status": "fulfilled",
"currency_code": "usd",
"coupon_code": null,
"customer_email": "[email protected]",
"customer_name": "John Doe",
"shipment": {
"tracking_url": null,
"tracking_number": null,
"provider": null,
"address": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
}
},
"fulfillments": [
{
"id": 91,
"provider": "DHL",
"tracking_number": "JD014600006281230542",
"tracking_url": "https://www.dhl.com/track?id=JD014600006281230542",
"fulfilled_at": "2026-08-14T10:05:12.000000Z",
"items": [
{
"order_item_id": 5501,
"name": "Classic Tee",
"quantity": 1
}
]
}
],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
},
"payment": {
"label": "Visa ending in 4242",
"status": "paid",
"method": "credit_card",
"transaction_id": "pi_3Q1example"
},
"details": {
"custom_fields": {
"Gift Message": "Happy birthday"
},
"checkout_type": ""
},
"items": [
{
"sku": "TEE-BLK-M",
"quantity": 1,
"price": 4900,
"name": "Classic Tee",
"slug": "classic-tee",
"url": "https://store.example.com/product/classic-tee",
"image_url": "https://cdn.example.com/tee.jpg",
"subtotal": 4900,
"options": [
{
"name": "Size",
"slug": "size",
"value": "M",
"value_slug": "m"
}
],
"weight": 180,
"weight_unit": "g",
"height": 2,
"width": 20,
"length": 30,
"downloads": [],
"has_subscription": false,
"availability": "available",
"estimated_shipping_date": null,
"bundle_components": [],
"fulfilled_quantity": 1,
"remaining_fulfillment_quantity": 0,
"fulfillment_state": "fulfilled"
}
],
"subtotal": 4900,
"formatted_subtotal": "$49.00",
"total": 5391,
"formatted_total": "$53.91",
"discount_total": 0,
"formatted_discount_total": "$0.00",
"shipping_total": 0,
"formatted_shipping_total": "$0.00",
"shipping_method_label": "Standard",
"tax_total": 491,
"formatted_tax_total": "$4.91",
"created_at": "2026-08-01T10:12:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z",
"placed_at": "2026-08-01T10:12:00.000000Z",
"fulfilled_at": "2026-08-14T10:05:12.000000Z",
"disputed_at": null,
"discounts": []
}
Example response (409, Order is in a status that cannot be fulfilled):
{
"message": "Order cannot be fulfilled from status canceled."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Unfulfill an order
requires authentication
Returns a fulfilled or partially fulfilled order to the unfulfilled state. Tracking stored on the order's shipment is kept; tracking recorded on a fulfillment is removed with the fulfillment.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/orders/1002/unfulfill" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/orders/1002/unfulfill"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/orders/1002/unfulfill';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"order_id": "1002",
"name": "1002",
"guid": "01KSNP2NFV6DJWW6MXME27Q0KE",
"status": "unfulfilled",
"currency_code": "usd",
"coupon_code": null,
"customer_email": "[email protected]",
"customer_name": "John Doe",
"shipment": {
"provider": "DHL",
"tracking_number": "JD014600006281230542",
"tracking_url": "https://www.dhl.com/track?id=JD014600006281230542",
"address": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
},
"payment": {
"label": "Visa ending in 4242",
"status": "paid",
"method": "credit_card",
"transaction_id": "pi_3Q1example"
},
"details": {
"custom_fields": {
"Gift Message": "Happy birthday"
},
"checkout_type": ""
},
"items": [
{
"sku": "TEE-BLK-M",
"quantity": 1,
"price": 4900,
"name": "Classic Tee",
"slug": "classic-tee",
"url": "https://store.example.com/product/classic-tee",
"image_url": "https://cdn.example.com/tee.jpg",
"subtotal": 4900,
"options": [
{
"name": "Size",
"slug": "size",
"value": "M",
"value_slug": "m"
}
],
"weight": 180,
"weight_unit": "g",
"height": 2,
"width": 20,
"length": 30,
"downloads": [],
"has_subscription": false,
"availability": "available",
"estimated_shipping_date": null,
"bundle_components": [],
"fulfilled_quantity": 0,
"remaining_fulfillment_quantity": 1,
"fulfillment_state": "unfulfilled"
}
],
"subtotal": 4900,
"formatted_subtotal": "$49.00",
"total": 5391,
"formatted_total": "$53.91",
"discount_total": 0,
"formatted_discount_total": "$0.00",
"shipping_total": 0,
"formatted_shipping_total": "$0.00",
"shipping_method_label": "Standard",
"tax_total": 491,
"formatted_tax_total": "$4.91",
"created_at": "2026-08-01T10:12:00.000000Z",
"updated_at": "2026-08-14T10:41:03.000000Z",
"placed_at": "2026-08-01T10:12:00.000000Z",
"fulfilled_at": null,
"disputed_at": null,
"discounts": []
}
Example response (409, Order was never fulfilled):
{
"message": "Order cannot be unfulfilled from status unfulfilled."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Cancel an order
requires authentication
Moves the order to the canceled status and sends the usual cancellation event and email.
This is a status change only. It does not refund the payment, return stock to inventory, remove fulfillments, or cancel subscriptions created by the order — handle those separately if your flow needs them.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/orders/1002/cancel" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/orders/1002/cancel"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/orders/1002/cancel';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"order_id": "1002",
"name": "1002",
"guid": "01KSNP2NFV6DJWW6MXME27Q0KE",
"status": "canceled",
"currency_code": "usd",
"coupon_code": null,
"customer_email": "[email protected]",
"customer_name": "John Doe",
"shipment": {
"tracking_url": null,
"tracking_number": null,
"provider": null,
"address": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"country": "us",
"state": "California",
"region": "us-ca",
"region_name": "California",
"city": "San Francisco",
"address_line_one": "1 Market St",
"address_line_two": null,
"postal_code": "94105",
"validation_result": null
},
"payment": {
"label": "Visa ending in 4242",
"status": "paid",
"method": "credit_card",
"transaction_id": "pi_3Q1example"
},
"details": {
"custom_fields": {
"Gift Message": "Happy birthday"
},
"checkout_type": ""
},
"items": [
{
"sku": "TEE-BLK-M",
"quantity": 1,
"price": 4900,
"name": "Classic Tee",
"slug": "classic-tee",
"url": "https://store.example.com/product/classic-tee",
"image_url": "https://cdn.example.com/tee.jpg",
"subtotal": 4900,
"options": [
{
"name": "Size",
"slug": "size",
"value": "M",
"value_slug": "m"
}
],
"weight": 180,
"weight_unit": "g",
"height": 2,
"width": 20,
"length": 30,
"downloads": [],
"has_subscription": false,
"availability": "available",
"estimated_shipping_date": null,
"bundle_components": [],
"fulfilled_quantity": 0,
"remaining_fulfillment_quantity": 1,
"fulfillment_state": "unfulfilled"
}
],
"subtotal": 4900,
"formatted_subtotal": "$49.00",
"total": 5391,
"formatted_total": "$53.91",
"discount_total": 0,
"formatted_discount_total": "$0.00",
"shipping_total": 0,
"formatted_shipping_total": "$0.00",
"shipping_method_label": "Standard",
"tax_total": 491,
"formatted_tax_total": "$4.91",
"created_at": "2026-08-01T10:12:00.000000Z",
"updated_at": "2026-08-14T11:02:47.000000Z",
"placed_at": "2026-08-01T10:12:00.000000Z",
"fulfilled_at": null,
"disputed_at": null,
"discounts": []
}
Example response (409, Order already reached a final status):
{
"message": "Order cannot be canceled from status canceled."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Webhooks
List webhook subscriptions
requires authentication
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/webhooks?limit=50&cursor=eyJpZCI6N30" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/webhooks"
);
const params = {
"limit": "50",
"cursor": "eyJpZCI6N30",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/webhooks';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'limit' => '50',
'cursor' => 'eyJpZCI6N30',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 7,
"name": "Order sync",
"url": "https://example.com/hooks/cartgenie",
"description": null,
"enabled": true,
"events": [
"new_order",
"order_updated"
],
"last_dispatched_at": "2026-08-14T09:12:00.000000Z",
"created_at": "2026-07-01T12:00:00.000000Z",
"updated_at": "2026-08-14T09:12:00.000000Z"
}
],
"meta": {
"per_page": 50,
"has_more": false,
"next_cursor": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get a webhook subscription
requires authentication
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/webhooks/7" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/webhooks/7"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/webhooks/7';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 7,
"name": "Order sync",
"url": "https://example.com/hooks/cartgenie",
"description": null,
"enabled": true,
"events": [
"new_order",
"order_updated"
],
"last_dispatched_at": "2026-08-14T09:12:00.000000Z",
"created_at": "2026-07-01T12:00:00.000000Z",
"updated_at": "2026-08-14T09:12:00.000000Z"
}
Example response (404, Subscription belongs to another store or does not exist):
{
"message": "Resource not found."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a webhook subscription
requires authentication
Registers a URL to receive the events you list. The URL must be a publicly reachable HTTPS endpoint — private and loopback addresses are rejected.
Receiving deliveries
The request body is {"type": "<event>", "payload": { ... }}. Every delivery
carries a Signature header holding an HMAC-SHA256 of the raw JSON body,
keyed with your store's webhook secret from Settings → Webhooks. Compute
the same digest over the body you received and compare before trusting it.
A delivery is attempted up to three times with exponential backoff and a 10 second timeout, so your endpoint should answer quickly and be safe to call twice with the same event.
Available events
new_order, order_updated, order_fulfillment_updated, new_customer,
customer_updated, new_subscription, subscription_updated,
new_subscription_charge, new_discount, discount_updated,
new_category, category_updated, new_product, product_updated,
new_abandoned_cart, refund_issued, inventory_updated.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/webhooks" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Order sync\",
\"url\": \"https:\\/\\/example.com\\/hooks\\/cartgenie\",
\"description\": \"Feeds our ERP\",
\"enabled\": true,
\"events\": [
\"new_order\",
\"order_updated\"
]
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/webhooks"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Order sync",
"url": "https:\/\/example.com\/hooks\/cartgenie",
"description": "Feeds our ERP",
"enabled": true,
"events": [
"new_order",
"order_updated"
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/webhooks';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Order sync',
'url' => 'https://example.com/hooks/cartgenie',
'description' => 'Feeds our ERP',
'enabled' => true,
'events' => ['new_order', 'order_updated'],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"id": 7,
"name": "Order sync",
"url": "https://example.com/hooks/cartgenie",
"description": null,
"enabled": true,
"events": [
"new_order",
"order_updated"
],
"last_dispatched_at": null,
"created_at": "2026-08-14T09:12:00.000000Z",
"updated_at": "2026-08-14T09:12:00.000000Z"
}
Example response (422, Endpoint is not publicly reachable over HTTPS):
{
"message": "The url must be a publicly reachable HTTPS URL.",
"errors": {
"url": [
"The url must be a publicly reachable HTTPS URL."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update a webhook subscription
requires authentication
Only the fields you send are changed. Sending events replaces the whole
list rather than adding to it.
Example request:
curl --request PATCH \
"https://api.cartgenie.com/public/v1/webhooks/7" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Order sync\",
\"url\": \"https:\\/\\/example.com\\/hooks\\/cartgenie\",
\"description\": \"Feeds our ERP\",
\"enabled\": false,
\"events\": [
\"new_order\"
]
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/webhooks/7"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Order sync",
"url": "https:\/\/example.com\/hooks\/cartgenie",
"description": "Feeds our ERP",
"enabled": false,
"events": [
"new_order"
]
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/webhooks/7';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Order sync',
'url' => 'https://example.com/hooks/cartgenie',
'description' => 'Feeds our ERP',
'enabled' => false,
'events' => ['new_order'],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 7,
"name": "Order sync",
"url": "https://example.com/hooks/cartgenie",
"description": null,
"enabled": false,
"events": [
"new_order"
],
"last_dispatched_at": "2026-08-14T09:12:00.000000Z",
"created_at": "2026-07-01T12:00:00.000000Z",
"updated_at": "2026-08-15T08:30:00.000000Z"
}
Example response (422, Endpoint is not publicly reachable over HTTPS):
{
"message": "The url must be a publicly reachable HTTPS URL.",
"errors": {
"url": [
"The url must be a publicly reachable HTTPS URL."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete a webhook subscription
requires authentication
Example request:
curl --request DELETE \
"https://api.cartgenie.com/public/v1/webhooks/7" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://api.cartgenie.com/public/v1/webhooks/7"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/webhooks/7';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204):
Empty response
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Send a test delivery
requires authentication
Queues one delivery of the chosen event to this subscription, carrying representative sample data rather than a real record. Use it to verify your endpoint and your signature check before going live.
The event must already be one this subscription listens for. The destination is re-validated at this point, so a URL that has since become unreachable is rejected here rather than failing silently later.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/webhooks/7/test" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"event\": \"new_order\"
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/webhooks/7/test"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"event": "new_order"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/webhooks/7/test';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'event' => 'new_order',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (202):
{
"queued": true,
"event": "new_order"
}
Example response (422, Subscription does not listen for that event):
{
"message": "Event is not configured for this webhook."
}
Example response (422, Destination is no longer reachable):
{
"message": "Webhook destination must be a publicly reachable HTTPS URL."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.