Product Category Enrichment API
AI-powered enrichment that generates SEO content for every product category — page titles, meta descriptions, keywords, HTML descriptions, URL slugs, and more.
Overview
The Product Category Enrichment API enriches product categories with structured SEO attributes that you define under Settings → Enrichment → Category Attributes. Each job uses your active attributes (or a specific Attribute Set), analyses the products linked to each category, and generates one value per attribute per category.
If no custom attributes are configured, the API falls back to six built-in SEO defaults: page_title, meta_description, meta_keywords, description (HTML), slug, and focus_keywords.
The API accepts rows of category data directly as JSON objects — no CSV file upload is required at the API level.
Endpoints Overview
| Method | Endpoint | Description |
|---|---|---|
| POST | /product-category-jobs/create |
Create a batch enrichment job |
| POST | /product-category-jobs/get |
List enrichment jobs |
| POST | /product-category-jobs/{job_id}/items/get |
Get a job's enriched results |
| POST | /product-category-jobs/{job_id}/stop |
Stop a running job |
| GET | /product-category-jobs/{job_id}/download |
Download job results as CSV |
All endpoints are prefixed with /api/{organization}.
Create a Batch Enrichment Job
Start a job to enrich multiple product categories in parallel. The job runs asynchronously — rows are processed concurrently (up to 5 at a time) in the background.
Endpoint: POST /api/{organization}/product-category-jobs/create
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
rows |
object[] | Yes | — | Array of category records. Each row must include a name, category_name, or categoryName key. 1–2,000 rows. |
inputColumns |
string[] | Yes | — | List of column names present in rows |
fileName |
string | No | — | Display name shown in the jobs list (max 255 characters, recommended to end with .csv) |
modelType |
string | No | budget-v1 |
AI model to use: budget-v1 or pro-v1 |
attributeSetId |
string | No | — | ID of a specific Attribute Set; omit to use all active attributes |
useWebsearch |
boolean | No | false |
Allow the AI to search the web for categories that have no linked products |
includeExistingAttributes |
boolean | No | true |
Pass the latest completed attribute values for each category to the model. Set false to regenerate from scratch. |
Model Types
| Value | Description |
|---|---|
budget-v1 |
Cost-effective, fast. Recommended for bulk runs. |
pro-v1 |
Higher quality. Best for flagship category pages. |
Limits
| Limit | Value |
|---|---|
| Max rows per job | 2,000 |
| Concurrency | 5 categories at a time |
Note: Only one enrichment job can run at a time per organisation. If a job is already
pendingorprocessing, the request returns an error asking you to wait.
Category Name Column Detection
The category name is detected from the row using the first non-blank match from these keys, in order:
name → category_name → categoryName
Enrichment Modes
Each category is processed in one of two modes chosen automatically at runtime:
| Mode | Triggered when | Behaviour |
|---|---|---|
products |
Category has ≥ 1 linked product in your catalogue | AI uses up to 10 sampled products as grounding; no web search |
websearch |
Category has no linked products and useWebsearch is true |
AI performs live web searches and fetches category pages |
If a category has no linked products and useWebsearch is false, that item fails with the error: "No products are linked to this category. Link products via Enrich Category on a product, or enable "Use Websearch" before enriching."
Example Request
curl -X POST https://catalog-ai.tdcapps.com/api/your-org/product-category-jobs/create \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{ "name": "Running Shoes", "slug": "running-shoes" },
{ "name": "Trail Running", "slug": "trail-running" }
],
"inputColumns": ["name", "slug"],
"fileName": "categories.csv",
"modelType": "budget-v1",
"useWebsearch": false
}'
Response
{
"success": true,
"data": {
"success": true,
"message": "Product-category enrichment job created successfully",
"jobId": "pcej_abc123"
}
}
List Enrichment Jobs
Retrieve a paginated list of product category enrichment jobs for the organisation.
Endpoint: POST /api/{organization}/product-category-jobs/get
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
page |
number | No | 1 |
Page number (min 1) |
limit |
number | No | 20 |
Items per page (max 100) |
Example Request
curl -X POST https://catalog-ai.tdcapps.com/api/your-org/product-category-jobs/get \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"page": 1, "limit": 20}'
Response
{
"success": true,
"data": {
"success": true,
"jobs": [
{
"id": "pcej_abc123",
"status": "completed",
"modelType": "budget-v1",
"fileName": "categories.csv",
"useWebsearch": false,
"attributeSetId": null,
"inputColumns": ["name", "slug"],
"totalCategories": 120,
"processedCount": 120,
"successCount": 118,
"failedCount": 2,
"errorSummary": null,
"startedAt": "2026-05-18T08:00:00Z",
"completedAt": "2026-05-18T08:04:12Z",
"createdAt": "2026-05-18T07:59:55Z",
"createdBy": {
"name": "Aditya",
"email": "aditya@example.com"
}
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 3,
"totalPages": 1
}
}
}
Job Status Values
| Status | Description |
|---|---|
pending |
Job is queued and will start shortly |
processing |
Categories are being enriched right now |
completed |
All categories finished successfully |
completed_with_errors |
Job finished but some categories failed |
failed |
Job failed before processing |
stopped |
Job was manually stopped |
Get Job Items
Retrieve detailed enriched results for a specific job, paginated by row.
Endpoint: POST /api/{organization}/product-category-jobs/{job_id}/items/get
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
job_id |
string | Yes | Job ID |
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
page |
number | No | 1 |
Page number (min 1) |
limit |
number | No | 50 |
Items per page (max 100) |
Example Request
curl -X POST https://catalog-ai.tdcapps.com/api/your-org/product-category-jobs/pcej_abc123/items/get \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"page": 1, "limit": 50}'
Response
{
"success": true,
"data": {
"success": true,
"job": {
"id": "pcej_abc123",
"modelType": "budget-v1",
"fileName": "categories.csv",
"inputColumns": ["name", "slug"],
"totalCategories": 120,
"processedCount": 120,
"successCount": 118,
"failedCount": 2,
"status": "completed",
"errorSummary": null
},
"items": [
{
"id": "item_001",
"jobId": "pcej_abc123",
"rowIndex": 0,
"categoryName": "Running Shoes",
"inputData": {
"name": "Running Shoes",
"slug": "running-shoes"
},
"enrichedAttributes": {
"page_title": {
"attributeId": "default_page_title",
"attributeName": "Page Title",
"attributeType": "text",
"value": "Running Shoes — Shop Road & Trail Runners"
},
"meta_description": {
"attributeId": "default_meta_description",
"attributeName": "Meta Description",
"attributeType": "text",
"value": "Browse our full range of running shoes for road, trail, and track. Find the perfect fit for your stride. Free shipping on orders over £50."
},
"meta_keywords": {
"attributeId": "default_meta_keywords",
"attributeName": "Meta Keywords",
"attributeType": "text",
"value": "running shoes, road running shoes, trail running shoes, mens running shoes, womens running shoes"
},
"description": {
"attributeId": "default_description",
"attributeName": "Description",
"attributeType": "html",
"value": "<div><p>Whether you're training for a marathon...</p></div>"
},
"slug": {
"attributeId": "default_slug",
"attributeName": "URL Slug",
"attributeType": "text",
"value": "running-shoes"
},
"focus_keywords": {
"attributeId": "default_focus_keywords",
"attributeName": "Focus Keywords",
"attributeType": "text",
"value": "running shoes"
}
},
"referenceData": {
"mode": "products",
"productIds": ["prod_001", "prod_002", "prod_003"]
},
"status": "completed",
"cost": 0.0031,
"startedAt": "2026-05-18T08:00:05Z",
"completedAt": "2026-05-18T08:00:11Z"
},
{
"id": "item_002",
"jobId": "pcej_abc123",
"rowIndex": 1,
"categoryName": "Trail Running",
"inputData": {
"name": "Trail Running",
"slug": "trail-running"
},
"referenceData": null,
"status": "failed",
"errorMessage": "No products are linked to this category. Link products via Enrich Category on a product, or enable \"Use Websearch\" before enriching."
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 120,
"totalPages": 3
}
}
}
enrichedAttributes Object
Each key in enrichedAttributes is the attribute slug (e.g. page_title, meta_description). The value object has the following fields:
| Field | Type | Description |
|---|---|---|
attributeId |
string | Internal attribute ID |
attributeName |
string | Human-readable attribute name |
attributeType |
string | One of text, html, number, boolean, select, multiselect |
value |
string | number | boolean | string[] | null | The enriched value |
referenceData Object
| Field | Type | Description |
|---|---|---|
mode |
string | "products" or "websearch" |
productIds |
string[] | (products mode only) IDs of the sampled products used as context |
urls |
string[] | (websearch mode only) URLs fetched during web research |
Item Status Values
| Status | Description |
|---|---|
pending |
Waiting to be processed |
processing |
Currently being enriched |
completed |
Successfully enriched |
failed |
Failed (see errorMessage) |
Stop a Job
Stop a pending or processing job. Categories already enriched keep their results. All remaining pending and processing items are immediately marked as failed with the message "Job was stopped by user".
Endpoint: POST /api/{organization}/product-category-jobs/{job_id}/stop
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
job_id |
string | Yes | ID of the job to stop |
Example Request
curl -X POST https://catalog-ai.tdcapps.com/api/your-org/product-category-jobs/pcej_abc123/stop \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
Response
{
"success": true,
"message": "Product-category enrichment job stopped successfully",
"data": {
"jobId": "pcej_abc123",
"status": "stopped"
}
}
Error Responses
If the job does not exist:
{
"success": false,
"message": "Job not found"
}
If the job is already completed, failed, or stopped:
{
"success": false,
"message": "Job cannot be stopped because it is not in progress"
}
Notes
- Only jobs with status
pendingorprocessingcan be stopped - All
pendingandprocessingitems are immediately markedfailedwith error"Job was stopped by user"
Download Job Results as CSV
Download all enriched rows as a CSV file. The file preserves your original input columns and appends enrichment status and one column per configured attribute.
Endpoint: GET /api/{organization}/product-category-jobs/{job_id}/download
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
job_id |
string | Yes | Job ID |
Example Request
curl -X GET https://catalog-ai.tdcapps.com/api/your-org/product-category-jobs/pcej_abc123/download \
-H "Authorization: Bearer YOUR_API_KEY" \
-o enriched-categories.csv
Response
A text/csv file with Content-Disposition: attachment. The filename format is:
{original_filename_without_extension}_enriched_{YYYY-MM-DD}.csv
Falls back to categories_{first8charsOfJobId}_enriched_{YYYY-MM-DD}.csv when no file name is set.
The header row is structured as:
<your input columns>, Enrichment Status, Enrichment Error, <one column per enriched attribute>
Attribute column notes:
htmltype attributes are written as raw HTML; any other attribute whose value contains HTML tags is automatically stripped to plain textmultiselectvalues are joined with;booleanvalues are written asYes/No- All other types are written as plain strings
nullvalues are written as empty strings
Default SEO Attributes
If your organisation has no custom enrichment attributes configured, the API automatically uses these six built-in defaults:
| Slug | Name | Type | Constraint |
|---|---|---|---|
page_title |
Page Title | text | ≤60 characters, Title Case, keyword near the front |
meta_description |
Meta Description | text | ≤160 characters, benefit-led, includes keyword |
meta_keywords |
Meta Keywords | text | 5–10 comma-separated lowercase keywords |
description |
Description | html | 150–350 words, wrapped in <div>, uses <h2> and <ul> |
slug |
URL Slug | text | kebab-case, 3–5 words, ≤75 characters |
focus_keywords |
Focus Keywords | text | 1–3 word primary keyword, lowercase |
Workflow Example
1. Prepare Category Rows
const categories = [
{ name: 'Running Shoes', slug: 'running-shoes' },
{ name: 'Trail Running', slug: 'trail-running' },
{ name: 'Road Running', slug: 'road-running' }
]
2. Create the Job
const createRes = await fetch(
'https://catalog-ai.tdcapps.com/api/your-org/product-category-jobs/create',
{
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
rows: categories,
inputColumns: ['name', 'slug'],
modelType: 'budget-v1',
useWebsearch: false
})
}
)
const { data } = await createRes.json()
const jobId = data.jobId
3. Poll for Completion
async function waitForJob(jobId) {
while (true) {
const res = await fetch(
'https://catalog-ai.tdcapps.com/api/your-org/product-category-jobs/get',
{
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ page: 1, limit: 100 })
}
)
const { data } = await res.json()
const job = data.jobs.find(j => j.id === jobId)
if (
job &&
['completed', 'completed_with_errors', 'failed', 'stopped'].includes(
job.status
)
) {
return job
}
await new Promise(r => setTimeout(r, 5000))
}
}
const finalJob = await waitForJob(jobId)
4. Fetch Enriched Items
const itemsRes = await fetch(
`https://catalog-ai.tdcapps.com/api/your-org/product-category-jobs/${jobId}/items/get`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ page: 1, limit: 50 })
}
)
const { data } = await itemsRes.json()
console.log(data.items)
5. Or Download the Enriched CSV
const csvRes = await fetch(
`https://catalog-ai.tdcapps.com/api/your-org/product-category-jobs/${jobId}/download`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
)
const csv = await csvRes.text()
fs.writeFileSync('enriched-categories.csv', csv)
Best Practices
- Include a recognised name column — each row must have one of
name,category_name, orcategoryName. - Include
slugas well — if you have it, including the slug gives the AI better context when generating the URL slug attribute. - Link products before enriching — categories with linked products produce richer, more accurate output than web search mode; the AI samples up to 10 products per category.
- Enable
useWebsearchfor unlinked categories — without linked products and with websearch off, those items will fail immediately. - Use
attributeSetId— restrict the job to a named Attribute Set when you only need to refresh specific fields; faster and cheaper. - Stay under 2,000 rows per job — split larger lists into multiple jobs.
- Poll every 5–10 seconds — enrichment is asynchronous; avoid hammering the endpoint.
- Check
failedCountand itemerrorMessage— common failures are missing linked products or credit exhaustion mid-job.
Error Responses
400 Bad Request
Invalid request body or missing required fields:
{
"success": false,
"message": "Invalid request body: ..."
}
401 Unauthorized
Missing API key:
{
"success": false,
"message": "Missing API Key"
}
403 Forbidden
Invalid or revoked API key, or the key does not have access to the specified organisation:
{
"success": false,
"message": "Invalid API Key"
}
409 Conflict — Job Already in Progress
A job is already pending or processing for this organisation:
{
"success": false,
"message": "A product-category enrichment job is already in progress. Please wait for it to complete before starting a new one."
}
402 Payment Required — Credit Balance Exhausted
Triggered at job creation if the credit balance is zero:
{
"success": false,
"message": "Credit limit reached. Please top up your credits to start a new enrichment job."
}
If credits exhaust mid-job, the job continues processing but remaining items fail with the error "Credit balance limit has been reached." The job's errorSummary is set to "Credit balance limit has been reached." Top up and create a new job to continue.
500 Internal Server Error
{
"success": false,
"message": "Internal Server Error"
}