Bulk operations — rollback, Flow Editor, AI, and categorization

Four endpoint families related by a common thread: they all operate on many products at once and can all end in partial success. This covers the recording and rollback of bulk operations, the visual rules editor (Flow Editor), AI-assisted transformations, and product categorization for a marketplace.

207 Multi-Status appears across several of these endpoints

POST /products/marketplace-categories, the synchronous branch of CSV import, POST /products/bulk/flow/execute, and POST /products/bulk/ai/transform can respond with 207 Multi-Status when some elements of the operation succeeded and others failed. If your client only distinguishes "2xx = all good," you will treat a partially-failed result as total success — always check the per-element detail of the response.


Bulk operations log

Every bulk operation that modifies products (CSV, Flow Editor, AI) gets recorded as a "bulk operation" — that's what builds the history and enables rollback.

Lists the tenant's bulk operations history.

pagenumber

Page number.

limitnumber

Items per page.

typestring

Filters by operation type (csv-import, flow, ai-transform, etc.).

statusstring

Filters by operation status.

sortBystring

Sort field.

sortOrderstring

asc or desc.

200
{
  "operations": [],
  "totalCount": 0,
  "page": 0,
  "limit": 20
}

Required permission: products:read and products:update (both, stacked)

This endpoint requires two permissions at once, not just one

Unlike most read endpoints in this API, GET /products/bulk/operations and GET /products/bulk/operations/{id} require both products:read and products:update simultaneously — a read-only API key cannot list the bulk operations history. This is a confirmed RBAC inconsistency in the code, not necessarily the intentional design; we document the actual behavior. The rollback endpoint (below), by contrast, requires only products:update.


Detail of a bulk operation

Returns the detail of a bulk operation, including whether it can still be rolled back.

idstringrequired

Operation ID (path).

200
{
  "id": "bulkop_65f3a1b2c4d5e6f7a8b9c0f1",
  "type": "flow",
  "status": "completed",
  "canRollback": true
}
404
{ "code": "not-found", "message": "Bulk operation not found" }

Required permission: products:read and products:update (both, stacked)

Shape partially confirmed

It was confirmed that the response carries the full operation plus the canRollback field. The rest of the fields in the example (id, type, status) are indicative based on the rest of the bulk operations family — confirm them against your own response.


Roll back a bulk operation

Reverts the changes applied by a previous bulk operation, when it's still reversible.

idstringrequired

ID of the operation to roll back (path).

{}
200
{
  "success": true,
  "restoredCount": 298,
  "failedSkus": ["PAN-AZUL-32"]
}
400
{ "code": "rollback-failed", "message": "This operation can no longer be rolled back" }

Required permission: products:update

Tip

Check canRollback on GET /products/bulk/operations/{id} before attempting the rollback — an operation may stop being reversible (for example, if a later operation already modified the same SKUs).


Flow Editor (visual/no-code bulk editing)

The Flow Editor lets you define bulk edit rules without writing code (a structured FlowDefinition). The three endpoints follow the validate → preview (dry-run) → execute pattern.

Validate a flow definition

Validates the structure of a FlowDefinition without executing it or touching any product.

flowobjectrequired

Flow definition to validate.

{
  "flow": {
    "filter": { "status": "draft" },
    "actions": [{ "type": "set-field", "field": "status", "value": "active" }]
  }
}
200
{
  "valid": true,
  "errors": [],
  "warnings": []
}
400
{ "code": "bad-request", "message": "Missing required field: flow" }

Required permission: products:update

Preview a flow (dry-run)

Simulates the execution of a flow over a sample of products, without persisting changes.

flowobjectrequired

Flow definition to preview.

limitnumber

Maximum number of products to consider in the simulation. Default: 100.

sampleSizenumber

How many example changes to return. Default: 10.

{
  "flow": {
    "filter": { "status": "draft" },
    "actions": [{ "type": "set-field", "field": "status", "value": "active" }]
  },
  "limit": 100,
  "sampleSize": 10
}
200
{
  "affectedCount": 340,
  "sampleChanges": [
    { "sku": "CAM-ROJO-M", "before": { "status": "draft" }, "after": { "status": "active" } }
  ]
}

Required permission: products:update

Execute a flow

Executes a FlowDefinition over the products it resolves, applying the changes. Can also run in dryRun mode.

