Introduction
Read and write store data from your own systems: products, variants, inventory, categories, orders, refunds, customers, discounts 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; GET /products allows at most 50, because each product includes all of its settings) 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. Send back only the cursor the same list handed out: a cursor this list cannot read answers 422, and one from another list that it can read returns the wrong page.
Keeping data in sync
products, variants, orders and customers accept updated_since, an ISO-8601 date and time with an offset (2026-08-01T00:00:00Z or 2026-08-01T03:00:00+03:00; encode + as %2B in a query string). 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 dashboard 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 dashboard 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 published bundle variant's stock is one of those derived values: it is computed from its components, so a sale of a component changes it without moving the bundle's updated_at.
Customers (GET /customers?updated_since=), discounts (GET /discounts) and categories (GET /categories) can be reconciled by listing them. The events for subscriptions, subscription charges 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. X-RateLimit-Limit and X-RateLimit-Remaining describe whichever of the two has fewer requests left, which between bursts is the per-second limit (4). Exceeding either limit returns 429 with a Retry-After header; after a burst over the per-second limit, wait one second before retrying, even when Retry-After is 0.
Errors
Failures return a JSON body with a message field, whatever the request's Accept header. 400 means the body is not valid JSON. 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, 405 means the path exists but does not take that method, 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. 424 means a service the request depends on could not complete it: Webflow rejected the product or category, refused the store's connection or could not list a field's items, or the payment gateway turned a refund down or left its outcome unknown. The message says which. 503 with Retry-After means that service is briefly unavailable or rate limiting; retry after the given delay.
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, as does an id that is not a number, even when the token is invalid. 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. A number may also be sent as a numeric string ("2500"). A PATCH or PUT that sends no fields answers 422. If the endpoint has a required field, the error names that field; otherwise the message is At least one supported field is required.
Order amounts are in the smallest unit of the order's currency_code; the formatted_* fields use the store's current currency format.
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 dashboard picks a token's scopes the way it sets member permissions: an access level per area, never above the creator's own. View Only grants the area's read scopes, Full Access adds its write scopes:
- Orders —
read_orders; full access addswrite_orders,write_refunds - Products —
read_products,read_categories; full access addswrite_products,write_inventory,write_categories - Discounts —
read_discounts; full access addswrite_discounts - Subscriptions —
read_subscriptions - Customers —
read_customers; full access addswrite_customers - Settings —
read_store,read_webhooks; full access addswrite_webhooks
The Public API is enabled per store. Until it is switched on for a store, every endpoint answers 404.
Store
Get the store
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. Check that before suspecting the token. An archived store answers 404 as well, carrying
This store is archived. as the message.
tax_classes lists the store's own tax classes; their id is what PUT /products/{id}/tax takes as
tax_class_id with tax_class: custom. domain is null for a store that has no domain yet.
Requires the read_store token scope.
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",
"tax_classes": [
{
"id": 5,
"name": "Reduced rate"
}
],
"created_at": "2025-11-02T08:31:00.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_store scope required for this endpoint."
}
Example response (404, Public API is not enabled for this store):
{
"message": ""
}
Example response (404, Store is archived):
{
"message": "This store is archived."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 product tax codes
requires authentication
The product tax codes of the store's tax provider (TaxJar), which
PUT /products/{id}/tax takes as tax_code_id. default_tax_code_id is
the code used for a product that has none of its own. For a store without a
tax provider provider and default_tax_code_id are null and data is
empty.
Requires the read_products token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/tax-codes" \
--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/tax-codes"
);
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/tax-codes';
$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):
{
"provider": "taxjar",
"default_tax_code_id": null,
"data": [
{
"id": 41,
"code": "20010",
"name": "Clothing",
"description": "All human wearing apparel suitable for general use"
}
]
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_products scope required for this endpoint."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
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 dashboard 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 dashboard 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.
stock, available_stock and track_inventory are product-level roll-ups
over the variants that carry an option combination. A product with option
sets can also hold a variant that carries none of them (options: []); it is
listed in variants and accepts PUT /variants/{id}/inventory, but
contributes nothing to these three fields, so the totals need not match and
track_inventory can differ from that variant's own. Treat the product-level
figures as the sellable quantity rather than deriving one from the variants
array.
A change this API makes to one of the product's variants moves the product's
updated_at, so the product comes back in the updated_since feed. Stock
changes do not always reach it — orders selling stock down never do — so
follow stock with the inventory_updated webhook.
Each product carries the fields and blocks described under GET /products/{id}.
Requires the read_products token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/products?limit=50&search=tee&slug=classic-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",
"search": "tee",
"slug": "classic-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',
'search' => 'tee',
'slug' => 'classic-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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.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": 4900,
"formatted_price": "$49.00",
"stock": 120,
"available_stock": 118,
"track_inventory": true,
"description": "<p>Soft cotton scarf.</p>",
"meta_description": "Soft cotton scarf in grey.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-SCARF-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2288,
"options": [],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6117.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/scarf.jpg",
"main_image_large": "https://cdn.example.com/scarf-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/scarf.jpg",
"alt_text": "Grey cotton scarf"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/scarf-back.jpg",
"alt_text": null
}
]
},
"url": "https://store.example.com/product/winter-scarf",
"variants": [
{
"id": 2288,
"product_id": 358,
"sku": "SCARF-GRY",
"title": "SCARF-GRY",
"published": false,
"price": 4900,
"formatted_price": "$49.00",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/scarf.jpg",
"alt_text": "Grey cotton scarf"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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 (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_products scope required for this endpoint."
}
Example response (422, Malformed cursor):
{
"message": "The cursor is invalid.",
"errors": {
"cursor": [
"The cursor is invalid."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
stock, available_stock and track_inventory are product-level roll-ups over the
variants that carry an option combination, so they need not equal the sum of the
variants array and track_inventory can differ from an individual variant's own.
See the list endpoint for the full explanation.
A product carries description, meta_description, product_type,
custom_fields and one block per settings endpoint — categories, tax,
shipping, and purchase_limits, pre_order, feed and
personalization_fields where the store has that feature turned on. Each
block has the shape its endpoint accepts, so you can edit a block and send
it back; personalization_fields is a list, sent back as
{"personalization_fields": [...]}. A variant's compare_at_price and
formatted_compare_at_price are null when not set.
published says whether the product is on sale on the storefront. It can
differ from status: a product on sale can read queued, or sync_error
after Webflow refused a change, and stays on sale until it is moved to
draft or archived.
Requires the read_products token scope.
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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 product
requires authentication
Creates a draft product with one variant from the fields a product needs up
front: its name and price, plus the optional details PATCH /products/{id}
also takes. Everything else is set afterwards with its own endpoint —
categories, tax class, shipping, then the variant's SKU, weight and
dimensions (PATCH /variants/{id}) and stock
(PUT /variants/{id}/inventory) — and the product goes live with
PUT /products/{id}/status. Until then it exists only in CartGenie, so
creating it never waits on Webflow.
price is in the store currency's smallest unit. Without slug one is made
from the name and numbered past the store's existing slugs
(classic-tee-2). A new product uses the built-in default tax class and
the physical type unless product_type says otherwise.
Fires the new_product webhook.
Requires the write_products token scope.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/products" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"custom_fields\": {
\"12\": \"Cotton\"
},
\"name\": \"Classic Tee\",
\"slug\": \"classic-tee\",
\"description\": \"<p>Soft cotton tee.<\\/p>\",
\"meta_description\": \"A soft cotton tee.\",
\"product_type\": \"physical\",
\"price\": 2500
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"custom_fields": {
"12": "Cotton"
},
"name": "Classic Tee",
"slug": "classic-tee",
"description": "<p>Soft cotton tee.<\/p>",
"meta_description": "A soft cotton tee.",
"product_type": "physical",
"price": 2500
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'custom_fields' => [12 => 'Cotton'],
'name' => 'Classic Tee',
'slug' => 'classic-tee',
'description' => '<p>Soft cotton tee.</p>',
'meta_description' => 'A soft cotton tee.',
'product_type' => 'physical',
'price' => 2500,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"id": 366,
"name": "Classic Tee",
"slug": "classic-tee",
"status": "draft",
"published": false,
"price": 2500,
"formatted_price": "$25.00",
"stock": 0,
"available_stock": 0,
"track_inventory": false,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "A soft cotton tee.",
"product_type": "physical",
"categories": {
"category_ids": [],
"primary_category_id": null
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": null
},
"purchase_limits": {
"min_quantity_per_order": null,
"max_quantity_per_order": null
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": null,
"condition": "new",
"mpn": null,
"gtin": null,
"google_product_category_id": null,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [],
"download": null,
"default_variant_id": 2352,
"options": [],
"bundle": null,
"shipping": {
"requires_shipping": false,
"auto_fulfill_order": false,
"hs_code": null,
"country_of_origin": null
},
"custom_fields": {
"12": "Cotton",
"13": null
},
"images": {
"main_image": "",
"main_image_large": "",
"main": null,
"extra": []
},
"url": "https://store.example.com/product/classic-tee",
"variants": [
{
"id": 2352,
"product_id": 366,
"sku": null,
"title": null,
"published": false,
"price": 2500,
"formatted_price": "$25.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [],
"subscription": null,
"download": null,
"images": {
"main": null,
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 0,
"reserved_stock": 0,
"available_stock": 0,
"track_inventory": false,
"requires_shipping": false,
"dimensions": {
"height": null,
"length": null,
"width": null,
"weight": null
},
"created_at": "2026-10-06T09:00:00.000000Z",
"updated_at": "2026-10-06T09:00:00.000000Z"
}
],
"created_at": "2026-10-06T09:00:00.000000Z",
"deleted_at": null,
"updated_at": "2026-10-06T09:00:00.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (422, Slug taken):
{
"message": "The slug has already been taken.",
"errors": {
"slug": [
"The slug has already been taken."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 product
requires authentication
Changes the product's own details. Only the fields you send change. Other
parts of the product have their own endpoints: status
(PUT /products/{id}/status), categories, tax class, shipping, purchase
limits, pre-order, feed details and personalization fields;
prices, SKUs, weight and dimensions live on the variants
(PATCH /variants/{id}).
slug is normalized the way the product editor does it (Classic Tee!
becomes classic-tee) and changes the product's page address on the
site. product_type is physical, digital, service or advanced.
Saving works like the product editor's Save: a draft or queued product
only changes in CartGenie, and a published product is republished with the
change, Webflow included, before the response. When Webflow refuses the
product nothing is saved (424); when Webflow is rate limiting the site
nothing is saved and the call answers 503 with Retry-After. When the
change would leave a published product unable to be published (no shipping
zones for a shippable product, no Webflow products collection), the call
answers 422 and nothing is saved.
Fires the product_updated webhook when something changed.
Requires the write_products token scope.
Example request:
curl --request PATCH \
"https://api.cartgenie.com/public/v1/products/364" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"custom_fields\": {
\"12\": \"Cotton\"
},
\"name\": \"Classic Tee\",
\"slug\": \"classic-tee\",
\"description\": \"<p>Soft cotton tee.<\\/p>\",
\"meta_description\": \"Soft cotton tee in five colors.\",
\"product_type\": \"physical\"
}"
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",
};
let body = {
"custom_fields": {
"12": "Cotton"
},
"name": "Classic Tee",
"slug": "classic-tee",
"description": "<p>Soft cotton tee.<\/p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'custom_fields' => [12 => 'Cotton'],
'name' => 'Classic Tee',
'slug' => 'classic-tee',
'description' => '<p>Soft cotton tee.</p>',
'meta_description' => 'Soft cotton tee in five colors.',
'product_type' => 'physical',
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Published product cannot be republished):
{
"message": "The product is published and could not be republished with this change: Product contains shippable variants but store has no shipping zones configured. Move it to draft first, or fix the store setup.",
"errors": {
"product": [
"The product is published and could not be republished with this change: Product contains shippable variants but store has no shipping zones configured. Move it to draft first, or fix the store setup."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a product's status
requires authentication
Works like the product editor's save buttons: draft takes the product
off the live site, queued stages it for the next Webflow site publish,
and published publishes it. Stores without Webflow treat queued as
published. This is the one endpoint that sends a draft or queued product to
Webflow; the other product endpoints only change it in CartGenie until then.
The Webflow item is written before the response. When Webflow refuses the
product, the call answers 424 and the requested status is not applied. A
product that was not published is then marked sync_error, as in the
editor, with no webhook, and stays there until Webflow accepts a later
status change. When Webflow is rate limiting the site, nothing changes and
the call answers 503 with Retry-After.
Fires the product_updated webhook when the status changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/status" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"published\"
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/status"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "published"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/status';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'status' => 'published',
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Product cannot be published):
{
"message": "Product contains shippable variants but store has no shipping zones configured.",
"errors": {
"status": [
"Product contains shippable variants but store has no shipping zones configured."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a product's categories
requires authentication
category_ids replaces the whole list and reads back sorted by id; send
[] to remove the product from every category. primary_category_id is
the category that product feeds use and must be one of category_ids;
without it the current primary category stays while it is still in the
list, otherwise the first category sent becomes primary, as in the product
editor.
Works like Save in the product editor: a published product is republished
with the change before the response (424 when Webflow refuses it, 503 with
Retry-After when Webflow is rate limiting the site, 422 when the change
would leave the product unable to be published). Fires the product_updated
webhook when something changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/categories" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"category_ids\": [
7,
9
],
\"primary_category_id\": 7
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/categories"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"category_ids": [
7,
9
],
"primary_category_id": 7
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/categories';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'category_ids' => [7, 9],
'primary_category_id' => 7,
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Primary category not in the list):
{
"message": "The primary category must be one of category_ids.",
"errors": {
"primary_category_id": [
"The primary category must be one of category_ids."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a product's tax class and tax code
requires authentication
tax_class is default, non_taxable or custom with the
tax_class_id of one of the store's tax classes (GET /store lists
them). With tax-inclusive pricing, each variant keeps its price and the tax
included in it follows the new class, as in the product editor.
Works like Save in the product editor: a published product is republished
with the change before the response (424 when Webflow refuses it, 503 with
Retry-After when Webflow is rate limiting the site, 422 when the change
would leave the product unable to be published). Fires the product_updated
webhook when something changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/tax" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"tax_class\": \"custom\",
\"tax_class_id\": 5,
\"tax_code_id\": 41
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/tax"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"tax_class": "custom",
"tax_class_id": 5,
"tax_code_id": 41
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/tax';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'tax_class' => 'custom',
'tax_class_id' => 5,
'tax_code_id' => 41,
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Tax class of another store):
{
"message": "The selected tax class id is invalid.",
"errors": {
"tax_class_id": [
"The selected tax class id is invalid."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a product's shipping details
requires authentication
Only the fields you send change. requires_shipping applies to every
variant, as in the product editor. auto_fulfill_order marks orders
fulfilled right after purchase and only fits a product that does not
ship: turning shipping on for such a product needs
auto_fulfill_order: false in the same call. hs_code and
country_of_origin (a two-letter code) are the customs details for
international orders. A bundle takes all of these from its components and
refuses them.
Works like Save in the product editor: a published product is republished
with the change before the response (424 when Webflow refuses it, 503 with
Retry-After when Webflow is rate limiting the site, 422 when the change
would leave the product unable to be published — for example when shipping
is turned on and the store has no shipping zones). Fires the
product_updated webhook when something changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/shipping" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"requires_shipping\": true,
\"auto_fulfill_order\": false,
\"hs_code\": \"6109.10\",
\"country_of_origin\": \"pt\"
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/shipping"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/shipping';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'requires_shipping' => true,
'auto_fulfill_order' => false,
'hs_code' => '6109.10',
'country_of_origin' => 'pt',
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Automatic fulfillment on a product that ships):
{
"message": "A product that requires shipping is fulfilled when it ships, not automatically. Send auto_fulfill_order false with requires_shipping true.",
"errors": {
"auto_fulfill_order": [
"A product that requires shipping is fulfilled when it ships, not automatically. Send auto_fulfill_order false with requires_shipping true."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a product's purchase limits
requires authentication
The smallest and largest quantity of the product one order may hold. Only
the fields you send change; null removes a limit, and both null turn
limits off. Sending a maximum to a product without limits sets the minimum
to 1. A new minimum above 1 may not exceed the stock of a product that
tracks inventory. A store without purchase limits enabled answers 422.
Works like Save in the product editor: a published product is republished
with the change before the response (424 when Webflow refuses it, 503 with
Retry-After when Webflow is rate limiting the site, 422 when the change
would leave the product unable to be published). Fires the product_updated
webhook when something changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/purchase-limits" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"min_quantity_per_order\": 2,
\"max_quantity_per_order\": 10
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/purchase-limits"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"min_quantity_per_order": 2,
"max_quantity_per_order": 10
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/purchase-limits';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'min_quantity_per_order' => 2,
'max_quantity_per_order' => 10,
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Minimum above the stock):
{
"message": "The minimum quantity per order cannot exceed the 3 currently in stock.",
"errors": {
"min_quantity_per_order": [
"The minimum quantity per order cannot exceed the 3 currently in stock."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a product's pre-order settings
requires authentication
pre_order: true sells the product ahead of stock: orders placed meanwhile
are marked as pre-orders in the cart, checkout and order confirmation.
estimated_shipping_date is free text shown to shoppers ("Mid 2026",
"Jan 15, 2026") and exists only while the product is a pre-order, so turning
pre-order off clears it. Only the fields you send change. A store without
pre-orders enabled answers 422.
Works like Save in the product editor: a published product is republished
with the change before the response (424 when Webflow refuses it or its
pre-order fields cannot be created, 503 with Retry-After when Webflow is
rate limiting the site, 422 when the change would leave the product unable
to be published). Fires the product_updated webhook when something
changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/pre-order" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"pre_order\": true,
\"estimated_shipping_date\": \"Mid 2026\"
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/pre-order"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"pre_order": true,
"estimated_shipping_date": "Mid 2026"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/pre-order';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'pre_order' => true,
'estimated_shipping_date' => 'Mid 2026',
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Date without pre-order):
{
"message": "An estimated shipping date applies only to a pre-order. Send pre_order true with it.",
"errors": {
"estimated_shipping_date": [
"An estimated shipping date applies only to a pre-order. Send pre_order true with it."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a product's feed details
requires authentication
What product feeds (Google Shopping and others) send for the product. Only
the fields you send change. condition is new, used or refurbished;
gtin is 8 to 14 digits; google_product_category_id is an id from
Google's product taxonomy; gender (male, female, unisex) and
age_group (newborn, infant, toddler, kids, adult) apply to every
variant, as in the product editor. The primary category used by feeds is
set with PUT /products/{id}/categories. A store without product feeds enabled
answers 422.
Works like Save in the product editor: a published product is republished
with the change before the response (424 when Webflow refuses it, 503 with
Retry-After when Webflow is rate limiting the site, 422 when the change
would leave the product unable to be published). Fires the product_updated
webhook when something changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/feed" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"brand\": \"Northwind\",
\"condition\": \"new\",
\"mpn\": \"NW-TEE-01\",
\"gtin\": \"00012345678905\",
\"google_product_category_id\": 212,
\"include_variant_info_in_title_feed\": true,
\"gender\": \"unisex\",
\"age_group\": \"adult\"
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/feed"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": "00012345678905",
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/feed';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'brand' => 'Northwind',
'condition' => 'new',
'mpn' => 'NW-TEE-01',
'gtin' => '00012345678905',
'google_product_category_id' => 212,
'include_variant_info_in_title_feed' => true,
'gender' => 'unisex',
'age_group' => 'adult',
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Malformed GTIN):
{
"message": "The gtin field format is invalid.",
"errors": {
"gtin": [
"The gtin field format is invalid."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a product's personalization fields
requires authentication
The fields a shopper fills in when adding the product to the cart, such as
an engraving text. personalization_fields replaces the whole list, in
order; send [] to remove them all. Up to 10 fields; names must be unique,
ignoring case. required defaults to false. Fields are matched by name, so
renaming a field replaces it with a new one. A store without personalization
fields enabled answers 422.
Works like Save in the product editor: a published product is republished
with the change before the response (424 when Webflow refuses it, 503 with
Retry-After when Webflow is rate limiting the site, 422 when the change
would leave the product unable to be published). Fires the product_updated
webhook when something changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/personalization-fields" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"personalization_fields\": [
{
\"name\": \"Engraving\",
\"placeholder\": \"Up to 20 letters\",
\"help\": \"Printed on the back.\",
\"required\": false
}
]
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/personalization-fields"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": "Printed on the back.",
"required": false
}
]
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/personalization-fields';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'personalization_fields' => [
['name' => 'Engraving', 'placeholder' => 'Up to 20 letters', 'help' => 'Printed on the back.', 'required' => false],
],
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Duplicate names):
{
"message": "Make sure that all personalization field names are unique.",
"errors": {
"personalization_fields": [
"Make sure that all personalization field names are unique."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a product's digital download
requires authentication
The file a buyer gets a link to after paying, by name and URL. Every variant
without a file of its own gets it, as the product editor's Digital Download
does; a variant with its own file (PUT /variants/{id}/download) keeps it.
Where the store does not offer per-variant files, every variant gets it. A bundle is
refused: its buyers get the files of its components.
Works like Save in the product editor: a published product is republished
with the change before the response (424 when Webflow refuses it, 503 with
Retry-After when Webflow is rate limiting the site, 422 when the change
would leave the product unable to be published). Fires the product_updated
webhook when something changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/download" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Pattern (PDF)\",
\"url\": \"https:\\/\\/files.example.com\\/pattern.pdf\"
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/download"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Pattern (PDF)",
"url": "https:\/\/files.example.com\/pattern.pdf"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/download';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Pattern (PDF)',
'url' => 'https://files.example.com/pattern.pdf',
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": {
"name": "Pattern (PDF)",
"url": "https://files.example.com/pattern.pdf"
},
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Not a link):
{
"message": "The url field must be a valid URL.",
"errors": {
"url": [
"The url field must be a valid URL."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Remove a product's digital download
requires authentication
Removes the product's file from every variant that carries it; a variant with a file of its own keeps that one.
Works like Save in the product editor, with the same responses as
PUT /products/{id}/download. Fires the product_updated webhook when
something changed.
Requires the write_products token scope.
Example request:
curl --request DELETE \
"https://api.cartgenie.com/public/v1/products/364/download" \
--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/download"
);
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/products/364/download';
$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 (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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a product's images
requires authentication
Replaces the main image, the extra images, or both, as the product
editor's image uploaders do. An image is {"id": …} to keep one the
product has — as read under images.main and images.extra — or
{"url": …} to add a new one, downloaded from a public HTTPS address (an
image of up to 4 MB: JPEG, PNG, BMP, GIF, SVG, WebP or AVIF). Each takes an
alt_text; left out, the image has none. Where the store does not have
image alt text turned on, responses leave alt_text out and sending it
returns 422; existing alt text is kept.
main null removes the main image. extra is the whole gallery in order,
up to 25: an extra image left out is deleted. Leave main or extra out to
keep those images as they are. The product's first variant shows the main image, as in the editor.
Works like Save in the product editor: a published product is republished
with the change before the response (424 when Webflow refuses it, 503 with
Retry-After when Webflow is rate limiting the site, 422 when the change
would leave the product unable to be published). If the republish fails, no
images are removed. Fires the product_updated webhook when something
changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/images" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"main\": {
\"id\": 901,
\"url\": \"https:\\/\\/cdn.example.com\\/tee.jpg\",
\"alt_text\": \"Black cotton tee\"
},
\"extra\": [
{
\"id\": 902,
\"url\": \"https:\\/\\/cdn.example.com\\/tee-back.jpg\",
\"alt_text\": \"Back of the tee\"
}
]
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/images"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"main": {
"id": 901,
"url": "https:\/\/cdn.example.com\/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https:\/\/cdn.example.com\/tee-back.jpg",
"alt_text": "Back of the tee"
}
]
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/images';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'main' => ['id' => 901, 'url' => 'https://cdn.example.com/tee.jpg', 'alt_text' => 'Black cotton tee'],
'extra' => [
['id' => 902, 'url' => 'https://cdn.example.com/tee-back.jpg', 'alt_text' => 'Back of the tee'],
],
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Image could not be downloaded):
{
"message": "The file at https://cdn.example.com/tee.jpg could not be downloaded: the server answered 404.",
"errors": {
"main.url": [
"The file at https://cdn.example.com/tee.jpg could not be downloaded: the server answered 404."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Add a product option
requires authentication
Adds an option set — up to three per product — and makes a variant for every new combination, as the product editor's options do. Each new variant copies the price, SKU and settings of the existing variant it extends; when there is none, it copies the product's first variant, without the SKU. A product has at most 100 combinations, 200 where the store's plan allows more.
swatch_type is button (default) or color; a color swatch needs a
color (#rrggbb) for every value. Image swatches are set in the dashboard.
Where the store does not have option swatches turned on, responses leave
swatch_type and color out, sending them returns 422, and options are
saved as buttons, as in the dashboard.
Works like Save in the product editor: a published product is republished
with its new variants before the response (424 when Webflow refuses it, 503
with Retry-After when Webflow is rate limiting the site, 422 when the change
would leave the product unable to be published). Fires the product_updated
webhook.
Requires the write_products token scope.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/products/364/options" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Size\",
\"swatch_type\": \"button\",
\"values\": [
{
\"value\": \"M\"
}
]
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/options"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Size",
"swatch_type": "button",
"values": [
{
"value": "M"
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/options';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Size',
'swatch_type' => 'button',
'values' => [
['value' => 'M'],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Too many combinations):
{
"message": "These values make 120 combinations; a product has at most 100.",
"errors": {
"values": [
"These values make 120 combinations; a product has at most 100."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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 product option
requires authentication
Renames the option set, changes its swatch type or its values. values
replaces the list in order: a value sent with its id keeps its variants
while its text changes, a value without an id is added with new
combinations, and a value left out is removed together with its variants.
Leave values out to keep the current values. An option with image swatches
reads back with swatch_type: image; leave swatch_type out or send
image to keep them, and each kept value keeps its image.
The option as read can be sent back as it is, id included.
Works like Save in the product editor, with the same responses as
POST /products/{id}/options. Fires the product_updated webhook when
something changed.
Requires the write_products token scope.
Example request:
curl --request PATCH \
"https://api.cartgenie.com/public/v1/products/364/options/31" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Size\",
\"swatch_type\": \"button\",
\"values\": [
{
\"id\": 88,
\"value\": \"Medium\"
}
],
\"id\": 31
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/options/31"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "Medium"
}
],
"id": 31
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/options/31';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Size',
'swatch_type' => 'button',
'values' => [
['id' => 88, 'value' => 'Medium'],
],
'id' => 31,
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Option of another product):
{
"message": "Resource not found."
}
Example response (422, Value of another option):
{
"message": "The value id must be one of the values of this option.",
"errors": {
"values.0.id": [
"The value id must be one of the values of this option."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 product option
requires authentication
Deletes an option set and rebuilds the variants from the options left. If
other options remain and the variants carry more than one of this option's
values, send keep_value_id to choose which value's variants to keep, with their prices,
SKUs and stock; the variants of the other values are deleted, as in the
product editor. Without that choice the call answers 422 listing the values
to choose from. Every other variant that matches no combination of the remaining
options is deleted too, including a variant with no options (options: []),
unless it is the product's default variant.
Works like Save in the product editor. Fires the product_updated webhook.
Requires the write_products token scope.
Example request:
curl --request DELETE \
"https://api.cartgenie.com/public/v1/products/364/options/31?keep_value_id=88" \
--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/options/31"
);
const params = {
"keep_value_id": "88",
};
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: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/options/31';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'keep_value_id' => '88',
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Value to keep not chosen):
{
"message": "Choose the value whose variants stay, as keep_value_id: 88 (M), 89 (L). The variants of the other values are deleted.",
"errors": {
"keep_value_id": [
"Choose the value whose variants stay, as keep_value_id: 88 (M), 89 (L). The variants of the other values are deleted."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Set a product's default variant
requires authentication
The variant the storefront preselects. null removes the choice, as
unstarring the variant in the product editor does. A disabled variant cannot
be the default. Works only where the store has default variants turned on;
otherwise the call returns 422 and products carry no default_variant_id.
Works like Save in the product editor. Fires the product_updated webhook
when the default changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/364/default-variant" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"variant_id\": 2302
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/364/default-variant"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"variant_id": 2302
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/364/default-variant';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'variant_id' => 2302,
],
]
);
$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,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2301,
"options": [
{
"id": 31,
"name": "Size",
"swatch_type": "button",
"values": [
{
"id": 88,
"value": "M",
"color": null
},
{
"id": 89,
"value": "L",
"color": null
}
]
}
],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"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",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": null,
"images": {
"main": {
"id": 903,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"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-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Disabled variant):
{
"message": "A disabled variant cannot be the default. Enable it first.",
"errors": {
"variant_id": [
"A disabled variant cannot be the default. Enable it first."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Set a bundle's components
requires authentication
Makes the product a bundle of components — or, for a bundle, replaces its
components — as the product editor does. Each component is a variant of
another product of the store that is not itself a bundle, is paid once (not
a subscription) and is published, scheduled or queued; a product that is a
component of a bundle cannot become one. A variant of the bundle that
overrides a component's quantity in the editor keeps its own quantity while
the component stays. A bundle's stock, weight, shipping and customs details
come from its components, and it is never fulfilled automatically, so a product
with automatic fulfillment must turn it off first.
Works like Save in the product editor: a published bundle is republished with
the change before the response, which needs every component on the live
site (424 when Webflow refuses it, 503 with Retry-After when Webflow is
rate limiting the site). Fires the product_updated webhook when something
changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/products/371/bundle" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"components\": [
{
\"variant_id\": 2301,
\"quantity\": 2
}
]
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/371/bundle"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"components": [
{
"variant_id": 2301,
"quantity": 2
}
]
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/371/bundle';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'components' => [
['variant_id' => 2301, 'quantity' => 2],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 371,
"name": "Starter kit",
"slug": "starter-kit",
"status": "published",
"published": true,
"price": 6900,
"formatted_price": "$69.00",
"stock": 40,
"available_stock": 40,
"track_inventory": true,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2380,
"options": [],
"bundle": {
"components": [
{
"variant_id": 2301,
"quantity": 2
},
{
"variant_id": 2310,
"quantity": 1
}
]
},
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"url": "https://store.example.com/product/starter-kit",
"variants": [
{
"id": 2380,
"product_id": 371,
"sku": "KIT-STARTER",
"title": null,
"published": true,
"price": 6900,
"formatted_price": "$69.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [],
"subscription": null,
"download": null,
"images": {
"main": null,
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 40,
"reserved_stock": 0,
"available_stock": 40,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
],
"created_at": "2026-01-14T09:00:00.000000Z",
"deleted_at": null,
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Store without bundles):
{
"message": "Product bundles are not enabled for this store.",
"errors": {
"request": [
"Product bundles are not enabled for this store."
]
}
}
Example response (422, Published product could not be republished as this bundle):
{
"message": "The product is published and could not be republished with this change: Product contains shippable variants but store has no shipping zones configured. Move it to draft first, or fix the store setup.",
"errors": {
"product": [
"The product is published and could not be republished with this change: Product contains shippable variants but store has no shipping zones configured. Move it to draft first, or fix the store setup."
]
}
}
Example response (422, Variant cannot be a component):
{
"message": "Variant 2310 cannot be a component: it must be a one-time variant of another product of this store that is not a bundle and is published, scheduled or queued.",
"errors": {
"components.1.variant_id": [
"Variant 2310 cannot be a component: it must be a one-time variant of another product of this store that is not a bundle and is published, scheduled or queued."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Turn a bundle back into a single product
requires authentication
Turns a bundle back into a single product, as switching the product type in
the editor does: the components are dropped from every variant, and the
product's own shipping and stock settings apply again. Fires the
product_updated webhook.
Requires the write_products token scope.
Example request:
curl --request DELETE \
"https://api.cartgenie.com/public/v1/products/371/bundle" \
--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/371/bundle"
);
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/products/371/bundle';
$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 (200):
{
"id": 371,
"name": "Starter kit",
"slug": "starter-kit",
"status": "published",
"published": true,
"price": 6900,
"formatted_price": "$69.00",
"stock": 40,
"available_stock": 40,
"track_inventory": true,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2380,
"options": [],
"bundle": null,
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"url": "https://store.example.com/product/starter-kit",
"variants": [
{
"id": 2380,
"product_id": 371,
"sku": "KIT-STARTER",
"title": null,
"published": true,
"price": 6900,
"formatted_price": "$69.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [],
"subscription": null,
"download": null,
"images": {
"main": null,
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 40,
"reserved_stock": 0,
"available_stock": 40,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
],
"created_at": "2026-01-14T09:00:00.000000Z",
"deleted_at": null,
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Not a bundle):
{
"message": "This product is not a bundle.",
"errors": {
"product": [
"This product is not a bundle."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Add or update a bundle component
requires authentication
Adds one variant to a bundle, or changes the quantity of one it already
holds; the other components stay. The same rules as
PUT /products/{id}/bundle apply, and the product must already be a bundle.
Fires the product_updated webhook when something changed.
Requires the write_products token scope.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/products/371/bundle/components" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"variant_id\": 2315,
\"quantity\": 1
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/products/371/bundle/components"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"variant_id": 2315,
"quantity": 1
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/products/371/bundle/components';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'variant_id' => 2315,
'quantity' => 1,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 371,
"name": "Starter kit",
"slug": "starter-kit",
"status": "published",
"published": true,
"price": 6900,
"formatted_price": "$69.00",
"stock": 40,
"available_stock": 40,
"track_inventory": true,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2380,
"options": [],
"bundle": {
"components": [
{
"variant_id": 2301,
"quantity": 2
},
{
"variant_id": 2315,
"quantity": 1
}
]
},
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"url": "https://store.example.com/product/starter-kit",
"variants": [
{
"id": 2380,
"product_id": 371,
"sku": "KIT-STARTER",
"title": null,
"published": true,
"price": 6900,
"formatted_price": "$69.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [],
"subscription": null,
"download": null,
"images": {
"main": null,
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 40,
"reserved_stock": 0,
"available_stock": 40,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
],
"created_at": "2026-01-14T09:00:00.000000Z",
"deleted_at": null,
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Not a bundle):
{
"message": "This product is not a bundle. Make it one with PUT /products/{id}/bundle.",
"errors": {
"product": [
"This product is not a bundle. Make it one with PUT /products/{id}/bundle."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Remove a bundle component
requires authentication
Takes one variant out of a bundle. A bundle keeps at least one component;
to turn it back into a single product use DELETE /products/{id}/bundle.
Fires the product_updated webhook.
Requires the write_products token scope.
Example request:
curl --request DELETE \
"https://api.cartgenie.com/public/v1/products/371/bundle/components/2310" \
--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/371/bundle/components/2310"
);
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/products/371/bundle/components/2310';
$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 (200):
{
"id": 371,
"name": "Starter kit",
"slug": "starter-kit",
"status": "published",
"published": true,
"price": 6900,
"formatted_price": "$69.00",
"stock": 40,
"available_stock": 40,
"track_inventory": true,
"description": "<p>Soft cotton tee.</p>",
"meta_description": "Soft cotton tee in five colors.",
"product_type": "physical",
"categories": {
"category_ids": [
7,
9
],
"primary_category_id": 7
},
"tax": {
"tax_class": "default",
"tax_class_id": null,
"tax_code_id": 41
},
"purchase_limits": {
"min_quantity_per_order": 1,
"max_quantity_per_order": 5
},
"pre_order": {
"pre_order": false,
"estimated_shipping_date": null
},
"feed": {
"brand": "Northwind",
"condition": "new",
"mpn": "NW-TEE-01",
"gtin": null,
"google_product_category_id": 212,
"include_variant_info_in_title_feed": true,
"gender": "unisex",
"age_group": "adult"
},
"personalization_fields": [
{
"name": "Engraving",
"placeholder": "Up to 20 letters",
"help": null,
"required": false
}
],
"download": null,
"default_variant_id": 2380,
"options": [],
"bundle": {
"components": [
{
"variant_id": 2301,
"quantity": 2
}
]
},
"shipping": {
"requires_shipping": true,
"auto_fulfill_order": false,
"hs_code": "6109.10",
"country_of_origin": "pt"
},
"custom_fields": {
"12": "Cotton",
"13": "https://cdn.example.com/media/care-guide.pdf"
},
"images": {
"main_image": "https://cdn.example.com/tee.jpg",
"main_image_large": "https://cdn.example.com/tee-large.jpg",
"main": {
"id": 901,
"url": "https://cdn.example.com/tee.jpg",
"alt_text": "Black cotton tee"
},
"extra": [
{
"id": 902,
"url": "https://cdn.example.com/tee-back.jpg",
"alt_text": null
}
]
},
"url": "https://store.example.com/product/starter-kit",
"variants": [
{
"id": 2380,
"product_id": 371,
"sku": "KIT-STARTER",
"title": null,
"published": true,
"price": 6900,
"formatted_price": "$69.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [],
"subscription": null,
"download": null,
"images": {
"main": null,
"extra": [],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 40,
"reserved_stock": 0,
"available_stock": 40,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
],
"created_at": "2026-01-14T09:00:00.000000Z",
"deleted_at": null,
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Product belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Last component):
{
"message": "A bundle needs at least one component. Remove the bundle with DELETE /products/{id}/bundle instead.",
"errors": {
"variant": [
"A bundle needs at least one component. Remove the bundle with DELETE /products/{id}/bundle instead."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 of the store's products, archived products excepted,
oldest update first. published says whether the variant has a
storefront copy. 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 quantity on hand, reserved_stock is held by checkouts in
progress, and available_stock is what a shopper can still buy.
A published bundle variant holds no stock of its own: stock,
available_stock and track_inventory are derived from its components. A
draft bundle's stock figures mean nothing until it is published.
compare_at_price and formatted_compare_at_price are null when not set.
Requires the read_products token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/variants?limit=50&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",
"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',
'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": 2302,
"product_id": 364,
"sku": "TEE-CLUB-M",
"title": "m",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": {
"interval": "month",
"interval_count": 1,
"term": "fixed_term",
"ends_after": 12,
"renewal_price": 3900,
"charge_shipping_on_renewals": true
},
"download": {
"name": "Pattern, size M (PDF)",
"url": "https://files.example.com/pattern-m.pdf"
},
"images": {
"main": {
"id": 911,
"url": "https://cdn.example.com/tee-m.jpg",
"alt_text": "Black tee, size M"
},
"extra": [
{
"id": 912,
"url": "https://cdn.example.com/tee-m-back.jpg",
"alt_text": null
}
],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
],
"meta": {
"per_page": 50,
"has_more": false,
"next_cursor": null
}
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_products scope required for this endpoint."
}
Example response (422, Malformed cursor):
{
"message": "The cursor is invalid.",
"errors": {
"cursor": [
"The cursor is invalid."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 variant
requires authentication
Changes the variant's price, compare-at price, SKU, weight, dimensions,
whether it is on sale (enabled) and its own description; only the fields
you send change. Prices are integers in the store currency's smallest unit.
Stock has its own endpoint (PUT /variants/{id}/inventory). A variant
without a description shows the product's.
enabled works only where the store has variant disabling turned on, and
description only where it has variant descriptions turned on; otherwise
sending the field returns 422 and responses leave it out. Where the store
uses default variants, the default
variant cannot be disabled — choose another default first. Otherwise,
disabling the default variant makes the first enabled variant the default,
as the variant editor does.
When the product is published it is republished with the change, Webflow
included, before the response, as the product editor does after a variant
save. When Webflow refuses the product nothing is saved (424); when Webflow
is rate limiting the site nothing is saved and the call answers 503 with
Retry-After. When the change would leave a published product unable to be
published, the call answers 422 and nothing is saved. A draft or queued
product keeps the change until it is next published.
Fires the product_updated webhook when a field changed.
Requires the write_products token scope.
Example request:
curl --request PATCH \
"https://api.cartgenie.com/public/v1/variants/2302" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"price\": 5400,
\"compare_at_price\": 5900,
\"sku\": \"TEE-BLK-M\",
\"weight\": 18000,
\"length\": 3000,
\"width\": 2000,
\"height\": 200,
\"enabled\": true,
\"description\": \"<p>Black, medium.<\\/p>\"
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/variants/2302"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"price": 5400,
"compare_at_price": 5900,
"sku": "TEE-BLK-M",
"weight": 18000,
"length": 3000,
"width": 2000,
"height": 200,
"enabled": true,
"description": "<p>Black, medium.<\/p>"
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/variants/2302';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'price' => 5400,
'compare_at_price' => 5900,
'sku' => 'TEE-BLK-M',
'weight' => 18000,
'length' => 3000,
'width' => 2000,
'height' => 200,
'enabled' => true,
'description' => '<p>Black, medium.</p>',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 2302,
"product_id": 364,
"sku": "TEE-BLK-M",
"title": "m",
"published": true,
"price": 5400,
"formatted_price": "$54.00",
"compare_at_price": 5900,
"formatted_compare_at_price": "$59.00",
"enabled": true,
"description": "<p>Black, medium.</p>",
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": {
"interval": "month",
"interval_count": 1,
"term": "fixed_term",
"ends_after": 12,
"renewal_price": 3900,
"charge_shipping_on_renewals": true
},
"download": {
"name": "Pattern, size M (PDF)",
"url": "https://files.example.com/pattern-m.pdf"
},
"images": {
"main": {
"id": 911,
"url": "https://cdn.example.com/tee-m.jpg",
"alt_text": "Black tee, size M"
},
"extra": [
{
"id": 912,
"url": "https://cdn.example.com/tee-m-back.jpg",
"alt_text": null
}
],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Variant belongs to another store, its product is archived, or it does not exist):
{
"message": "Resource not found."
}
Example response (422, Published product cannot be republished):
{
"message": "The variant's product is published and could not be republished with this change: Product contains shippable variants but store has no shipping zones configured. Move it to draft first, or fix the store setup.",
"errors": {
"product": [
"The variant's product is published and could not be republished with this change: Product contains shippable variants but store has no shipping zones configured. Move it to draft first, or fix the store setup."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a variant's subscription billing
requires authentication
Sells the variant as a subscription: the customer is charged every
interval_count × interval instead of once, as the Subscription billing
method in the variant editor does. For a product without options, use its
only variant. Replaces the whole subscription: a field left out takes its
default. A billing period longer than the limits below, set in the
dashboard, reads back unchanged, but you must shorten it before sending it.
Subscriptions already started keep the terms they started with.
term is renews_until_canceled (default) or fixed_term, which ends
after ends_after payments (2 to 1000). renewal_price sets a different
price for renewals than for the first payment; null, or a value equal to
the variant's price, charges the variant's price for every payment. It works
only where the store has renewal prices turned on; otherwise sending it
returns 422 and responses leave it out. charge_shipping_on_renewals
(default true) applies only to a variant that requires shipping.
Checkout takes the first payment, and Stripe charges the next one a billing period later. Stripe allows at most 730 days for that, so a billing period can be at most 730 days, 104 weeks, 23 months or 1 year.
A bundle component, a variant of a bundle that holds a subscription
component, and, on a product with options, the variant that carries none of
them (options: []) cannot be billed by subscription.
Works like Save in the variant editor: a published product is republished
with the change before the response (424 when Webflow refuses it, 503 with
Retry-After when Webflow is rate limiting the site, 422 when the change
would leave the product unable to be published). Fires the product_updated
webhook when something changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/variants/2302/subscription" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"interval\": \"month\",
\"interval_count\": 1,
\"term\": \"fixed_term\",
\"ends_after\": 12,
\"renewal_price\": 3900,
\"charge_shipping_on_renewals\": true
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/variants/2302/subscription"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"interval": "month",
"interval_count": 1,
"term": "fixed_term",
"ends_after": 12,
"renewal_price": 3900,
"charge_shipping_on_renewals": true
};
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/2302/subscription';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'interval' => 'month',
'interval_count' => 1,
'term' => 'fixed_term',
'ends_after' => 12,
'renewal_price' => 3900,
'charge_shipping_on_renewals' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 2302,
"product_id": 364,
"sku": "TEE-CLUB-M",
"title": "m",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": {
"interval": "month",
"interval_count": 1,
"term": "fixed_term",
"ends_after": 12,
"renewal_price": 3900,
"charge_shipping_on_renewals": true
},
"download": {
"name": "Pattern, size M (PDF)",
"url": "https://files.example.com/pattern-m.pdf"
},
"images": {
"main": {
"id": 911,
"url": "https://cdn.example.com/tee-m.jpg",
"alt_text": "Black tee, size M"
},
"extra": [
{
"id": 912,
"url": "https://cdn.example.com/tee-m-back.jpg",
"alt_text": null
}
],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Variant belongs to another store, its product is archived, or it does not exist):
{
"message": "Resource not found."
}
Example response (422, Bundle component):
{
"message": "This variant is a component of bundle(s): Gift Box. Remove it from those bundles before billing it by subscription.",
"errors": {
"variant": [
"This variant is a component of bundle(s): Gift Box. Remove it from those bundles before billing it by subscription."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Remove a variant's subscription billing
requires authentication
Turns the variant's subscription off, as choosing One-Time Payment in the variant editor does; its renewal price goes with it. Subscriptions already started keep renewing.
Works like Save in the variant editor, with the same 424 and 503 responses
as PATCH /variants/{id}. Fires the product_updated webhook
when something changed.
Requires the write_products token scope.
Example request:
curl --request DELETE \
"https://api.cartgenie.com/public/v1/variants/2302/subscription" \
--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/2302/subscription"
);
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/variants/2302/subscription';
$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 (200):
{
"id": 2302,
"product_id": 364,
"sku": "TEE-CLUB-M",
"title": "m",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": null,
"download": {
"name": "Pattern, size M (PDF)",
"url": "https://files.example.com/pattern-m.pdf"
},
"images": {
"main": {
"id": 911,
"url": "https://cdn.example.com/tee-m.jpg",
"alt_text": "Black tee, size M"
},
"extra": [
{
"id": 912,
"url": "https://cdn.example.com/tee-m-back.jpg",
"alt_text": null
}
],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Variant belongs to another store, its product is archived, or it does not exist):
{
"message": "Resource not found."
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a variant's digital download
requires authentication
Gives the variant a file of its own in place of the product's, as the
variant editor's Digital Download does. The product's file is set with
PUT /products/{id}/download; the variant that holds it (the product's
lowest-id variant, the only one of a product without options) refuses this
call. Works only where the store has per-variant digital downloads turned
on; otherwise the call returns 422 and variants carry no download. A
bundle variant is refused: bundle buyers get the files of its components.
Works like Save in the variant editor, with the same 422, 424 and 503
responses as PATCH /variants/{id}. Fires the product_updated webhook
when something changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/variants/2302/download" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Pattern, size M (PDF)\",
\"url\": \"https:\\/\\/files.example.com\\/pattern-m.pdf\"
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/variants/2302/download"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Pattern, size M (PDF)",
"url": "https:\/\/files.example.com\/pattern-m.pdf"
};
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/2302/download';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Pattern, size M (PDF)',
'url' => 'https://files.example.com/pattern-m.pdf',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 2302,
"product_id": 364,
"sku": "TEE-CLUB-M",
"title": "m",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": {
"interval": "month",
"interval_count": 1,
"term": "fixed_term",
"ends_after": 12,
"renewal_price": 3900,
"charge_shipping_on_renewals": true
},
"download": {
"name": "Pattern, size M (PDF)",
"url": "https://files.example.com/pattern-m.pdf"
},
"images": {
"main": {
"id": 911,
"url": "https://cdn.example.com/tee-m.jpg",
"alt_text": "Black tee, size M"
},
"extra": [
{
"id": 912,
"url": "https://cdn.example.com/tee-m-back.jpg",
"alt_text": null
}
],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Variant belongs to another store, its product is archived, or it does not exist):
{
"message": "Resource not found."
}
Example response (422, Variant holding the product's file):
{
"message": "This variant holds the product's digital download. Use PUT /products/364/download.",
"errors": {
"variant": [
"This variant holds the product's digital download. Use PUT /products/364/download."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Remove a variant's digital download
requires authentication
Returns the variant to the product's file, or to none when the product has none, as clearing the file in the variant editor does. Works only where the store has per-variant digital downloads turned on; otherwise the call returns 422.
Works like Save in the variant editor, with the same 422, 424 and 503
responses as PATCH /variants/{id}. Fires the product_updated webhook
when something changed.
Requires the write_products token scope.
Example request:
curl --request DELETE \
"https://api.cartgenie.com/public/v1/variants/2302/download" \
--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/2302/download"
);
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/variants/2302/download';
$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 (200):
{
"id": 2302,
"product_id": 364,
"sku": "TEE-CLUB-M",
"title": "m",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": {
"interval": "month",
"interval_count": 1,
"term": "fixed_term",
"ends_after": 12,
"renewal_price": 3900,
"charge_shipping_on_renewals": true
},
"download": {
"name": "Pattern, size M (PDF)",
"url": "https://files.example.com/pattern-m.pdf"
},
"images": {
"main": {
"id": 911,
"url": "https://cdn.example.com/tee-m.jpg",
"alt_text": "Black tee, size M"
},
"extra": [
{
"id": 912,
"url": "https://cdn.example.com/tee-m-back.jpg",
"alt_text": null
}
],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Variant belongs to another store, its product is archived, or it does not exist):
{
"message": "Resource not found."
}
Example response (422, Store without per-variant downloads):
{
"message": "Variant digital downloads are not enabled for this store.",
"errors": {
"request": [
"Variant digital downloads are not enabled for this store."
]
}
}
Example response (422, Variant holding the product's file):
{
"message": "This variant holds the product's digital download. Use PUT /products/364/download.",
"errors": {
"variant": [
"This variant holds the product's digital download. Use PUT /products/364/download."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a variant's images
requires authentication
Replaces the variant's own main image, its extra images, or both, as the
variant editor's image uploaders do, in the shape read under the variant's
images: {"id": …} keeps an image, {"url": …} adds one downloaded from a
public HTTPS address (up to 4 MB: JPEG, PNG, BMP, GIF, SVG, WebP or AVIF),
each with an optional alt_text. main null removes the main image;
extra is the whole gallery in order, up to 25. A variant without images
of its own shows the product's.
hide_main_image and hide_extra_images hide the variant's main or extra
images altogether, the product's included; they work only where the store
has option value images turned on, as in the variant editor; otherwise
sending them returns 422 and responses leave them out. alt_text works as
in PUT /products/{id}/images.
The product's first variant shows the product's main image, which is set
with PUT /products/{id}/images; for that variant, leave main out or send
it exactly as read.
Works like Save in the variant editor, with the same responses as
PUT /products/{id}/images. Fires the product_updated webhook when
something changed.
Requires the write_products token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/variants/2302/images" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"main\": {
\"id\": 911,
\"url\": \"https:\\/\\/cdn.example.com\\/tee-m.jpg\",
\"alt_text\": \"Black tee, size M\"
},
\"extra\": [
{
\"id\": 912,
\"url\": \"https:\\/\\/cdn.example.com\\/tee-m-back.jpg\",
\"alt_text\": \"Back of the tee\"
}
],
\"hide_main_image\": false,
\"hide_extra_images\": false
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/variants/2302/images"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"main": {
"id": 911,
"url": "https:\/\/cdn.example.com\/tee-m.jpg",
"alt_text": "Black tee, size M"
},
"extra": [
{
"id": 912,
"url": "https:\/\/cdn.example.com\/tee-m-back.jpg",
"alt_text": "Back of the tee"
}
],
"hide_main_image": false,
"hide_extra_images": false
};
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/2302/images';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'main' => ['id' => 911, 'url' => 'https://cdn.example.com/tee-m.jpg', 'alt_text' => 'Black tee, size M'],
'extra' => [
['id' => 912, 'url' => 'https://cdn.example.com/tee-m-back.jpg', 'alt_text' => 'Back of the tee'],
],
'hide_main_image' => false,
'hide_extra_images' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 2302,
"product_id": 364,
"sku": "TEE-CLUB-M",
"title": "m",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": {
"interval": "month",
"interval_count": 1,
"term": "fixed_term",
"ends_after": 12,
"renewal_price": 3900,
"charge_shipping_on_renewals": true
},
"download": {
"name": "Pattern, size M (PDF)",
"url": "https://files.example.com/pattern-m.pdf"
},
"images": {
"main": {
"id": 911,
"url": "https://cdn.example.com/tee-m.jpg",
"alt_text": "Black tee, size M"
},
"extra": [
{
"id": 912,
"url": "https://cdn.example.com/tee-m-back.jpg",
"alt_text": null
}
],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_products scope required for this endpoint."
}
Example response (404, Variant belongs to another store, its product is archived, or it does not exist):
{
"message": "Resource not found."
}
Example response (422, Image could not be downloaded):
{
"message": "The file at https://cdn.example.com/tee-m.jpg is a text/html file, which this field does not take.",
"errors": {
"main.url": [
"The file at https://cdn.example.com/tee-m.jpg is a text/html file, which this field does not take."
]
}
}
Example response (424, Webflow rejected the product):
{
"message": "Webflow rejected the product: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Set a variant's 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 stock 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 stock, not available_stock, so checkouts
holding reserved stock do not cause conflicts.
Requires the write_inventory token scope.
Example request:
curl --request PUT \
"https://api.cartgenie.com/public/v1/variants/2302/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/2302/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/2302/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": 2302,
"product_id": 364,
"sku": "TEE-CLUB-M",
"title": "m",
"published": true,
"price": 4900,
"formatted_price": "$49.00",
"compare_at_price": null,
"formatted_compare_at_price": null,
"enabled": true,
"description": null,
"options": [
{
"option_id": 31,
"option": "Size",
"value_id": 88,
"value": "M"
}
],
"subscription": {
"interval": "month",
"interval_count": 1,
"term": "fixed_term",
"ends_after": 12,
"renewal_price": 3900,
"charge_shipping_on_renewals": true
},
"download": {
"name": "Pattern, size M (PDF)",
"url": "https://files.example.com/pattern-m.pdf"
},
"images": {
"main": {
"id": 911,
"url": "https://cdn.example.com/tee-m.jpg",
"alt_text": "Black tee, size M"
},
"extra": [
{
"id": 912,
"url": "https://cdn.example.com/tee-m-back.jpg",
"alt_text": null
}
],
"hide_main_image": false,
"hide_extra_images": false
},
"stock": 120,
"reserved_stock": 2,
"available_stock": 118,
"track_inventory": true,
"requires_shipping": true,
"dimensions": {
"height": 200,
"length": 3000,
"width": 2000,
"weight": 18000
},
"created_at": "2026-01-14T09:00:00.000000Z",
"updated_at": "2026-08-14T10:05:12.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_inventory scope required for this endpoint."
}
Example response (404, Variant belongs to another store or does not exist):
{
"message": "Resource not found."
}
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.",
"errors": {
"variant": [
"Inventory tracking is not enabled for this variant."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 canceled order appears as
a status change rather than a deletion.
Each entry is the same object GET /orders/{orderName} returns, including the
custom checkout fields the store marks private.
Requires the read_orders token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/orders?limit=50&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",
"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',
'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",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"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"
}
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_orders scope required for this endpoint."
}
Example response (422, Malformed cursor):
{
"message": "The cursor is invalid.",
"errors": {
"cursor": [
"The cursor is invalid."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 identified by their name, not by an internal 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 depend on what the order contains. 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.
shipment.address and billing carry name, email, phone, address_line_one, address_line_two,
city, postal_code, state, country, region, region_name and
validation_result, with one state field for every country: a US or
Canadian state as its code (CA), free text elsewhere. region is the
region id (us-ca) and region_name its name (California), both null
where the address names no region the store knows.
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; an offline payment carries its own reference from
the start. billing and payment.label are both null only
on an order with no billing details recorded.
details.custom_fields carries every custom checkout field the shopper
filled in, including the ones the store marks private — marking a field
private hides it from the shopper, not from this API. When a private field
has the same name as a public one, the private value is returned. The map is always an object, empty when the
order carries none.
Requires the read_orders token scope.
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",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"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 (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_orders scope required for this endpoint."
}
Example response (404, Unknown order):
{
"message": "Resource not found."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 an order's 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.
Fires the order_updated webhook and moves the order's updated_at on every
call, also when the values sent are the ones already stored.
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.
Requires the write_orders token scope.
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",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"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 (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_orders scope required for this endpoint."
}
Example response (404, Order belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (422, Nothing to ship):
{
"message": "Order has no shippable items.",
"errors": {
"order": [
"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."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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; items cannot be fulfilled one by
one through this endpoint. As in the dashboard, a refunded order can still be
fulfilled. Fires the order_updated and order_fulfillment_updated webhooks.
Each fulfillments[].items[].order_item_id identifies the order line it
covers; it is not repeated in items[], which carries no id. A line covering
a bundle also carries components[], one entry per bundled variant with its
name and quantity; their live_product_variant_id identifies the
component within the order and is not a variant id from GET /variants.
Requires the write_orders token scope.
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",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"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",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"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 (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_orders scope required for this endpoint."
}
Example response (404, Order belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (409, Order is in a status that cannot be fulfilled):
{
"message": "Order cannot be fulfilled from status canceled."
}
Example response (422, Tracking link is not a URL):
{
"message": "The tracking url field must be a valid URL.",
"errors": {
"tracking_url": [
"The tracking url field must be a valid URL."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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. Fires the order_updated and
order_fulfillment_updated webhooks.
Requires the write_orders token scope.
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",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"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 (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_orders scope required for this endpoint."
}
Example response (404, Order belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (409, Order was never fulfilled):
{
"message": "Order cannot be unfulfilled from status unfulfilled."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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, fires the order_updated webhook
and sends the cancellation email, as canceling in the dashboard does.
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.
Requires the write_orders token scope.
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",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"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 (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_orders scope required for this endpoint."
}
Example response (404, Order belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (409, Order already reached a final status):
{
"message": "Order cannot be canceled from status canceled."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Mark an order as paid
requires authentication
Records the payment for an order placed with an offline payment method, the
same as Mark as paid in the dashboard: the order moves to unfulfilled,
stock is taken, the order is sent to your shipping and tax providers, and,
when send_notification is true, the customer gets the payment confirmation
email.
Only orders paid offline that are still unpaid can be marked paid. Fires the
order_payment_updated webhook.
When the order was marked unpaid earlier and the stock returned then has since sold, the call answers 409 and the order stays unpaid.
Requires the write_orders token scope.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/orders/1002/mark-paid" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"send_notification\": true
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/orders/1002/mark-paid"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"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/mark-paid';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'send_notification' => true,
],
]
);
$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",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"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 (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_orders scope required for this endpoint."
}
Example response (404, Order belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (409, Order was not paid offline):
{
"message": "Only orders paid offline can be marked paid."
}
Example response (409, Order is already paid or canceled):
{
"message": "Order cannot be marked paid from payment status paid and status unfulfilled."
}
Example response (409, Stock returned when the order was marked unpaid has since sold):
{
"message": "Not enough stock to mark the order paid: T-shirt: Red needs 2, 0 available."
}
Example response (422, Not a boolean):
{
"message": "The send notification field must be true or false.",
"errors": {
"send_notification": [
"The send notification field must be true or false."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Mark an order as unpaid
requires authentication
Reverses Mark as paid for an order placed with an offline payment method:
payment status goes back to unpaid and the order to the status the offline
payment method places new orders in (pending or unfulfilled), its stock is
returned to inventory, it no longer counts toward your order quota, and its
transaction is removed from your tax provider. Emails already sent are not
recalled. Marking the order paid again runs the payment steps afresh.
Only paid, unfulfilled orders paid offline and without refunds can be marked
unpaid. Fires the order_payment_updated webhook.
Requires the write_orders token scope.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/orders/1002/mark-unpaid" \
--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/mark-unpaid"
);
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/mark-unpaid';
$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": "pending",
"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",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"validation_result": null
},
"payment": {
"label": "Bank transfer",
"status": "unpaid",
"method": "offline",
"transaction_id": null
},
"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 (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_orders scope required for this endpoint."
}
Example response (404, Order belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (409, Order was not paid offline):
{
"message": "Only orders paid offline can be marked unpaid."
}
Example response (409, Order is fulfilled, refunded or not paid):
{
"message": "Order cannot be marked unpaid from payment status paid and status fulfilled."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
List an order's refunds
requires authentication
Every refund recorded on the order, oldest first, including failed ones —
unlike the order's refunds field, which lists them only once the order is
refunded.
Requires the read_orders token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/orders/1002/refunds" \
--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/refunds"
);
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/refunds';
$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):
{
"data": [
{
"id": 88,
"amount": 1500,
"formatted_amount": "$15.00",
"quantity": null,
"status": "completed",
"reason": "Damaged in transit",
"created_at": "2026-10-06T09:00:00.000000Z"
}
]
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_orders scope required for this endpoint."
}
Example response (404, Order belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Refund an order
requires authentication
Refunds any paid order, the same as Issue Refund in the dashboard. Leave
amount out to refund everything not yet refunded, or send part of it;
further partial refunds are allowed until the total is reached. Send an
Idempotency-Key header to make a retry safe: a repeat with the same key
within a day gets the first answer back (Idempotency-Replay: true) instead
of a second refund, including a 424 whose outcome is unknown; the same key
with a different body is a 422. Only a refund that went through, or whose
outcome is unknown, is remembered; any other refusal (409, 422, or a 424 the
gateway refused) can be retried with the same key. The order's
payment.status becomes partially_refunded or refunded, and the
refund_issued webhook fires.
What happens to the money depends on how the order was paid:
- Stripe: the amount goes back to the customer's card.
- PayPal: refund the order in PayPal, as in the dashboard; CartGenie records the refund when PayPal reports it. The call answers 409.
- An offline payment method: CartGenie cannot move the money. The refund is recorded so the order, reports and webhooks reflect it — return the money to the customer yourself.
When the gateway turns the refund down the call answers 424 and the refund
is recorded as failed.
restock: true returns the order's items to inventory with a full refund,
where the store has returning items to inventory turned on; it is refused with 422 on a partial
refund or when the order has no tracked items left to return. Items go back
after the money does, so if that step fails the refund still stands and
restocked says whether the items are back — when it is false, return
them in the dashboard. A refund does not cancel the order or its
subscriptions.
Requires the write_refunds token scope.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/orders/1002/refund" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Idempotency-Key: zap-run-42" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"amount\": 1500,
\"restock\": false,
\"reason\": \"Damaged in transit\"
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/orders/1002/refund"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Idempotency-Key": "zap-run-42",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"amount": 1500,
"restock": false,
"reason": "Damaged in transit"
};
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/refund';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Idempotency-Key' => 'zap-run-42',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'amount' => 1500,
'restock' => false,
'reason' => 'Damaged in transit',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"id": 88,
"amount": 1500,
"formatted_amount": "$15.00",
"quantity": null,
"status": "completed",
"reason": "Damaged in transit",
"created_at": "2026-10-06T09:00:00.000000Z",
"restocked": false
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_refunds scope required for this endpoint."
}
Example response (404, Order belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (409, Order already refunded):
{
"message": "Order cannot be refunded from payment status refunded."
}
Example response (409, Nothing left to refund):
{
"message": "Nothing is left to refund on this order."
}
Example response (409, Same Idempotency-Key still in progress):
{
"message": "A request with this Idempotency-Key is still in progress; retry once it has answered."
}
Example response (409, Paid with PayPal):
{
"message": "Refund PayPal orders in PayPal; the refund is recorded here when PayPal reports it."
}
Example response (409, Payment method removed from the store):
{
"message": "The store no longer has the order's payment method set up, so the refund cannot be recorded."
}
Example response (422, More than is left):
{
"message": "The amount may not be greater than 1500.",
"errors": {
"amount": [
"The amount may not be greater than 1500."
]
}
}
Example response (422, Store does not return items on refund):
{
"message": "Returning items to inventory is not enabled for this store.",
"errors": {
"restock": [
"Returning items to inventory is not enabled for this store."
]
}
}
Example response (422, Nothing to return to inventory):
{
"message": "The order has no tracked items left to return to inventory; leave restock out.",
"errors": {
"restock": [
"The order has no tracked items left to return to inventory; leave restock out."
]
}
}
Example response (422, Idempotency key reused with a different request):
{
"message": "This key was used with a different request.",
"errors": {
"Idempotency-Key": [
"This key was used with a different request."
]
}
}
Example response (422, Restock with a partial refund):
{
"message": "Items go back to inventory only with a full refund; leave restock out.",
"errors": {
"restock": [
"Items go back to inventory only with a full refund; leave restock out."
]
}
}
Example response (424, Gateway refused the refund):
{
"message": "The payment gateway refused the refund."
}
Example response (424, Refund could not be completed):
{
"message": "The refund could not be completed, and the money may have moved. Check the payment gateway before retrying."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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
Get sample deliveries for an event
requires authentication
Returns delivery bodies for one event in exactly the shape a live delivery carries, so you can map fields before the first real event arrives.
new_order, order_updated, order_fulfillment_updated,
order_payment_updated and refund_issued return up to 3 of the store's
latest matching orders — orders with a fulfillment for
order_fulfillment_updated, orders placed with an offline payment method
for order_payment_updated, orders with a completed refund for refund_issued.
When the store has none, and for every other event, the
response holds one representative example instead of a real record.
Deliveries keep a format of their own, which differs from this API's
responses for the same record: product and category status is a display
label (Draft, Sync Error) rather than the API's value; product,
category, customer and discount timestamps carry a +00:00 offset while
order timestamps carry Z; and addresses carry state as a state name or
a region id (us-ca), next to region and region_name, where
the API answers with the state code (CA). Map fields from these samples,
not from the API's responses.
The token needs the read scope that matches the event: read_orders for
order, refund, fulfillment and abandoned-cart events, read_customers,
read_subscriptions, read_discounts, read_categories, or
read_products for product and inventory events. Its owner needs the
matching view permission in the store.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/webhooks/sample?event=new_category" \
--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/sample"
);
const params = {
"event": "new_category",
};
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/sample';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'event' => 'new_category',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"type": "new_category",
"payload": {
"name": "Mugs",
"slug": "mugs",
"status": "Published",
"published_at": "2025-05-28T11:00:00+00:00",
"created_at": "2025-05-25T10:00:00+00:00",
"updated_at": "2025-05-28T11:00:00+00:00",
"description": null,
"meta_description": null
}
}
]
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token lacks the scope for this event):
{
"message": "This token does not have the read_categories scope required for the new_category event."
}
Example response (422, Unknown event):
{
"message": "The selected event is invalid.",
"errors": {
"event": [
"The selected event is invalid."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
List webhook subscriptions
requires authentication
Requires the read_webhooks token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/webhooks?limit=50" \
--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",
};
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',
],
]
);
$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
}
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_webhooks scope required for this endpoint."
}
Example response (422, Malformed cursor):
{
"message": "The cursor is invalid.",
"errors": {
"cursor": [
"The cursor is invalid."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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
Requires the read_webhooks token scope.
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 (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_webhooks scope required for this endpoint."
}
Example response (404, Subscription belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 (the order was
fulfilled, partially fulfilled or unfulfilled; order_updated fires as
well), 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 and order_payment_updated (an order paid with an
offline payment method was marked paid or unpaid).
new_product and product_updated carry the product's id, name, slug,
status, price, hs_code, country_of_origin, dates and url, not its
variants or settings; read GET /products/{id} for the whole product.
Requires the write_webhooks token scope.
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": "Feeds our ERP",
"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 (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_webhooks scope required for this endpoint."
}
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."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Requires the write_webhooks token scope.
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": "Feeds our ERP",
"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 (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_webhooks scope required for this endpoint."
}
Example response (404, Subscription belongs to another store or does not exist):
{
"message": "Resource not found."
}
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."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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
Requires the write_webhooks token scope.
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
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_webhooks scope required for this endpoint."
}
Example response (404, Subscription belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 webhook 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. The test is delivered even
while the subscription is paused (enabled: false).
Requires the write_webhooks token scope.
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 (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_webhooks scope required for this endpoint."
}
Example response (404, Subscription belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (422, Subscription does not listen for that event):
{
"message": "Event is not configured for this webhook.",
"errors": {
"event": [
"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.",
"errors": {
"url": [
"Webhook destination must be a publicly reachable HTTPS URL."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Categories
List categories
requires authentication
Archived categories are not listed. Pass slug or name to find one
category; name matches case-insensitively. status is draft,
queued, published or scheduled, or sync_error after Webflow refused
the category, or imported for a category imported from Webflow that was
never saved.
The list leaves out product_ids, which can hold every product of the
store; read them from GET /categories/{category}.
Requires the read_categories token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/categories?slug=summer-collection&name=Summer+collection&limit=50" \
--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/categories"
);
const params = {
"slug": "summer-collection",
"name": "Summer collection",
"limit": "50",
};
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/categories';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'slug' => 'summer-collection',
'name' => 'Summer collection',
'limit' => '50',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 7,
"name": "Summer collection",
"slug": "summer-collection",
"description": "Light fabrics for hot days.",
"meta_description": "Summer clothing",
"status": "published",
"image_url": "https://cdn.example.com/categories/7/summer.jpg",
"custom_fields": {
"21": "Light fabrics for hot days"
},
"published_at": "2026-06-01T09:00:00.000000Z",
"created_at": "2026-05-20T09:00:00.000000Z",
"updated_at": "2026-06-01T09:00:00.000000Z"
}
],
"meta": {
"per_page": 50,
"has_more": false,
"next_cursor": null
}
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_categories scope required for this endpoint."
}
Example response (422, Malformed cursor):
{
"message": "The cursor is invalid.",
"errors": {
"cursor": [
"The cursor is invalid."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 category
requires authentication
Requires the read_categories token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/categories/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/categories/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/categories/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": "Summer collection",
"slug": "summer-collection",
"description": "Light fabrics for hot days.",
"meta_description": "Summer clothing",
"status": "published",
"product_ids": [
12,
15
],
"image_url": "https://cdn.example.com/categories/7/summer.jpg",
"custom_fields": {
"21": "Light fabrics for hot days"
},
"published_at": "2026-06-01T09:00:00.000000Z",
"created_at": "2026-05-20T09:00:00.000000Z",
"updated_at": "2026-06-01T09:00:00.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_categories scope required for this endpoint."
}
Example response (404, Category belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 category
requires authentication
Saves the category and applies status the way the dashboard's save
buttons do: draft (the default) keeps it off the live site, queued
stages it for the next Webflow site publish, and published publishes it
now. Stores without Webflow treat queued as published. On a Webflow
store the category is written to the Webflow categories collection before
the response returns. custom_fields takes the store's category custom
fields (GET /categories/custom-fields), which a category also reads back;
images are not set through the API.
Fires the new_category webhook.
Requires the write_categories token scope.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/categories" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"custom_fields\": {
\"21\": \"Light fabrics for hot days\"
},
\"name\": \"Summer collection\",
\"slug\": \"summer-collection\",
\"description\": \"Light fabrics for hot days.\",
\"meta_description\": \"Summer clothing\",
\"status\": \"published\",
\"product_ids\": [
12,
15
]
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/categories"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"custom_fields": {
"21": "Light fabrics for hot days"
},
"name": "Summer collection",
"slug": "summer-collection",
"description": "Light fabrics for hot days.",
"meta_description": "Summer clothing",
"status": "published",
"product_ids": [
12,
15
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/categories';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'custom_fields' => [21 => 'Light fabrics for hot days'],
'name' => 'Summer collection',
'slug' => 'summer-collection',
'description' => 'Light fabrics for hot days.',
'meta_description' => 'Summer clothing',
'status' => 'published',
'product_ids' => [12, 15],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"id": 7,
"name": "Summer collection",
"slug": "summer-collection",
"description": "Light fabrics for hot days.",
"meta_description": "Summer clothing",
"status": "published",
"product_ids": [
12,
15
],
"image_url": "https://cdn.example.com/categories/7/summer.jpg",
"custom_fields": {
"21": "Light fabrics for hot days"
},
"published_at": "2026-06-01T09:00:00.000000Z",
"created_at": "2026-06-01T09:00:00.000000Z",
"updated_at": "2026-06-01T09:00:00.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_categories scope required for this endpoint."
}
Example response (422, Slug already used):
{
"message": "The slug has already been taken.",
"errors": {
"slug": [
"The slug has already been taken."
]
}
}
Example response (424, Webflow rejected the category):
{
"message": "Webflow rejected the category: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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 category
requires authentication
Only the fields you send change; product_ids replaces the whole list. A
product added without a primary category gets this one as its primary
category, and a product removed from its primary category falls back to
another of its categories, as in the product editor.
Without status, the category keeps its current status and is saved again
with it, so a published category is updated in Webflow too. A scheduled,
imported or sync_error category, or a published or queued one on a store
whose Webflow categories collection is disconnected, is saved in CartGenie
only. A request that changes nothing leaves the category, and its Webflow
item, untouched.
Fires the category_updated webhook once when anything changed — a field,
the products or the status.
Requires the write_categories token scope.
Example request:
curl --request PATCH \
"https://api.cartgenie.com/public/v1/categories/7" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"custom_fields\": {
\"21\": \"Light fabrics for hot days\"
},
\"name\": \"Summer collection\",
\"slug\": \"summer-collection\",
\"description\": \"Light fabrics for hot days.\",
\"meta_description\": \"Summer clothing\",
\"status\": \"published\",
\"product_ids\": [
12,
15
]
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/categories/7"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"custom_fields": {
"21": "Light fabrics for hot days"
},
"name": "Summer collection",
"slug": "summer-collection",
"description": "Light fabrics for hot days.",
"meta_description": "Summer clothing",
"status": "published",
"product_ids": [
12,
15
]
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/categories/7';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'custom_fields' => [21 => 'Light fabrics for hot days'],
'name' => 'Summer collection',
'slug' => 'summer-collection',
'description' => 'Light fabrics for hot days.',
'meta_description' => 'Summer clothing',
'status' => 'published',
'product_ids' => [12, 15],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 7,
"name": "Summer collection",
"slug": "summer-collection",
"description": "Light fabrics for hot days.",
"meta_description": "Summer clothing",
"status": "published",
"product_ids": [
12,
15
],
"image_url": "https://cdn.example.com/categories/7/summer.jpg",
"custom_fields": {
"21": "Light fabrics for hot days"
},
"published_at": "2026-06-01T09:00:00.000000Z",
"created_at": "2026-05-20T09:00:00.000000Z",
"updated_at": "2026-06-01T09:00:00.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_categories scope required for this endpoint."
}
Example response (404, Category belongs to another store, is archived or does not exist):
{
"message": "Resource not found."
}
Example response (422, Slug already used):
{
"message": "The slug has already been taken.",
"errors": {
"slug": [
"The slug has already been taken."
]
}
}
Example response (424, Webflow rejected the category):
{
"message": "Webflow rejected the category: Validation Failure"
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
Example response (503, Webflow rate limit):
{
"message": "Webflow is rate limiting this site. Nothing was saved; retry in a minute."
}
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.
Customers
List customers
requires authentication
Returns the store's customers, oldest update first. A customer is created
the first time someone checks out with a new email, or with
POST /customers. Pass
updated_since for an incremental feed; email finds one customer by
address, case-insensitively, and search matches part of a name or email.
Requires the read_customers token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/customers?email=jane%40example.com&search=jane&updated_since=2026-08-01T00%3A00%3A00Z&limit=50" \
--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/customers"
);
const params = {
"email": "[email protected]",
"search": "jane",
"updated_since": "2026-08-01T00:00:00Z",
"limit": "50",
};
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/customers';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'email' => '[email protected]',
'search' => 'jane',
'updated_since' => '2026-08-01T00:00:00Z',
'limit' => '50',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 312,
"name": "Jane Doe",
"email": "[email protected]",
"phone": "+1 555 0100",
"billing_address": {
"name": "Jane Doe",
"phone": "+1 555 0100",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us"
},
"shipping_address": null,
"created_at": "2026-05-20T09:00:00.000000Z",
"updated_at": "2026-05-20T09:00:00.000000Z"
}
],
"meta": {
"per_page": 50,
"has_more": false,
"next_cursor": null
}
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_customers scope required for this endpoint."
}
Example response (422, Malformed cursor):
{
"message": "The cursor is invalid.",
"errors": {
"cursor": [
"The cursor is invalid."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 customer
requires authentication
Requires the read_customers token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/customers/312" \
--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/customers/312"
);
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/customers/312';
$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": 312,
"name": "Jane Doe",
"email": "[email protected]",
"phone": "+1 555 0100",
"billing_address": null,
"shipping_address": null,
"created_at": "2026-05-20T09:00:00.000000Z",
"updated_at": "2026-05-20T09:00:00.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_customers scope required for this endpoint."
}
Example response (404, Customer belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
List a customer's orders
requires authentication
The customer's orders, with the same shape, sort order and updated_since
filter as GET /orders. Needs read_orders as well as read_customers.
Requires the read_customers and read_orders token scopes.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/customers/312/orders?limit=50&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/customers/312/orders"
);
const params = {
"limit": "50",
"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/customers/312/orders';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'limit' => '50',
'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",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"validation_result": null
}
},
"fulfillments": [],
"billing": {
"name": "John Doe",
"email": "[email protected]",
"phone": "+14155550123",
"address_line_one": "1 Market St",
"address_line_two": null,
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us",
"region": "us-ca",
"region_name": "California",
"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"
}
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_customers and read_orders scopes required for this endpoint."
}
Example response (404, Customer belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (422, Malformed cursor):
{
"message": "The cursor is invalid.",
"errors": {
"cursor": [
"The cursor is invalid."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 customer
requires authentication
Adds a customer before they have ordered, for example from a CRM. Their
first checkout with the same email attaches to this customer. Emails are
unique per store and stored lowercase. No webhook is sent: new_customer
still fires when the customer's first order is paid.
An address is read and written with the same fields, so an address taken
from a response can be sent back as it is. It needs country, a two-letter
code such as us. state is one field for every country: in the United
States and Canada it must be one of the country's states, sent as its code
(CA), region id (us-ca) or name (California), and always reads back
as the code; elsewhere it is free text, kept as sent. An address field not
listed below is refused with 422.
Requires the write_customers token scope.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/customers" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Jane Doe\",
\"email\": \"[email protected]\",
\"phone\": \"+1 555 0100\",
\"billing_address\": {
\"name\": \"Jane Doe\",
\"phone\": \"+1 555 0100\",
\"address_line_one\": \"1 Market St\",
\"address_line_two\": \"Suite 4\",
\"city\": \"San Francisco\",
\"postal_code\": \"94105\",
\"state\": \"CA\",
\"country\": \"us\"
},
\"shipping_address\": {
\"name\": \"Jane Doe\",
\"phone\": \"+1 555 0100\",
\"address_line_one\": \"500 Howard St\",
\"address_line_two\": \"Floor 2\",
\"city\": \"San Francisco\",
\"postal_code\": \"94105\",
\"state\": \"CA\",
\"country\": \"us\"
}
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/customers"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Jane Doe",
"email": "[email protected]",
"phone": "+1 555 0100",
"billing_address": {
"name": "Jane Doe",
"phone": "+1 555 0100",
"address_line_one": "1 Market St",
"address_line_two": "Suite 4",
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us"
},
"shipping_address": {
"name": "Jane Doe",
"phone": "+1 555 0100",
"address_line_one": "500 Howard St",
"address_line_two": "Floor 2",
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/customers';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Jane Doe',
'email' => '[email protected]',
'phone' => '+1 555 0100',
'billing_address' => ['name' => 'Jane Doe', 'phone' => '+1 555 0100', 'address_line_one' => '1 Market St', 'address_line_two' => 'Suite 4', 'city' => 'San Francisco', 'postal_code' => '94105', 'state' => 'CA', 'country' => 'us'],
'shipping_address' => ['name' => 'Jane Doe', 'phone' => '+1 555 0100', 'address_line_one' => '500 Howard St', 'address_line_two' => 'Floor 2', 'city' => 'San Francisco', 'postal_code' => '94105', 'state' => 'CA', 'country' => 'us'],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"id": 312,
"name": "Jane Doe",
"email": "[email protected]",
"phone": "+1 555 0100",
"billing_address": {
"name": "Jane Doe",
"phone": "+1 555 0100",
"address_line_one": "1 Market St",
"address_line_two": "Suite 4",
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us"
},
"shipping_address": {
"name": "Jane Doe",
"phone": "+1 555 0100",
"address_line_one": "500 Howard St",
"address_line_two": "Floor 2",
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us"
},
"created_at": "2026-10-06T09:00:00.000000Z",
"updated_at": "2026-10-06T09:00:00.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_customers scope required for this endpoint."
}
Example response (422, Email already taken):
{
"message": "The email has already been taken.",
"errors": {
"email": [
"The email has already been taken."
]
}
}
Example response (422, Unknown US state):
{
"message": "The state must be a state of us, as a code, region id or name, such as CA.",
"errors": {
"billing_address.state": [
"The state must be a state of us, as a code, region id or name, such as CA."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 customer
requires authentication
Only the fields you send change; an address you send replaces the stored
one whole, and null clears it. An address sent back exactly as it reads
changes nothing. Fires the customer_updated webhook when something changed.
Requires the write_customers token scope.
Example request:
curl --request PATCH \
"https://api.cartgenie.com/public/v1/customers/312" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Jane Smith\",
\"email\": \"[email protected]\",
\"phone\": \"+1 555 0101\",
\"billing_address\": {
\"name\": \"Jane Smith\",
\"phone\": \"+1 555 0101\",
\"address_line_one\": \"1 Market St\",
\"address_line_two\": \"Suite 4\",
\"city\": \"San Francisco\",
\"postal_code\": \"94105\",
\"state\": \"CA\",
\"country\": \"us\"
},
\"shipping_address\": {
\"name\": \"Jane Smith\",
\"phone\": \"+1 555 0101\",
\"address_line_one\": \"500 Howard St\",
\"address_line_two\": \"Floor 2\",
\"city\": \"San Francisco\",
\"postal_code\": \"94105\",
\"state\": \"CA\",
\"country\": \"us\"
}
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/customers/312"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Jane Smith",
"email": "[email protected]",
"phone": "+1 555 0101",
"billing_address": {
"name": "Jane Smith",
"phone": "+1 555 0101",
"address_line_one": "1 Market St",
"address_line_two": "Suite 4",
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us"
},
"shipping_address": {
"name": "Jane Smith",
"phone": "+1 555 0101",
"address_line_one": "500 Howard St",
"address_line_two": "Floor 2",
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us"
}
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/customers/312';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Jane Smith',
'email' => '[email protected]',
'phone' => '+1 555 0101',
'billing_address' => ['name' => 'Jane Smith', 'phone' => '+1 555 0101', 'address_line_one' => '1 Market St', 'address_line_two' => 'Suite 4', 'city' => 'San Francisco', 'postal_code' => '94105', 'state' => 'CA', 'country' => 'us'],
'shipping_address' => ['name' => 'Jane Smith', 'phone' => '+1 555 0101', 'address_line_one' => '500 Howard St', 'address_line_two' => 'Floor 2', 'city' => 'San Francisco', 'postal_code' => '94105', 'state' => 'CA', 'country' => 'us'],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 312,
"name": "Jane Smith",
"email": "[email protected]",
"phone": "+1 555 0101",
"billing_address": {
"name": "Jane Smith",
"phone": "+1 555 0101",
"address_line_one": "1 Market St",
"address_line_two": "Suite 4",
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us"
},
"shipping_address": {
"name": "Jane Smith",
"phone": "+1 555 0101",
"address_line_one": "500 Howard St",
"address_line_two": "Floor 2",
"city": "San Francisco",
"postal_code": "94105",
"state": "CA",
"country": "us"
},
"created_at": "2026-05-20T09:00:00.000000Z",
"updated_at": "2026-10-06T09:00:00.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_customers scope required for this endpoint."
}
Example response (404, Customer belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (422, Email already taken):
{
"message": "The email has already been taken.",
"errors": {
"email": [
"The email has already been taken."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Erase a customer
requires authentication
For a data-erasure request. Removes the customer's name, phone, addresses
and notes, and unlinks their saved payment details; the email becomes
erased-{id}@erased.invalid, including on discount codes issued to that
email. Carts with that email that never checked out lose the email and
addresses. The customer
and their orders stay, because orders are the store's financial records:
an order keeps the addresses it was placed with, and its customer email
reads as the erased one. The customer record at the payment provider is not
deleted; remove it there if the request covers it. Refused while the
customer has a subscription that can still charge, since renewals need the
saved payment details. There is no undo.
Requires the write_customers token scope.
Example request:
curl --request DELETE \
"https://api.cartgenie.com/public/v1/customers/312" \
--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/customers/312"
);
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/customers/312';
$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, Erased):
Empty response
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_customers scope required for this endpoint."
}
Example response (404, Customer belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (409, Live subscription):
{
"message": "The customer has a subscription that can still renew; cancel it before erasing the customer."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Custom fields
List product custom fields
requires authentication
The fields the store has added to its products, in the order the dashboard
shows them. custom_fields on POST /products and PATCH /products/{id}
takes a value per field id; a required field needs one when a product is
created and cannot be cleared, and a PATCH is refused while one is still
empty. A field sent back exactly as it reads is left as it is.
Values by type: text, rich_text (HTML), email, phone, link,
video_link and color (#rrggbb) are strings; number is a number
(whole when integer is true) between min and max; switch is a
boolean; date is an ISO 8601 date (2026-10-06 or 2026-10-06T09:30:00+02:00);
option is one of options; reference is the id of one item from
GET /products/custom-fields/{field}/items and multi-reference a list of
them; image and file are the public HTTPS URL of a file CartGenie
downloads and stores, and multi_image a list of up to 10 image URLs. Any
other list, a reordered one included, replaces every file of the field.
null clears a field. min and max bound the length of text and
rich_text.
Requires the read_products token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/products/custom-fields" \
--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/custom-fields"
);
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/custom-fields';
$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):
{
"data": [
{
"id": 12,
"label": "Material",
"type": "option",
"required": true,
"help_text": null,
"options": [
"Cotton",
"Linen"
],
"min": null,
"max": null,
"integer": null
},
{
"id": 13,
"label": "Care guide",
"type": "file",
"required": false,
"help_text": "PDF, up to 10 MB",
"options": null,
"min": null,
"max": null,
"integer": null
}
]
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_products scope required for this endpoint."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
List a product reference field's items
requires authentication
For a reference or multi-reference field: the items of the Webflow
collection it refers to, read from Webflow on each call.
Requires the read_products token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/products/custom-fields/14/items" \
--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/custom-fields/14/items"
);
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/custom-fields/14/items';
$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):
{
"data": [
{
"id": "64f1c2a9b3e5d7a1c2e3f4a5",
"name": "Summer collection"
}
]
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_products scope required for this endpoint."
}
Example response (404, Not a reference field of this store):
{
"message": "Resource not found."
}
Example response (424, Webflow could not be read):
{
"message": "The items could not be read from Webflow; try again later."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
List category custom fields
requires authentication
The fields the store has added to its categories. custom_fields on
POST /categories and PATCH /categories/{category} takes a value per
field id, in the same types as product custom fields.
Requires the read_categories token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/categories/custom-fields" \
--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/categories/custom-fields"
);
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/categories/custom-fields';
$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):
{
"data": [
{
"id": 21,
"label": "Banner caption",
"type": "text",
"required": false,
"help_text": null,
"options": null,
"min": null,
"max": 120,
"integer": null
}
]
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_categories scope required for this endpoint."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
List a category reference field's items
requires authentication
Requires the read_categories token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/categories/custom-fields/22/items" \
--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/categories/custom-fields/22/items"
);
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/categories/custom-fields/22/items';
$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):
{
"data": [
{
"id": "64f1c2a9b3e5d7a1c2e3f4a5",
"name": "Classic Tee"
}
]
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_categories scope required for this endpoint."
}
Example response (404, Not a reference field of this store):
{
"message": "Resource not found."
}
Example response (424, Webflow could not be read):
{
"message": "The items could not be read from Webflow; try again later."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Discounts
List discounts
requires authentication
Pass code to find the discount a coupon code belongs to; codes match
case-insensitively. Automatic discounts have no code, so filtering by code
leaves them out.
Percentage values are in percent (12.5 is 12.5%). Fixed values and every
order-value limit are integers in the store currency's smallest unit, the
same as order totals.
Requires the read_discounts token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/discounts?code=SUMMER10&limit=50" \
--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/discounts"
);
const params = {
"code": "SUMMER10",
"limit": "50",
};
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/discounts';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'code' => 'SUMMER10',
'limit' => '50',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 41,
"name": "Summer sale",
"notes": null,
"activation": "code",
"code": "SUMMER10",
"coupon_name": null,
"type": "percentage",
"value": 12.5,
"formatted_value": "12.5%",
"application_mode": "once_per_order",
"target": "products",
"category_ids": [],
"product_ids": [
12,
15
],
"starts_at": "2026-06-01T00:00:00.000000Z",
"ends_at": null,
"min_order_value": 5000,
"formatted_min_order_value": "$50.00",
"max_discount_value": 2000,
"formatted_max_discount_value": "$20.00",
"min_quantity": 2,
"max_uses": 100,
"max_uses_per_customer": 1,
"times_used": 12,
"enabled": true,
"created_at": "2026-05-20T09:00:00.000000Z",
"updated_at": "2026-05-20T09:00:00.000000Z"
}
],
"meta": {
"per_page": 50,
"has_more": false,
"next_cursor": null
}
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_discounts scope required for this endpoint."
}
Example response (422, Malformed cursor):
{
"message": "The cursor is invalid.",
"errors": {
"cursor": [
"The cursor is invalid."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 discount
requires authentication
Requires the read_discounts token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/discounts/41" \
--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/discounts/41"
);
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/discounts/41';
$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": 41,
"name": "Summer sale",
"notes": null,
"activation": "code",
"code": "SUMMER10",
"coupon_name": null,
"type": "percentage",
"value": 10,
"formatted_value": "10%",
"application_mode": "once_per_order",
"target": "all",
"category_ids": [],
"product_ids": [],
"starts_at": "2026-06-01T00:00:00.000000Z",
"ends_at": "2026-08-31T23:59:00.000000Z",
"min_order_value": 5000,
"formatted_min_order_value": "$50.00",
"max_discount_value": 2000,
"formatted_max_discount_value": "$20.00",
"min_quantity": 2,
"max_uses": 100,
"max_uses_per_customer": 1,
"times_used": 12,
"enabled": false,
"created_at": "2026-05-20T09:00:00.000000Z",
"updated_at": "2026-05-20T09:00:00.000000Z"
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_discounts scope required for this endpoint."
}
Example response (404, Discount belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 discount
requires authentication
Creates a coupon-code discount, or an automatic one where the store has
automatic discounts enabled. value is in percent for percentage and in
the currency's smallest unit for fixed; free_shipping takes no value and
always targets shipping. product_ids are the ids from GET /products and
must be published; category_ids are the store's category ids.
Fires the new_discount webhook.
Requires the write_discounts token scope.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/discounts" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Summer sale\",
\"notes\": \"Newsletter campaign\",
\"activation\": \"code\",
\"code\": \"SUMMER10\",
\"type\": \"percentage\",
\"value\": 10,
\"application_mode\": \"once_per_order\",
\"target\": \"all\",
\"starts_at\": \"2026-06-01T00:00:00Z\",
\"ends_at\": \"2026-08-31T23:59:00Z\",
\"min_order_value\": 5000,
\"max_discount_value\": 2000,
\"min_quantity\": 2,
\"max_uses\": 100,
\"max_uses_per_customer\": 1,
\"enabled\": true
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/discounts"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Summer sale",
"notes": "Newsletter campaign",
"activation": "code",
"code": "SUMMER10",
"type": "percentage",
"value": 10,
"application_mode": "once_per_order",
"target": "all",
"starts_at": "2026-06-01T00:00:00Z",
"ends_at": "2026-08-31T23:59:00Z",
"min_order_value": 5000,
"max_discount_value": 2000,
"min_quantity": 2,
"max_uses": 100,
"max_uses_per_customer": 1,
"enabled": 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/discounts';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Summer sale',
'notes' => 'Newsletter campaign',
'activation' => 'code',
'code' => 'SUMMER10',
'type' => 'percentage',
'value' => 10.0,
'application_mode' => 'once_per_order',
'target' => 'all',
'starts_at' => '2026-06-01T00:00:00Z',
'ends_at' => '2026-08-31T23:59:00Z',
'min_order_value' => 5000,
'max_discount_value' => 2000,
'min_quantity' => 2,
'max_uses' => 100,
'max_uses_per_customer' => 1,
'enabled' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"id": 41,
"name": "Summer sale",
"notes": null,
"activation": "code",
"code": "SUMMER10",
"coupon_name": null,
"type": "percentage",
"value": 12.5,
"formatted_value": "12.5%",
"application_mode": "once_per_order",
"target": "products",
"category_ids": [],
"product_ids": [
12,
15
],
"starts_at": "2026-06-01T00:00:00.000000Z",
"ends_at": null,
"min_order_value": 5000,
"formatted_min_order_value": "$50.00",
"max_discount_value": 2000,
"formatted_max_discount_value": "$20.00",
"min_quantity": 2,
"max_uses": 100,
"max_uses_per_customer": 1,
"times_used": 0,
"enabled": true,
"created_at": "2026-05-20T09:00:00.000000Z",
"updated_at": "2026-05-20T09:00:00.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_discounts scope required for this endpoint."
}
Example response (422, Code already used by another discount):
{
"message": "The coupon code already exists.",
"errors": {
"code": [
"The coupon code already exists."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 discount
requires authentication
Only the fields you send change; the result must still be a valid discount,
so changing type may require value, and target: products requires
product_ids. Send enabled: false to turn the discount off and true to
turn it back on. A discount whose type is free or whose target is
variants keeps that value when updated, and sending it back unchanged is
accepted; neither value can be chosen for another discount.
Fires the discount_updated webhook when something changed.
Requires the write_discounts token scope.
Example request:
curl --request PATCH \
"https://api.cartgenie.com/public/v1/discounts/41" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Summer sale\",
\"notes\": \"Extended to August\",
\"activation\": \"code\",
\"code\": \"SUMMER15\",
\"type\": \"percentage\",
\"value\": 15,
\"application_mode\": \"once_per_order\",
\"target\": \"all\",
\"starts_at\": \"2026-06-01T00:00:00Z\",
\"ends_at\": \"2026-09-30T23:59:00Z\",
\"min_order_value\": 5000,
\"max_discount_value\": 2000,
\"min_quantity\": 2,
\"max_uses\": 200,
\"max_uses_per_customer\": 1,
\"enabled\": false
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/discounts/41"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Summer sale",
"notes": "Extended to August",
"activation": "code",
"code": "SUMMER15",
"type": "percentage",
"value": 15,
"application_mode": "once_per_order",
"target": "all",
"starts_at": "2026-06-01T00:00:00Z",
"ends_at": "2026-09-30T23:59:00Z",
"min_order_value": 5000,
"max_discount_value": 2000,
"min_quantity": 2,
"max_uses": 200,
"max_uses_per_customer": 1,
"enabled": false
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/discounts/41';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Summer sale',
'notes' => 'Extended to August',
'activation' => 'code',
'code' => 'SUMMER15',
'type' => 'percentage',
'value' => 15.0,
'application_mode' => 'once_per_order',
'target' => 'all',
'starts_at' => '2026-06-01T00:00:00Z',
'ends_at' => '2026-09-30T23:59:00Z',
'min_order_value' => 5000,
'max_discount_value' => 2000,
'min_quantity' => 2,
'max_uses' => 200,
'max_uses_per_customer' => 1,
'enabled' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"id": 41,
"name": "Summer sale",
"notes": "Extended to August",
"activation": "code",
"code": "SUMMER15",
"coupon_name": null,
"type": "percentage",
"value": 15,
"formatted_value": "15%",
"application_mode": "once_per_order",
"target": "all",
"category_ids": [],
"product_ids": [],
"starts_at": "2026-06-01T00:00:00.000000Z",
"ends_at": "2026-09-30T23:59:00.000000Z",
"min_order_value": 5000,
"formatted_min_order_value": "$50.00",
"max_discount_value": 2000,
"formatted_max_discount_value": "$20.00",
"min_quantity": 2,
"max_uses": 200,
"max_uses_per_customer": 1,
"times_used": 12,
"enabled": true,
"created_at": "2026-05-20T09:00:00.000000Z",
"updated_at": "2026-07-01T10:30:00.000000Z"
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_discounts scope required for this endpoint."
}
Example response (404, Discount belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (422, Code already used by another discount):
{
"message": "The coupon code already exists.",
"errors": {
"code": [
"The coupon code already exists."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
List a discount's codes
requires authentication
Every code that applies the discount: its own code (kind: primary) and the
codes issued with POST /discounts/{discount}/codes (kind: generated). An
automatic discount has no code of its own, so its list is empty.
times_used counts the orders placed with that code, paid or awaiting an
offline payment, so it attributes redemptions to the recipient the code was
sent to.
Requires the read_discounts token scope.
Example request:
curl --request GET \
--get "https://api.cartgenie.com/public/v1/discounts/41/codes?kind=generated&limit=50" \
--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/discounts/41/codes"
);
const params = {
"kind": "generated",
"limit": "50",
};
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/discounts/41/codes';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'kind' => 'generated',
'limit' => '50',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200):
{
"data": [
{
"id": 905,
"discount_id": 41,
"code": "WELCOME-7KQ2M9XHTA",
"kind": "generated",
"max_uses": 1,
"times_used": 1,
"expires_at": "2026-11-05T09:00:00.000000Z",
"email": "[email protected]",
"created_at": "2026-10-06T09:00:00.000000Z"
}
],
"meta": {
"per_page": 50,
"has_more": false,
"next_cursor": null
}
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the read_discounts scope required for this endpoint."
}
Example response (404, Discount belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (422, Malformed cursor):
{
"message": "The cursor is invalid.",
"errors": {
"cursor": [
"The cursor is invalid."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 discount
requires authentication
Deletes the discount with every code that applies it, including issued ones. Carts holding any of those codes are repriced; orders already placed keep the discount they were given. There is no undo.
Requires the write_discounts token scope.
Example request:
curl --request DELETE \
"https://api.cartgenie.com/public/v1/discounts/41" \
--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/discounts/41"
);
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/discounts/41';
$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, Deleted):
Empty response
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_discounts scope required for this endpoint."
}
Example response (404, Discount belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.
Issue discount codes
requires authentication
Issues count unique random codes that each apply the discount, for an email
or SMS tool that hands one code to each recipient. Every code carries the
discount's own rules (dates, minimum order, total max_uses of the discount)
plus the limits sent here:
max_usescaps how often that one code can be redeemed;1makes it single-use.expires_in_dayscounts from the moment the code is issued, so every recipient gets the same window whenever the message goes out. The discount's ownends_atstill applies when it is earlier.emailrestricts the code to a checkout with that email address. A restricted code is issued one at a time (count: 1).
Codes are uppercase: the prefix is uppercased and followed by 10 characters
from A–Z and 2–9 without look-alikes (0, 1, I, O). A discount holds
at most 100,000 issued codes; delete unused ones to issue more. Only a discount
activated by code can issue codes.
Requires the write_discounts token scope.
Example request:
curl --request POST \
"https://api.cartgenie.com/public/v1/discounts/41/codes" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"count\": 1,
\"prefix\": \"WELCOME-\",
\"max_uses\": 1,
\"expires_in_days\": 30,
\"email\": \"[email protected]\"
}"
const url = new URL(
"https://api.cartgenie.com/public/v1/discounts/41/codes"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"count": 1,
"prefix": "WELCOME-",
"max_uses": 1,
"expires_in_days": 30,
"email": "[email protected]"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://api.cartgenie.com/public/v1/discounts/41/codes';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'count' => 1,
'prefix' => 'WELCOME-',
'max_uses' => 1,
'expires_in_days' => 30,
'email' => '[email protected]',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201):
{
"data": [
{
"id": 905,
"discount_id": 41,
"code": "WELCOME-7KQ2M9XHTA",
"kind": "generated",
"max_uses": 1,
"times_used": 0,
"expires_at": "2026-11-05T09:00:00.000000Z",
"email": "[email protected]",
"created_at": "2026-10-06T09:00:00.000000Z"
}
]
}
Example response (400, Body that is not valid JSON):
{
"message": "The request body is not valid JSON."
}
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_discounts scope required for this endpoint."
}
Example response (404, Discount belongs to another store or does not exist):
{
"message": "Resource not found."
}
Example response (409, Automatic discount):
{
"message": "Codes can only be issued for a discount activated by code."
}
Example response (422, Too many codes on the discount):
{
"message": "A discount holds at most 100,000 issued codes; this one already has 99,990.",
"errors": {
"count": [
"A discount holds at most 100,000 issued codes; this one already has 99,990."
]
}
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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 an issued discount code
requires authentication
The code stops applying at once; carts holding it are repriced. A discount's
own code (kind: primary) is changed with PATCH /discounts/{discount} and
cannot be deleted here.
Requires the write_discounts token scope.
Example request:
curl --request DELETE \
"https://api.cartgenie.com/public/v1/discounts/41/codes/905" \
--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/discounts/41/codes/905"
);
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/discounts/41/codes/905';
$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, Deleted):
Empty response
Example response (401, Missing or invalid token):
{
"message": "Unauthenticated."
}
Example response (403, Token without the required scope):
{
"message": "This token does not have the write_discounts scope required for this endpoint."
}
Example response (404, Code belongs to another discount or does not exist):
{
"message": "Resource not found."
}
Example response (409, The discount's own code):
{
"message": "A discount's own code cannot be deleted; change it with PATCH /discounts/{discount}."
}
Example response (429, Rate limit exceeded):
{
"message": "Too Many Attempts."
}
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.