Categories
Categories give products and bundles reusable identities for grouping and targeting. Each category has a server-generated category_id, a stable handle, and a renameable display name.
Create each category with POST /v1/categories before anything uses it. Other resources name a category by its handle, and a write that names a handle no category has fails instead of creating one. Catalog setup creates categories and assigns them to products and bundles step by step.
Where categories are used#
| Resource | Field | Effect |
|---|---|---|
| Products and bundles | category_handles | Assigns up to 100 categories |
| Product and bundle lists | category_handle query | Lists one category's items |
| Promotions | line_item.categories rule | Matches lines in the category |
| Return reasons | category_handles | Offers the reason for those lines only |
| Return policies | scope.category_handles | Covers products in the categories |
On a product or bundle, a present category_handles array replaces the whole set, [] clears it, and the change doesn't need expected_version. Handles must match exactly. A handle that no category has returns 404 RESOURCE_NOT_FOUND with param naming the entry, such as category_handles[2], and details[0].resource_type set to category. Nothing is saved.
Handles and names#
The handle is the category's permanent key, and the name is its label.
handlehas 1 to 100 lowercase letters and digits, with single hyphens between words, such asespresso-drinks. No two of your categories share one. It never changes, so store it in your code, rules, and filters.- Omit
handleon create and Flint derives it fromname: letters are lowercased, accents are dropped, and each run of other characters becomes one hyphen, soCafé & Bakerybecomescafe-bakery. A name with no Latin letters or digits, such as茶, produces no handle, so sendhandlewith it. namehas 1 to 100 characters after Flint trims surrounding spaces, and the handle derived from it must also fit in 100 characters. Renaming withPATCH /v1/categories/{category_id}changes onlyname.- Neither a name nor a handle can contain
>.
To find the category that owns a handle, list with it: GET /v1/categories?handle=espresso-drinks returns that category as the only item in data, or an empty data when no category has the handle. The value must itself be a valid handle, so Espresso-Drinks returns 400 INVALID_REQUEST instead of matching.
Category references on other resources#
Products, bundles, and order and invoice line items return categories as {category_id, handle, name} objects. Products and bundles always show the category's current name. Order line items keep the handle and name from when the order was created, and invoice line items keep them from when the line was billed.
Match a reference to its category by category_id, which never changes. A handle can outlive its category: after a delete, a new category can take the same handle with a new category_id.
Versions and conditional writes#
Every category has a version that starts at 1 and increases with each change to it. Updates increase it, and so do changes to what uses it: assigning or removing a product or bundle, and a promotion, return reason, or return policy starting or stopping to reference it. The categories references on other resources carry no version.
Redeeming a promotion or releasing its reserved use does not change the category's version or targeting_reference_count. A promotion reaching its end time or use limit does not change the count either.
PATCH changes only the fields you send. To refuse the write if the category changed since you read it, send the version you read as expected_version: in the body on PATCH, or as a query parameter on DELETE. Without it, concurrent updates are applied one after the other.
curl -X PATCH https://api.withflintpay.com/v1/categories/ctg_01J00000000000000000000000 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "expected_version": 4, "name": "Espresso and tea" }'
{
"data": {
"assigned_bundle_count": 1,
"assigned_product_count": 6,
"category_id": "ctg_01J00000000000000000000000",
"created_at": "2026-03-17T14:30:00Z",
"handle": "espresso-drinks",
"merchant_id": "mer_01KEZF7C00MPP0T9S0KH5BB4CC",
"name": "Espresso and tea",
"status": "active",
"targeting_reference_count": 1,
"updated_at": "2026-03-19T09:12:00Z",
"version": 5
},
"request_id": "req_01KWM615AGC7G311TCBK6QE1Q8"
}
The handle stays espresso-drinks after the rename, so every product, filter, and rule that uses it keeps working.
Status and deletion#
status on categoryactiveEvery category you can read. Products and bundles can be assigned to it, and promotions and return configuration can target it.deletedFinalReturned only byDELETEand incategory.deleted. The category is gone:GETreturns404and lists leave it out.
Delete permanently removes a category that nothing uses, such as one created by mistake. assigned_product_count and assigned_bundle_count show how many products and bundles still carry it. targeting_reference_count counts promotions, return reasons, and return policies whose revisions reference it, including historical revisions. Promotions still count after they end or use up their limit; deleted promotions don't count. Each referencing resource counts once, even if several rules or revisions reference the category.
All three counts must be zero before you can delete the category. If any is above zero, the request returns 409 RESOURCE_IN_USE, and details[0].blocking_resources lists up to 25 referencing resources, with the total in blocking_resource_count. Publishing a new return policy revision without the category does not remove a historical revision's reference, so that category still cannot be deleted.
Errors to handle#
- StatusHTTP 409CodeALREADY_
EXISTS What to doOn create, another category already has the handle, including one derived fromname.details[0].blocking_resourcesnames that category. Use it, or send a differenthandle. - StatusHTTP 409CodeRESOURCE_
IN_ USE What to doOn delete, a product, bundle, promotion, return reason, or return policy revision still uses the category.details[0].blocking_resourcesnames the referencing resources. Remove references where possible; a historical policy revision's reference keeps the category from being deleted. - StatusHTTP 409What to doThe
expected_versionyou sent is stale, and nothing changed.details[0].current_versionhas the current version. Read the category again and decide whether the change still applies. - StatusHTTP 404What to doNo category has the
category_id, or, on a product, bundle, promotion, return reason, or return policy write, no category has a handle you sent.paramnames the entry.
Webhook events#
- category.
created A category was created. - category.
updated Its name, description,external_reference_id, or metadata changed. - category.
deleted The category was deleted. The payload carries its last snapshot withstatus: deleted, since it can no longer be read.
A rename sends only category.updated, even though categories[].name changes on every product and bundle that carries the category. Assigning or removing a product or bundle changes the category's counts and version without a category event. Keep another system in sync covers mirroring categories and backfilling them from the lists, and the catalog events list every copied field that changes without its own event.
The Category object#
A catalog grouping used to organize items.
Returned by GET /v1/categories/{category_id} and carried by the endpoints below.
Attributes
assigned_bundle_countintegerRequiredNumber of bundles currently assigned to the category.
assigned_product_countintegerRequiredNumber of products currently assigned to the category.
category_idstringRequiredStable identifier of the category.
created_atstringRequiredWhen this resource was created, as an RFC3339 timestamp.
descriptionstringDescription of the category.
external_reference_idstringCaller-owned identifier for this resource in an external system.
handlestringRequiredImmutable handle used to assign this category to products and bundles.
merchant_idstringRequiredIdentifier of the merchant that owns this resource.
metadatamap of stringCaller-owned metadata stored on this resource.
namestringRequiredCategory name shown to buyers.
statusenumRequiredCategory lifecycle status. Active categories can be assigned; deleted is returned only by DELETE.
activedeleted
targeting_reference_countintegerRequiredNumber of promotions, return reasons and return policy versions that reference this category. Deleting the category is refused with `409 RESOURCE_IN_USE` while this or `assigned_product_count` or `assigned_bundle_count` is above zero.
updated_atstringRequiredWhen this resource last changed, as an RFC3339 timestamp.
versionintegerRequiredPersisted owned-state version. Clock-based availability and expanded resources do not advance this counter.
{
"assigned_bundle_count": 1,
"assigned_product_count": 2,
"category_id": "ctg_01J00000000000000000000000",
"created_at": "2026-03-17T14:30:00Z",
"handle": "events",
"merchant_id": "mer_01KEZF7C00MPP0T9S0KH5BB4CC",
"metadata": {
"department": "admissions"
},
"name": "Events",
"status": "active",
"targeting_reference_count": 0,
"updated_at": "2026-03-17T14:30:00Z",
"version": 1
}List categories#
/v1/categoriesRequires scope commerce.catalog.read or commerce.catalog.write
Rate limit: read
List categories. Default order: handle ascending; this key uniquely orders the results.
Query parameters
page_sizeintegerMaximum number of results to return, from 1 to 100. Defaults to 20.
page_tokenstringOpaque next_page_token from the previous response. Omit on the first request. You may change `page_size` between pages. Reuse it with the same endpoint, filters, sort, and request context. Do not parse or construct tokens. An invalid, expired, or mismatched token returns a validation error.
handlestringExact handle match. Handles are lowercase, so matching is case-sensitive with no normalization; a value that isn't a valid handle returns 400 INVALID_REQUEST.
external_reference_idstringExact-match filter on the caller-owned external reference ID.
querystringSearch across category ID, external reference ID, handle, name, and description. Text fields match any part of the value, and %, _ and \ are ordinary characters, not wildcards. IDs match from the start and need the type prefix, such as ctg_01.
sort_byenumField to sort by. Defaults to handle; ties are broken by category_id.
handlecreated_atupdated_at
sort_directionenumSort direction. Defaults to asc.
ascdesc
created_afterstringInclusive lower bound on created_at.
created_beforestringExclusive upper bound on created_at.
updated_afterstringInclusive lower bound on updated_at.
updated_beforestringExclusive upper bound on updated_at.
Response · 200
A page of categories.
dataarray of objectRequiredThe requested category data.
assigned_bundle_countintegerRequiredNumber of bundles currently assigned to the category.
assigned_product_countintegerRequiredNumber of products currently assigned to the category.
category_idstringRequiredStable identifier of the category.
created_atstringRequiredWhen this resource was created, as an RFC3339 timestamp.
descriptionstringDescription of the category.
external_reference_idstringCaller-owned identifier for this resource in an external system.
handlestringRequiredImmutable handle used to assign this category to products and bundles.
merchant_idstringRequiredIdentifier of the merchant that owns this resource.
metadatamap of stringCaller-owned metadata stored on this resource.
namestringRequiredCategory name shown to buyers.
statusenumRequiredCategory lifecycle status. Active categories can be assigned; deleted is returned only by DELETE.
activedeleted
targeting_reference_countintegerRequiredNumber of promotions, return reasons and return policy versions that reference this category. Deleting the category is refused with `409 RESOURCE_IN_USE` while this or `assigned_product_count` or `assigned_bundle_count` is above zero.
updated_atstringRequiredWhen this resource last changed, as an RFC3339 timestamp.
versionintegerRequiredPersisted owned-state version. Clock-based availability and expanded resources do not advance this counter.
metaobjectAdditional information about this response.
api_versionstringAPI version used to produce this response.
idempotency_replayedbooleanWhether this response replays a stored result for the supplied Idempotency-Key.
trace_idstringTrace identifier to include when contacting Flint about this request.
warningsarray of objectConditions that need attention even though the request succeeded.
next_page_tokenstringToken for the next page. Omitted when there are no more results.
request_idstringRequiredIdentifier of this API request, useful when contacting support.
curl https://api.withflintpay.com/v1/categories \
-H "Flint-Version: 2026-09-07" \
-H "Authorization: Bearer YOUR_API_KEY"A page of categories.
{
"data": [
{
"assigned_bundle_count": 1,
"assigned_product_count": 2,
"category_id": "ctg_01J00000000000000000000000",
"created_at": "2026-03-17T14:30:00Z",
"handle": "events",
"merchant_id": "mer_01KEZF7C00MPP0T9S0KH5BB4CC",
"metadata": {
"department": "admissions"
},
"name": "Events",
"status": "active",
"targeting_reference_count": 0,
"updated_at": "2026-03-17T14:30:00Z",
"version": 1
}
],
"meta": {
"api_version": "2026-09-07"
},
"next_page_token": "example",
"request_id": "req_01KWM6TG3MDX7N5YDMNCXPRQ0Q"
}Create category#
/v1/categoriesIdempotentRequires scope commerce.catalog.write
Rate limit: write
Creates a reusable category. If handle is omitted, Flint derives it from the name and never changes it on rename.
Request body
descriptionstringDescription of the category.
external_reference_idstringCaller-owned identifier for this resource in an external system.
handlestringUnique category handle, using lowercase letters, digits, and single hyphens between words. Cannot be changed after creation.
metadatamap of stringCaller-owned metadata. Values can't be empty strings. Keys starting with flint_ are reserved for Flint, are not returned, and cannot be written through the public API.
namestringRequiredCategory name shown to buyers.
Response · 201
The created category.
dataobjectRequiredThe requested category data.
assigned_bundle_countintegerRequiredNumber of bundles currently assigned to the category.
assigned_product_countintegerRequiredNumber of products currently assigned to the category.
category_idstringRequiredStable identifier of the category.
created_atstringRequiredWhen this resource was created, as an RFC3339 timestamp.
descriptionstringDescription of the category.
external_reference_idstringCaller-owned identifier for this resource in an external system.
handlestringRequiredImmutable handle used to assign this category to products and bundles.
merchant_idstringRequiredIdentifier of the merchant that owns this resource.
metadatamap of stringCaller-owned metadata stored on this resource.
namestringRequiredCategory name shown to buyers.
statusenumRequiredCategory lifecycle status. Active categories can be assigned; deleted is returned only by DELETE.
activedeleted
targeting_reference_countintegerRequiredNumber of promotions, return reasons and return policy versions that reference this category. Deleting the category is refused with `409 RESOURCE_IN_USE` while this or `assigned_product_count` or `assigned_bundle_count` is above zero.
updated_atstringRequiredWhen this resource last changed, as an RFC3339 timestamp.
versionintegerRequiredPersisted owned-state version. Clock-based availability and expanded resources do not advance this counter.
metaobjectAdditional information about this response.
api_versionstringAPI version used to produce this response.
idempotency_replayedbooleanWhether this response replays a stored result for the supplied Idempotency-Key.
trace_idstringTrace identifier to include when contacting Flint about this request.
warningsarray of objectConditions that need attention even though the request succeeded.
request_idstringRequiredIdentifier of this API request, useful when contacting support.
curl -X POST https://api.withflintpay.com/v1/categories \
-H "Flint-Version: 2026-09-07" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: a-unique-key" \
-d '{
"handle": "events",
"name": "Events"
}'Get category#
/v1/categories/{category_id}Requires scope commerce.catalog.read or commerce.catalog.write
Rate limit: read
Returns one category by ID, with its handle, name, and the counts of products and bundles assigned to it. A deleted category fails with `RESOURCE_NOT_FOUND`.
Path parameters
category_idstringRequiredFlint category ID.
curl https://api.withflintpay.com/v1/categories/ctg_01KEZF7C001T4WWBPVGWRZD88Q \
-H "Flint-Version: 2026-09-07" \
-H "Authorization: Bearer YOUR_API_KEY"The requested category.
{
"data": {
"assigned_bundle_count": 1,
"assigned_product_count": 2,
"category_id": "ctg_01J00000000000000000000000",
"created_at": "2026-03-17T14:30:00Z",
"handle": "events",
"merchant_id": "mer_01KEZF7C00MPP0T9S0KH5BB4CC",
"metadata": {
"department": "admissions"
},
"name": "Events",
"status": "active",
"targeting_reference_count": 0,
"updated_at": "2026-03-17T14:30:00Z",
"version": 1
},
"meta": {
"api_version": "2026-09-07"
},
"request_id": "req_01KWM615AGC7G311TCBK6QE1Q8"
}Update category#
/v1/categories/{category_id}IdempotentRequires scope commerce.catalog.write
Rate limit: write
Updates a category's name, description, `external_reference_id`, or metadata. Only the fields you send change; the handle stays the same. Send `expected_version` to reject the update with `VERSION_CONFLICT` when the category changed since you read it.
Path parameters
category_idstringRequiredFlint category ID.
Request body
descriptionstring or nullSend null to clear it.
expected_versionintegerCategory version last read by the caller. Optional for updates.
external_reference_idstring or nullCaller-owned identifier for this resource in an external system. Send null to clear it.
metadatamap of string or nullCaller-owned metadata. Omit this field to leave metadata unchanged. Send an object to merge by key, set a key to null to remove it, or set metadata to null to clear all caller-owned metadata. An empty object makes no change. Values can't be empty strings. Keys starting with flint_ are reserved for Flint, are not returned, and cannot be written through the public API. This operation also limits the metadata object you send, before merging: at most 50 keys, counting null deletions, each key 1 to 256 Unicode code points, and at most 32768 UTF-8 bytes of keys plus values.
namestringCategory name shown to buyers.
curl -X PATCH https://api.withflintpay.com/v1/categories/ctg_01KEZF7C001T4WWBPVGWRZD88Q \
-H "Flint-Version: 2026-09-07" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: a-unique-key" \
-d '{
"expected_version": 1,
"name": "Seasonal favorites"
}'Delete category#
/v1/categories/{category_id}IdempotentRequires scope commerce.catalog.write
Rate limit: write
Permanently deletes an unreferenced category and returns it with status deleted. Deleted categories cannot be retrieved or listed. Remove all product, bundle, promotion, and return configuration references before deleting.
Path parameters
category_idstringRequiredFlint category ID.
Query parameters
expected_versionintegerOptional resource version last read by the caller.
curl -X DELETE https://api.withflintpay.com/v1/categories/ctg_01KEZF7C001T4WWBPVGWRZD88Q \
-H "Flint-Version: 2026-09-07" \
-H "Authorization: Bearer YOUR_API_KEY"Was this helpful?
