MENU navbar-image

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:

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."
}
 

Request   

GET public/v1/store

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
}
 

Request   

GET public/v1/tax-codes

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
}
 

Request   

GET public/v1/products

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

limit   integer  optional    

Results per page, 1–50. Example: 50

cursor   string  optional    

Opaque cursor from meta.next_cursor of the previous page.

search   string  optional    

Matches against product name and slug. Example: tee

slug   string  optional    

The exact slug of one product. Example: classic-tee

updated_since   string  optional    

ISO-8601 date and time with an offset (Z or +02:00). Returns products changed or deleted since then. Example: 2026-08-01T00:00:00Z

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."
}
 

Request   

GET public/v1/products/{id}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

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."
}
 

Request   

POST public/v1/products

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

custom_fields   object  optional    

Values by custom field id from GET /products/custom-fields; every required field needs one.

name   string     

Example: Classic Tee

slug   string  optional    

Unique in the store. Example: classic-tee

description   string  optional    

HTML allowed, as in the product editor. Example: <p>Soft cotton tee.</p>

meta_description   string  optional    

Example: A soft cotton tee.

product_type   string  optional    

physical (default), digital, service or advanced. Example: physical

price   integer     

In the smallest currency unit. Example: 2500

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."
}
 

Request   

PATCH public/v1/products/{id}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

custom_fields   object  optional    

Values by custom field id from GET /products/custom-fields; only the fields sent change, null clears one.

name   string  optional    

Example: Classic Tee

slug   string  optional    

Unique in the store. Example: classic-tee

description   string  optional    

HTML allowed, as in the product editor. Example: <p>Soft cotton tee.</p>

meta_description   string  optional    

Example: Soft cotton tee in five colors.

product_type   string  optional    

physical, digital, service or advanced. Example: physical

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."
}
 

Request   

PUT public/v1/products/{id}/status

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

status   string     

draft, queued or published. Example: published

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."
}
 

Request   

PUT public/v1/products/{id}/categories

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

category_ids   integer[]     

Ids from GET /categories.

primary_category_id   integer  optional    

One of category_ids. Example: 7

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."
}
 

Request   

PUT public/v1/products/{id}/tax

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

tax_class   string     

default, non_taxable or custom. Example: custom

tax_class_id   integer  optional    

Required with tax_class: custom; with a built-in class only null, as the product reads back. Example: 5

tax_code_id   integer  optional    

The tax provider's product tax code, from GET /tax-codes; null falls back to the provider's default code, leaving it out keeps the current one. Example: 41

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."
}
 

Request   

PUT public/v1/products/{id}/shipping

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

requires_shipping   boolean  optional    

Example: true

auto_fulfill_order   boolean  optional    

Example: false

hs_code   string  optional    

Send null to clear it. Example: 6109.10

country_of_origin   string  optional    

Send null to clear it. Example: pt

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."
}
 

Request   

PUT public/v1/products/{id}/purchase-limits

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

min_quantity_per_order   integer  optional    

Example: 2

max_quantity_per_order   integer  optional    

Example: 10

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."
}
 

Request   

PUT public/v1/products/{id}/pre-order

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

pre_order   boolean  optional    

Example: true

estimated_shipping_date   string  optional    

Needs pre_order true. Example: Mid 2026

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."
}
 

Request   

PUT public/v1/products/{id}/feed

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

brand   string  optional    

Example: Northwind

condition   string  optional    

Example: new

mpn   string  optional    

Example: NW-TEE-01

gtin   string  optional    

Example: 00012345678905

google_product_category_id   integer  optional    

Example: 212

include_variant_info_in_title_feed   boolean  optional    

Example: true

gender   string  optional    

Example: unisex

age_group   string  optional    

Example: adult

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."
}
 

Request   

PUT public/v1/products/{id}/personalization-fields

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

personalization_fields   object[]     
name   string     

Example: Engraving

placeholder   string  optional    

Hint shown inside the empty input. Example: Up to 20 letters

help   string  optional    

Help text shown with the field. Example: Printed on the back.

required   boolean  optional    

Example: false

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."
}
 

Request   

PUT public/v1/products/{id}/download

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

name   string     

Example: Pattern (PDF)

url   string     

An http or https link. Example: https://files.example.com/pattern.pdf

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."
}
 