flowobjectrequired

Flow definition to execute.

dryRunboolean

If true, simulates without persisting. Default: false.

limitnumber

Limit of products to process.

batchSizenumber

Processing batch size. Default: 100.

operationNamestring

Visible name in the bulk operations history.

{
  "flow": {
    "filter": { "status": "draft" },
    "actions": [{ "type": "set-field", "field": "status", "value": "active" }]
  },
  "operationName": "Activar borradores de temporada"
}
200Total success.
{
  "totalProcessed": 340,
  "succeeded": 340,
  "failed": 0,
  "errors": []
}
207Partial success — review errors[].
{
  "totalProcessed": 340,
  "succeeded": 335,
  "failed": 5,
  "errors": [
    { "sku": "PAN-AZUL-32", "message": "validation-error" }
  ]
}

Required permission: products:update

Result shape indicative

It was confirmed that this endpoint can respond 200 (total success) or 207 (partial). The exact names of the body fields (totalProcessed, succeeded, failed, errors) were not verified line by line — use them as a guide and confirm them against your own execution. The operation gets recorded and is queryable/reversible via the bulk operations log described above.

curl -X POST https://api.fenicia.io/products/bulk/flow/execute \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "flow": {
      "filter": { "status": "draft" },
      "actions": [{ "type": "set-field", "field": "status", "value": "active" }]
    },
    "operationName": "Activar borradores de temporada"
  }'

AI Mutations (AI-assisted transformation)

Applies a natural language prompt over a set of products to transform specific fields (for example, rewriting descriptions or generating marketplace bullet points).

Transform products with AI

Applies an AI prompt over a batch of products to transform specific fields.

promptstringrequired

Natural language instruction for the model.

productsobject[]required

Products to transform, each with at least its sku.

targetFieldsstring[]

Fields the model should modify.

modelstring

claude or gpt. Default: claude.

options.maxTokensPerRequestnumber

Default: 4096.

options.batchSizenumber

Default: 10.

options.temperaturenumber

Default: 0.3.

options.stopOnErrorboolean

Stops the whole batch at the first error. Default: false.

{
  "prompt": "Reescribe la descripción con un tono más comercial, máximo 200 caracteres",
  "products": [{ "sku": "CAM-ROJO-M" }, { "sku": "PAN-AZUL-32" }],
  "targetFields": ["description"],
  "model": "claude",
  "options": { "batchSize": 10, "temperature": 0.3 }
}
200Total success.
{
  "totalProcessed": 25,
  "succeeded": 25,
  "failed": 0,
  "results": [
    { "sku": "CAM-ROJO-M", "success": true, "changedFields": ["description"] }
  ]
}
207Partial success.
{
  "totalProcessed": 25,
  "succeeded": 22,
  "failed": 3,
  "results": [
    { "sku": "PAN-AZUL-32", "success": false, "error": "MODEL_ERROR" }
  ]
}
429
{ "code": "budget-exceeded", "message": "Daily AI token budget exceeded for this tenant" }

Required permission: products:update

Result shape indicative

The top-level wrapper (200/207 depending on partial success) is confirmed; the exact names of fields like results[], changedFields were not verified line by line — treat them as indicative.

The AI budget is daily and per tenant

Before your prompt gets processed, the server validates against a daily tenant token budget. If you've already exhausted it, the request fails with 429 budget-exceeded without even attempting the transformation — check GET /products/bulk/ai/budget before launching a large batch.

Check today's AI budget

Returns the tenant's AI token consumption for the current day.

200
{
  "tenantId": "65f2a0b1c4d5e6f7a8b9c0aa",
  "date": "2026-07-21",
  "used": 812400,
  "limit": 2000000,
  "remaining": 1187600,
  "percentUsed": 40.62,
  "isOverLimit": false,
  "isWarning": false
}

Required permission: products:read

Check the AI usage history

Returns the AI token consumption history for the last days.

daysnumber

Days back to include. Default: 30.

200
{
  "history": [
    { "date": "2026-07-20", "tokensUsed": 640200, "requestCount": 48 }
  ]
}

Required permission: products:read

Shape of each history[] entry indicative

The { history: TokenUsage[] } wrapper was confirmed; the exact fields of each TokenUsage (here date, tokensUsed, requestCount) were not verified line by line.


