Skip to content

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#

ResourceFieldEffect
Products and bundlescategory_handlesAssigns up to 100 categories
Product and bundle listscategory_handle queryLists one category's items
Promotionsline_item.categories ruleMatches lines in the category
Return reasonscategory_handlesOffers the reason for those lines only
Return policiesscope.category_handlesCovers 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.

  • handle has 1 to 100 lowercase letters and digits, with single hyphens between words, such as espresso-drinks. No two of your categories share one. It never changes, so store it in your code, rules, and filters.
  • Omit handle on create and Flint derives it from name: letters are lowercased, accents are dropped, and each run of other characters becomes one hyphen, so Café & Bakery becomes cafe-bakery. A name with no Latin letters or digits, such as 茶, produces no handle, so send handle with it.
  • name has 1 to 100 characters after Flint trims surrounding spaces, and the handle derived from it must also fit in 100 characters. Renaming with PATCH /v1/categories/{category_id} changes only name.
  • 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
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" }'
Response
{
  "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 category
Final values do not change again
  • active
    Every category you can read. Products and bundles can be assigned to it, and promotions and return configuration can target it.
  • deletedFinal
    Returned only by DELETE and in category.deleted. The category is gone: GET returns 404 and 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#

  • HTTP 409
    On create, another category already has the handle, including one derived from name. details[0].blocking_resources names that category. Use it, or send a different handle.
  • HTTP 409
    On delete, a product, bundle, promotion, return reason, or return policy revision still uses the category. details[0].blocking_resources names the referencing resources. Remove references where possible; a historical policy revision's reference keeps the category from being deleted.
  • HTTP 409
    VERSION_CONFLICTRefresh, then retry
    The expected_version you sent is stale, and nothing changed. details[0].current_version has the current version. Read the category again and decide whether the change still applies.
  • HTTP 404
    No category has the category_id, or, on a product, bundle, promotion, return reason, or return policy write, no category has a handle you sent. param names 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 with status: 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_countintegerRequired

Number of bundles currently assigned to the category.

assigned_product_countintegerRequired

Number of products currently assigned to the category.

category_idstringRequired

Stable identifier of the category.

created_atstringRequired

When this resource was created, as an RFC3339 timestamp.

descriptionstring

Description of the category.

external_reference_idstring

Caller-owned identifier for this resource in an external system.

handlestringRequired

Immutable handle used to assign this category to products and bundles.

merchant_idstringRequired

Identifier of the merchant that owns this resource.

metadatamap of string

Caller-owned metadata stored on this resource.

namestringRequired

Category name shown to buyers.

statusenumRequired

Category lifecycle status. Active categories can be assigned; deleted is returned only by DELETE.

  • active
  • deleted
targeting_reference_countintegerRequired

Number 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_atstringRequired

When this resource last changed, as an RFC3339 timestamp.

versionintegerRequired

Persisted owned-state version. Clock-based availability and expanded resources do not advance this counter.

JSON
{
  "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#

GET/v1/categories

Requires 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_sizeinteger

Maximum number of results to return, from 1 to 100. Defaults to 20.

page_tokenstring

Opaque 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.

handlestring

Exact 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_idstring

Exact-match filter on the caller-owned external reference ID.

querystring

Search 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_byenum

Field to sort by. Defaults to handle; ties are broken by category_id.

  • handle
  • created_at
  • updated_at
sort_directionenum

Sort direction. Defaults to asc.

  • asc
  • desc
created_afterstring

Inclusive lower bound on created_at.

created_beforestring

Exclusive upper bound on created_at.

updated_afterstring

Inclusive lower bound on updated_at.

updated_beforestring

Exclusive upper bound on updated_at.

Response · 200

A page of categories.

dataarray of objectRequired

The requested category data.

metaobject

Additional information about this response.

next_page_tokenstring

Token for the next page. Omitted when there are no more results.

request_idstringRequired

Identifier 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.

JSON
{
  "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#

POST/v1/categoriesIdempotent

Requires 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

descriptionstring

Description of the category.

external_reference_idstring

Caller-owned identifier for this resource in an external system.

handlestring

Unique category handle, using lowercase letters, digits, and single hyphens between words. Cannot be changed after creation.

metadatamap of string

Caller-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.

namestringRequired

Category name shown to buyers.

Response · 201

The created category.

dataobjectRequired

The requested category data.

metaobject

Additional information about this response.

request_idstringRequired

Identifier 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#

GET/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_idstringRequired

Flint category ID.

Response · 200

The requested category.

Same response as Create category.

curl https://api.withflintpay.com/v1/categories/ctg_01KEZF7C001T4WWBPVGWRZD88Q \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY"

The requested category.

JSON
{
  "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#

PATCH/v1/categories/{category_id}Idempotent

Requires 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_idstringRequired

Flint category ID.

Request body

descriptionstring or null

Send null to clear it.

expected_versioninteger

Category version last read by the caller. Optional for updates.

external_reference_idstring or null

Caller-owned identifier for this resource in an external system. Send null to clear it.

metadatamap of string or null

Caller-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.

namestring

Category name shown to buyers.

Response · 200

The updated category.

Same response as Create category.

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#

DELETE/v1/categories/{category_id}Idempotent

Requires 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_idstringRequired

Flint category ID.

Query parameters

expected_versioninteger

Optional resource version last read by the caller.

Response · 200

The deleted category.

Same response as Create category.

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?