Request   

DELETE public/v1/products/{id}/download

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

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."
}
 

Request   

PUT public/v1/products/{id}/images

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

main   object  optional    

The main image, or null to remove it.

id   integer  optional    

The id of the current main image, to keep it. Example: 901

url   string  optional    

Required for a new image. Example: https://cdn.example.com/tee.jpg

alt_text   string  optional    

Example: Black cotton tee

extra   object[]  optional    

The extra images in order.

id   integer  optional    

The id of one of the current extra images, to keep it. Example: 902

url   string  optional    

Required for a new image. Example: https://cdn.example.com/tee-back.jpg

alt_text   string  optional    

Example: Back of the tee

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."
}
 

Request   

POST public/v1/products/{id}/options

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

name   string     

Unique among the product's options regardless of case. Example: Size

swatch_type   string  optional    

button or color. Example: button

values   object[]     

In display order, unique regardless of case.

id   integer  optional    

Leave out: the values of a new option have no ids yet.

value   string     

Example: M

color   string  optional    

Required for swatch_type color.

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."
}
 

Request   

PATCH public/v1/products/{id}/options/{option}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

option   integer     

The option id from the product's options. Example: 31

Body Parameters

name   string  optional    

Example: Size

swatch_type   string  optional    

button or color; image for an option that already has it. Example: button

values   object[]  optional    
id   integer  optional    

A value id from the option's values; leave out for a new value. Example: 88

value   string     

Example: Medium

color   string  optional    

Required for swatch_type color.

id   integer  optional    

The option id from the path, when sending the option back as read. Example: 31

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."
}
 

Request   

DELETE public/v1/products/{id}/options/{option}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

option   integer     

The option id. Example: 31

Query Parameters

keep_value_id   integer  optional    

The value whose variants stay. Example: 88

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."
}
 

Request   

PUT public/v1/products/{id}/default-variant

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 364

Body Parameters

variant_id   integer     

The id of one of the product's variants, or null to remove the default. Example: 2302

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."
}
 

Request   

PUT public/v1/products/{id}/bundle

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The product id. Example: 371

Body Parameters

components   object[]     

Up to 100.

variant_id   integer     

A variant id from GET /variants. Example: 2301

quantity   integer     

Units of the variant in one bundle. Example: 2

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."
}
 

Request   

DELETE public/v1/products/{id}/bundle

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The bundle's product id. Example: 371

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."
}
 

Request   

POST public/v1/products/{id}/bundle/components

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The bundle's product id. Example: 371

Body Parameters

variant_id   integer     

A variant id from GET /variants. Example: 2315

quantity   integer     

Units of the variant in one bundle. Example: 1

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."
}
 

Request   

DELETE public/v1/products/{id}/bundle/components/{variant}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The bundle's product id. Example: 371

variant   integer     

The component's variant id. Example: 2310

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."
}
 

Request   

GET public/v1/variants

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

limit   integer  optional    

Results per page, 1–250. Example: 50

cursor   string  optional    

Opaque cursor from meta.next_cursor of the previous page.

sku   string  optional    

Returns every variant carrying exactly this SKU; letter case counts. Example: TEE-BLK-M

updated_since   string  optional    

ISO-8601 date and time with an offset (Z or +02:00). Example: 2026-08-01T00:00:00Z

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."
}
 

Request   

PATCH public/v1/variants/{id}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The variant id. Example: 2302

Body Parameters

price   integer  optional    

In the store currency's smallest unit. Example: 5400

compare_at_price   integer  optional    

Send null to clear it. Example: 5900

sku   string  optional    

Send null to clear it. SKUs need not be unique. Example: TEE-BLK-M

weight   integer  optional    

Hundredths of the store's weight unit, as dimensions reports them. Only for a variant that requires shipping; any other variant accepts only its current value or null, which changes nothing. Example: 18000

length   integer  optional    

Hundredths of the store's dimension unit. Only for a variant that requires shipping; any other variant accepts only its current value or null, which changes nothing. Example: 3000

width   integer  optional    

Hundredths of the store's dimension unit. Only for a variant that requires shipping; any other variant accepts only its current value or null, which changes nothing. Example: 2000

height   integer  optional    

Hundredths of the store's dimension unit. Only for a variant that requires shipping; any other variant accepts only its current value or null, which changes nothing. Example: 200