Marketplace categorization

Assigns the category of a specific marketplace (MercadoLibre, T1/Sears/Sanborns, Amazon, Walmart, etc.) to a batch of SKUs. The result is saved as a product metafield — it doesn't create a new field in the data model.

Assigns a marketplace category to a batch of SKUs for a specific channel.

skusstring[]required

SKUs to categorize. Maximum 100 per request.

channelIdstringrequired

ID of the target channel/marketplace.

category.idstringrequired

Category ID in the marketplace. Maximum 100 characters.

category.namestringrequired

Category name. Maximum 255 characters.

category.pathstring[]

Hierarchical category path. Maximum 10 levels.

{
  "skus": ["CAM-ROJO-M", "CAM-AZUL-L", "CAM-VERDE-S"],
  "channelId": "65f2a0b1c4d5e6f7a8b9c0ab",
  "category": {
    "id": "MLM1055",
    "name": "Camisas",
    "path": ["Ropa", "Hombre", "Camisas"]
  }
}
200Total success — all SKUs were categorized.
{
  "success": true,
  "total": 3,
  "categorized": 3,
  "failed": 0,
  "channelType": "mercadolibre",
  "siteId": "MLM",
  "category": { "id": "MLM1055", "name": "Camisas", "path": ["Ropa", "Hombre", "Camisas"] },
  "results": [
    { "sku": "CAM-ROJO-M", "success": true },
    { "sku": "CAM-AZUL-L", "success": true },
    { "sku": "CAM-VERDE-S", "success": true }
  ]
}
207Partial success — check results[] to know which SKU failed and why.
{
  "success": false,
  "total": 3,
  "categorized": 2,
  "failed": 1,
  "channelType": "mercadolibre",
  "siteId": "MLM",
  "category": { "id": "MLM1055", "name": "Camisas", "path": ["Ropa", "Hombre", "Camisas"] },
  "results": [
    { "sku": "CAM-ROJO-M", "success": true },
    { "sku": "CAM-AZUL-L", "success": true },
    { "sku": "CAM-VERDE-S", "success": false, "error": "not-found" }
  ]
}
400
{ "code": "too-many-skus", "message": "skus exceeds the maximum of 100" }

Required permission: products:update

Where the result is stored

The assigned category is persisted in product.metafields[], under namespace: 'mkt_category' and key: '{channelType}:{siteId}' — it isn't a top-level product field. The siteId depends on the channel type: MercadoLibre uses site_id (default MLM), T1/Sears/Sanborns use salesChannel/marketplace (default SR), Amazon uses marketplace_id (default MX), and Walmart uses country (default MX).

curl -X POST https://api.fenicia.io/products/marketplace-categories \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "skus": ["CAM-ROJO-M", "CAM-AZUL-L", "CAM-VERDE-S"],
    "channelId": "65f2a0b1c4d5e6f7a8b9c0ab",
    "category": {
      "id": "MLM1055",
      "name": "Camisas",
      "path": ["Ropa", "Hombre", "Camisas"]
    }
  }'

Errors

CodeStatusDescription
not-found404No bulk operation exists with that id, or the SKU doesn't exist (categorization).
rollback-failed400The operation can no longer be rolled back (the rollback window expired or it was already rolled back).
bad-request400A required field is missing (flow in Flow Editor, prompt/products without SKU in AI Mutations).
budget-exceeded429The tenant's daily AI token budget was exhausted.
validation-errorGeneric code used inside errors[]/results[] for per-element validation failures.
invalid-skus400The skus field is invalid or empty.
too-many-skus400skus exceeds the maximum of 100 per request.
invalid-sku-values400One or more values within skus are invalid.
invalid-channel-id400channelId is invalid or doesn't correspond to a tenant channel.
invalid-category400The category object is invalid or incomplete.
invalid-category-path400category.path has an invalid format.
category-id-too-long400category.id exceeds 100 characters.
category-name-too-long400category.name exceeds 255 characters.
category-path-too-deep400category.path exceeds 10 levels.
auth:invalid_token401The API key is invalid or has been revoked.
auth:permission_denied403The API key doesn't have the required permission (products:read, products:update, or both depending on the endpoint).

See the full error catalog for the rest of the possible codes.

Next steps