enabled   boolean  optional    

A disabled variant cannot be bought. Example: true

description   string  optional    

HTML allowed; null falls back to the product description. Example: <p>Black, medium.</p>

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."
}
 

Request   

PUT public/v1/variants/{id}/subscription

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The variant id. Example: 2302

Body Parameters

interval   string     

day, week, month or year. Example: month

interval_count   integer  optional    

Number of intervals between payments. Defaults to 1. Example: 1

term   string  optional    

renews_until_canceled or fixed_term. Example: fixed_term

ends_after   integer  optional    

Required for a fixed term: the number of payments, 2 to 1000; null for any other term. Example: 12

renewal_price   integer  optional    

Price of each renewal, in the store currency's smallest unit; null charges the variant's price. Example: 3900

charge_shipping_on_renewals   boolean  optional    

Charge shipping on each renewal. Defaults to true; applies only to a variant that requires shipping; any other variant accepts true or null and refuses false. Example: true

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."
}
 

Request   

DELETE public/v1/variants/{id}/subscription

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The variant id. Example: 2302

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."
}
 

Request   

PUT public/v1/variants/{id}/download

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The variant id. Example: 2302

Body Parameters

name   string     

Example: Pattern, size M (PDF)

url   string     

An http or https link. Example: https://files.example.com/pattern-m.pdf

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."
}
 

Request   

DELETE public/v1/variants/{id}/download

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The variant id. Example: 2302

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."
}
 

Request   

PUT public/v1/variants/{id}/images

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The variant id. Example: 2302

Body Parameters

main   object  optional    

The variant's own main image, or null to remove it.

id   integer  optional    

The id of the current main image, to keep it. Example: 911

url   string  optional    

Required for a new image. Example: https://cdn.example.com/tee-m.jpg

alt_text   string  optional    

Example: Black tee, size M

extra   object[]  optional    

The extra images in order.

id   integer  optional    

The id of one of the current extra images, to keep it. Example: 912

url   string  optional    

Required for a new image. Example: https://cdn.example.com/tee-m-back.jpg

alt_text   string  optional    

Example: Back of the tee

hide_main_image   boolean  optional    

Hide the variant's main image, the product's included. Example: false

hide_extra_images   boolean  optional    

Hide the variant's extra images, the product's included. Example: false

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."
}
 

Request   

PUT public/v1/variants/{id}/inventory

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   integer     

The variant id. Example: 2302

Body Parameters

stock   integer     

The new absolute stock level. Example: 120

expected_stock   integer  optional    

The level you last read. When it no longer matches, the request fails with 409. Example: 118

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."
}
 

Request   

GET public/v1/orders

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

limit   integer  optional    

Results per page, 1–250. Example: 50

cursor   string  optional    

Opaque cursor from meta.next_cursor of the previous page.

updated_since   string  optional    

ISO-8601 date and time with an offset (Z or +02:00). Example: 2026-08-01T00:00:00Z

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."
}
 

Request   

GET public/v1/orders/{orderName}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

orderName   string     

The order's name. Example: 1002

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."
}
 

Request   

PATCH public/v1/orders/{orderName}/tracking

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

orderName   string     

The order's name. Example: 1002

Body Parameters

shipping_carrier   string  optional    

The carrier name shown to the customer. Example: DHL

tracking_number   string  optional    

Example: JD014600006281230542

tracking_url   string  optional    

Must be a valid URL. Example: https://www.dhl.com/track?id=JD014600006281230542

fulfillment_id   integer  optional    

Required when the order has multiple fulfillments or is partially fulfilled. Example: 91

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."
}
 

Request   

POST public/v1/orders/{orderName}/fulfill

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

orderName   string     

The order's name. Example: 1002

Body Parameters

shipping_carrier   string  optional    

Example: DHL

tracking_number   string  optional    

Example: JD014600006281230542

tracking_url   string  optional    

Example: https://www.dhl.com/track?id=JD014600006281230542

send_notification   boolean  optional    

Emails the customer that the order shipped. Defaults to false. Example: true

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."
}
 

Request   

POST public/v1/orders/{orderName}/unfulfill

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

orderName   string     

The order's name. Example: 1002

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."
}
 

Request   

POST public/v1/orders/{orderName}/cancel

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

orderName   string     

The order's name. Example: 1002

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."
}
 

Request   

POST public/v1/orders/{orderName}/mark-paid

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

orderName   string     

The order's name. Example: 1002

Body Parameters

send_notification   boolean  optional    

Emails the customer the payment confirmation. Defaults to false. Example: true

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."
}
 

Request   

POST public/v1/orders/{orderName}/mark-unpaid

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

orderName   string     

The order's name. Example: 1002

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."
}
 

Request   

GET public/v1/orders/{orderName}/refunds

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

orderName   string     

The order's name. Example: 1002

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:

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."
}
 

Request   

POST public/v1/orders/{orderName}/refund

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Idempotency-Key        

Example: zap-run-42

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

orderName   string     

The order's name. Example: 1002

Body Parameters

amount   integer  optional    

In the order currency's smallest unit; defaults to everything not yet refunded. Example: 1500

restock   boolean  optional    

Return the items to inventory. Example: false

reason   string  optional    

Kept with the refund and returned as reason. Example: Damaged in transit

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."
}
 

Request   

GET public/v1/webhooks/sample

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

event   string     

The event to sample, from the list under "Create a webhook subscription". Example: new_category

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."
}
 

Request   

GET public/v1/webhooks

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

limit   integer  optional    

Results per page, 1–250. Example: 50

cursor   string  optional    

Opaque cursor from meta.next_cursor of the previous page.

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."
}
 

Request   

GET public/v1/webhooks/{webhook}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

webhook   integer     

The subscription id. Example: 7

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."
}
 

Request   

POST public/v1/webhooks

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

A label for this subscription. Example: Order sync

url   string     

Publicly reachable HTTPS endpoint. Example: https://example.com/hooks/cartgenie

description   string  optional    

Free-form note, up to 10000 characters. Example: Feeds our ERP

enabled   boolean  optional    

Defaults to true. Example: true

events   string[]     

One or more event names from the list above.

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."
}
 

Request   

PATCH public/v1/webhooks/{webhook}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

webhook   integer     

The subscription id. Example: 7

Body Parameters

name   string  optional    

Example: Order sync

url   string  optional    

Publicly reachable HTTPS endpoint. Example: https://example.com/hooks/cartgenie

description   string  optional    

Example: Feeds our ERP

enabled   boolean  optional    

Send false to pause deliveries without deleting the subscription. Example: false

events   string[]  optional    

Replaces the subscribed events.

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."
}
 

Request   

DELETE public/v1/webhooks/{webhook}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

webhook   integer     

The subscription id. Example: 7

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."
}
 

Request   

POST public/v1/webhooks/{webhook}/test

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

webhook   integer     

The subscription id. Example: 7

Body Parameters

event   string     

An event this subscription is subscribed to. Example: new_order

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."
}
 

Request   

GET public/v1/categories

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

slug   string  optional    

A category slug. Example: summer-collection

name   string  optional    

A category name. Example: Summer collection

limit   integer  optional    

Results per page, 1–250. Example: 50

cursor   string  optional    

Opaque cursor from meta.next_cursor of the previous page.

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."
}
 

Request   

GET public/v1/categories/{category}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

category   integer     

The category id. Example: 7

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."
}
 

Request   

POST public/v1/categories

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

custom_fields   object  optional    

Values by custom field id from GET /categories/custom-fields; every required field needs one.

name   string     

Example: Summer collection

slug   string  optional    

Unique within the store; normalized as in the dashboard (Summer Sale! becomes summer-sale) and derived from name when left out. Example: summer-collection

description   string  optional    

Example: Light fabrics for hot days.

meta_description   string  optional    

Example: Summer clothing

status   string  optional    

draft (default), queued or published. Example: published

product_ids   integer[]  optional    

Ids from GET /products. A product without a primary category gets this one.

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."
}
 

Request   

PATCH public/v1/categories/{category}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

category   integer     

The category id. Example: 7

Body Parameters

custom_fields   object  optional    

Values by custom field id from GET /categories/custom-fields; only the fields sent change, null clears one.

name   string  optional    

Example: Summer collection

slug   string  optional    

Normalized as in the dashboard. Example: summer-collection

description   string  optional    

Example: Light fabrics for hot days.

meta_description   string  optional    

Example: Summer clothing

status   string  optional    

draft, queued or published. The category's current status is also accepted and changes nothing, so a category fetched with GET /categories/{category} can be sent back unchanged. Example: published

product_ids   integer[]  optional    

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."
}
 

Request   

GET public/v1/customers

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

email   string  optional    

An email address to look up. Example: [email protected]

search   string  optional    

Part of a name or email. Example: jane

updated_since   string  optional    

ISO-8601 date and time with an offset (Z or +02:00). Example: 2026-08-01T00:00:00Z

limit   integer  optional    

Results per page, 1–250. Example: 50

cursor   string  optional    

Opaque cursor from meta.next_cursor of the previous page.

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."
}
 

Request   

GET public/v1/customers/{customer}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

customer   integer     

The customer id. Example: 312

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."
}
 

Request   

GET public/v1/customers/{customer}/orders

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

customer   integer     

The customer id. Example: 312

Query Parameters

limit   integer  optional    

Results per page, 1–250. Example: 50

cursor   string  optional    

Opaque cursor from meta.next_cursor of the previous page.

updated_since   string  optional    

ISO-8601 date and time with an offset (Z or +02:00). Example: 2026-08-01T00:00:00Z

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."
}
 

Request   

POST public/v1/customers

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string  optional    

Example: Jane Doe

email   string     

Example: [email protected]

phone   string  optional    

Example: +1 555 0100

billing_address   object  optional    
name   string  optional    

Example: Jane Doe

phone   string  optional    

Example: +1 555 0100

address_line_one   string  optional    

Example: 1 Market St

address_line_two   string  optional    

Example: Suite 4

city   string  optional    

Example: San Francisco

postal_code   string  optional    

Example: 94105

state   string  optional    

Required for us and ca (code, region id or name of a state); free text elsewhere. Example: CA

country   string  optional    

Two-letter country code, required with the address. Example: us

shipping_address   object  optional    
name   string  optional    

Example: Jane Doe

phone   string  optional    

Example: +1 555 0100

address_line_one   string  optional    

Example: 500 Howard St

address_line_two   string  optional    

Example: Floor 2

city   string  optional    

Example: San Francisco

postal_code   string  optional    

Example: 94105

state   string  optional    

Required for us and ca (code, region id or name of a state); free text elsewhere. Example: CA

country   string  optional    

Two-letter country code, required with the address. Example: us

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."
}
 

Request   

PATCH public/v1/customers/{customer}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

customer   integer     

The customer id. Example: 312

Body Parameters

name   string  optional    

Example: Jane Smith

email   string  optional    

Example: [email protected]

phone   string  optional    

Example: +1 555 0101

billing_address   object  optional    
name   string  optional    

Example: Jane Smith

phone   string  optional    

Example: +1 555 0101

address_line_one   string  optional    

Example: 1 Market St

address_line_two   string  optional    

Example: Suite 4

city   string  optional    

Example: San Francisco

postal_code   string  optional    

Example: 94105

state   string  optional    

Required for us and ca (code, region id or name of a state); free text elsewhere. Example: CA

country   string  optional    

Two-letter country code, required with the address. Example: us

shipping_address   object  optional    
name   string  optional    

Example: Jane Smith

phone   string  optional    

Example: +1 555 0101

address_line_one   string  optional    

Example: 500 Howard St

address_line_two   string  optional    

Example: Floor 2

city   string  optional    

Example: San Francisco

postal_code   string  optional    

Example: 94105

state   string  optional    

Required for us and ca (code, region id or name of a state); free text elsewhere. Example: CA

country   string  optional    

Two-letter country code, required with the address. Example: us

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."
}
 

Request   

DELETE public/v1/customers/{customer}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

customer   integer     

The customer id. Example: 312

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."
}
 

Request   

GET public/v1/products/custom-fields

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
}
 

Request   

GET public/v1/products/custom-fields/{field}/items

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

field   integer     

The custom field id. Example: 14

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."
}
 

Request   

GET public/v1/categories/custom-fields

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

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."
}
 

Request   

GET public/v1/categories/custom-fields/{field}/items

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

field   integer     

The custom field id. Example: 22

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."
}
 

Request   

GET public/v1/discounts

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

code   string  optional    

A coupon code to look up. Example: SUMMER10

limit   integer  optional    

Results per page, 1–250. Example: 50

cursor   string  optional    

Opaque cursor from meta.next_cursor of the previous page.

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."
}
 

Request   

GET public/v1/discounts/{discount}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

discount   integer     

The discount id. Example: 41

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."
}
 

Request   

POST public/v1/discounts

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

name   string     

Example: Summer sale

notes   string  optional    

Internal note. Example: Newsletter campaign

activation   string  optional    

code (default) or automatic. Example: code

code   string  optional    

Required for code activation. Letters, digits, - and _; converted to upper case. Example: SUMMER10

coupon_name   string  optional    

Required for automatic activation; shown at checkout.

type   string     

percentage, fixed or free_shipping. Example: percentage

value   number  optional    

Required unless free_shipping. 1–100 for percentage; an integer in the smallest currency unit for fixed. Example: 10

application_mode   string  optional    

once_per_order (default) or once_per_item; only for fixed. Example: once_per_order

target   string  optional    

all (default), products or categories. Example: all

category_ids   integer[]  optional    

Required when target is categories.

product_ids   integer[]  optional    

Required when target is products.

starts_at   string  optional    

ISO 8601; defaults to now. A time without an offset is read in the store timezone. Example: 2026-06-01T00:00:00Z

ends_at   string  optional    

ISO 8601, after starts_at; null never expires. Example: 2026-08-31T23:59:00Z

min_order_value   integer  optional    

Minimum order subtotal in the smallest currency unit. Example: 5000

max_discount_value   integer  optional    

Cap for percentage discounts, in the smallest currency unit. Example: 2000

min_quantity   integer  optional    

Minimum number of items. Example: 2

max_uses   integer  optional    

Total uses before the discount turns off. Example: 100

max_uses_per_customer   integer  optional    

Example: 1

enabled   boolean  optional    

Defaults to true. Example: true

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."
}
 

Request   

PATCH public/v1/discounts/{discount}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

discount   integer     

The discount id. Example: 41

Body Parameters

name   string  optional    

Example: Summer sale

notes   string  optional    

Example: Extended to August

activation   string  optional    

code or automatic. Example: code

code   string  optional    

Example: SUMMER15

coupon_name   string  optional    
type   string  optional    

percentage, fixed or free_shipping. Example: percentage

value   number  optional    

Example: 15

application_mode   string  optional    

Example: once_per_order

target   string  optional    

all, products or categories. Example: all

category_ids   integer[]  optional    
product_ids   integer[]  optional    
starts_at   string  optional    

Example: 2026-06-01T00:00:00Z

ends_at   string  optional    

Example: 2026-09-30T23:59:00Z

min_order_value   integer  optional    

Example: 5000

max_discount_value   integer  optional    

Example: 2000

min_quantity   integer  optional    

Example: 2

max_uses   integer  optional    

Example: 200

max_uses_per_customer   integer  optional    

Example: 1

enabled   boolean  optional    

Example: false

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."
}
 

Request   

GET public/v1/discounts/{discount}/codes

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

discount   integer     

The discount id. Example: 41

Query Parameters

kind   string  optional    

primary or generated. Example: generated

limit   integer  optional    

Results per page, 1–250. Example: 50

cursor   string  optional    

Opaque cursor from meta.next_cursor of the previous page.

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."
}
 

Request   

DELETE public/v1/discounts/{discount}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

discount   integer     

The discount id. Example: 41

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:

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."
}
 

Request   

POST public/v1/discounts/{discount}/codes

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

discount   integer     

The discount id. Example: 41

Body Parameters

count   integer     

Number of codes, 1–1000. Example: 1

prefix   string  optional    

Up to 20 letters, digits, - or _. Example: WELCOME-

max_uses   integer  optional    

Redemptions allowed per code; null is unlimited. Example: 1

expires_in_days   integer  optional    

Days from now until each code expires, 1–3650. Example: 30

email   string  optional    

Restrict the code to this checkout email; requires count 1. Example: [email protected]

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."
}
 

Request   

DELETE public/v1/discounts/{discount}/codes/{code}

Headers

Authorization        

Example: Bearer {YOUR_API_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

discount   integer     

The discount id. Example: 41

code   integer     

The code id from the code list. Example: 905