Products API
The Products API covers a tenant's full catalog: search and listing, product detail, create/edit/delete, variants, media, automotive compatibility (fitment), materials/supplies/BOM/modifiers (manufacturing and POS), bulk operations (rollback, visual flow editing, AI mutations), CSV import/export, categorization, and publishing to marketplaces, collections, and categories.
Read this before any other article in this section. Unlike other domains in this API, there is no single response contract here — that's the single most important finding in the whole domain, and it shapes how you should read every article that follows.
Domain architecture
"Products" in this API is actually three independent lambdas behind three separate route bases, each with its own authorization scheme:
| Route base | Covers |
|---|---|
/products/... | Catalog, detail, CRUD, variants, media, fitment, materials/BOM, bulk, CSV, channel publishing |
/collections/... | Product collections (manual/dynamic groupings) |
/categories/... | Own category tree + AI-assisted classification for SAT, T1, Walmart, TikTok, and mappings to marketplaces |
All of them require authentication; each lambda's internal routing is done by its own code via path/method, not by API Gateway.
Base URL and authentication
https://api.fenicia.ioThere is no version prefix (/v1) in the URL. Every request requires an API key sent as a Bearer token:
Authorization: Bearer fkapi_your_api_keyTip
Store your API key in an environment variable (FENICIA_API_KEY) and never include it directly in source code.
⚠️ There is no single response envelope
In the Orders domain, every response follows {data, meta} / {error}. That is not true in Products. Only one endpoint in the whole domain uses that envelope; the rest respond with ad-hoc shapes per endpoint — some wrapped, some not wrapped at all, some as a flat array.
Verify the shape in each article — never assume it
There is no general rule you can apply to a new endpoint in this domain. The response shape is documented case by case in each article of this section.
Real examples, so you can calibrate the variety:
| Endpoint (example) | 200/201 response shape |
|---|---|
GET /products?availableAt=<locationId> | { "data": Product[], "meta": { "pagination": {...} } } — the only case with this envelope |
GET /products (without availableAt) | Product[] — flat array, no wrapper (legacy shape) |
GET /products/count | { "count": number } or { "count": number, "searchStrategy": string } if there's a term |
GET /products/exist/{sku} | { "exist": boolean } |
POST /products/query | { "products": [...], "total": number, "page": number, "limit": number } |
GET /products/{id} | Product — direct object, no wrapper |
POST /products / PUT /products/{id} | Product — direct object, no wrapper (201 / 200) |
DELETE /products/{id} | { "success": true, "message": "<string>" } |
| Async operations (CSV, channel export, scheduled bulk) | 202 + { "jobId": "..." } (or { "scheduleId": "..." }) |
| Bulk operations with partial success | 207 + endpoint-specific result, typically with a results[]/errors[] array per item |
{
"data": [
{ "sku": "CAM-ROJO-M", "title": { "value": "Camisa Roja Talla M" }, "price": 499.0, "status": "active" }
],
"meta": {
"pagination": { "page": 0, "limit": 20, "total": 143, "totalPages": 8, "hasMore": true }
}
}[
{ "sku": "CAM-ROJO-M", "title": { "value": "Camisa Roja Talla M" }, "price": 499.0, "status": "active" }
]See Catalog and search for the full detail on GET /products and its two shapes.
Pagination
page and limit are the pagination parameters across the entire listing surface. The base is consistent (page starts at 0), but the default and ceiling of limit vary by endpoint — there is no single global value:
page: integer, base0across all listing endpoints.limit: the most common default inlambda-productsis20; the underlying library's validator (validatePagination) uses50as a generic default and applies a ceiling of500on the vast majority of routes.- Ceiling exception: async CSV export uses
EXPORT_MAX_LIMIT = 10000, far above the rest of the domain.
Confirm the exact default and maximum in each endpoint's article before assuming them — Catalog and search documents those of GET /products, and Bulk CSV documents those of import/export.
Projection with extend
GET /products/{id} (and, at the library level, the rest of the listing endpoints) supports ?extend= to request fields that don't come by default in the base projection (the default response weighs ~3 KB per product; without extend, several heavy fields are intentionally omitted).
extend value | What it adds |
|---|---|
variants | Full variants array |
options | Variation options (size, color, etc.) |
description | Long / HTML description |
bindings | Sales channel links |
metadata | Metafields |
inventory | Inventory per variant and location (live join against the inventory collection) |
dimensions | Shipping dimensions |
seo | metaTitle, metaDescription, slug |
all | Everything above |
You can combine several comma-separated values: ?extend=variants,inventory,seo.
`embeddings` exists at the library level, not confirmed on this endpoint
The underlying library defines embeddings as an additional extendable field (alongside the ones in the table). This audit did not confirm that GET /products/{id} accepts it in its query parsing — don't assume it without testing first.
Full response detail with each value in Retrieve a product.
Permissions
The permission catalog lives in @fenicia/core. Each route base uses its own namespace, with one notable exception:
PRODUCTS.READ products:read PRODUCTS.CREATE products:create
PRODUCTS.UPDATE products:update PRODUCTS.DELETE products:delete
PRODUCTS.IMPORT products:import PRODUCTS.EXPORT products:export
PRODUCTS.SYNC products:sync PRODUCTS.MANAGE products:manage
PRODUCTS.ALL products:*COLLECTIONS.* follows the same pattern (READ/CREATE/UPDATE/DELETE) for /collections/....
/categories/... does NOT have its own permission namespace
Despite being a separate lambda and route base, /categories/... reuses PERMISSIONS.PRODUCTS.* (mostly READ and MANAGE) plus SETTINGS.READ/SETTINGS.MANAGE for its stats and cache-administration endpoints. There is no PERMISSIONS.CATEGORIES.*.
Stacked RBAC — some routes require two permissions, not one
Most endpoints validate a single permission. A subset requires two, stacked, and there is a documented asymmetry between dev and production:
| Route | Required permission(s) |
|---|---|
/products/export* | PRODUCTS.EXPORT + PRODUCTS.READ |
GET /products/bulk/operations* | PRODUCTS.READ + PRODUCTS.UPDATE |
GET /products/bulk/schedule* (dev only, not yet in master) | PRODUCTS.UPDATE alone — does not require READ, unlike the rest of the domain's GET routes |
Don't assume "one permission per route" when integrating: confirm in the specific article (Channel publishing, Bulk operations) whether the route you're calling requires more than one.
Four /categories/... routes without explicit requirePermission()
walmart/unified-classify, cache/lookup, cache/stats, and cache/invalidate only validate that a tenantId exists in the session — they do not check an explicit RBAC permission, unlike almost the rest of the file. This could be intentional (internal use) or an oversight; treat it as a known quirk, not as the domain's general pattern.
Irreversible operations
Deleting a product is a hard delete
DELETE /products/{id} permanently removes the document from the database — there is no trash can or soft-delete at this endpoint's level. status: 'disabled' is an independent logical state and not equivalent to deleting it. Before calling this endpoint from your integration, confirm you really want an unrecoverable deletion. Detail in Manage products.
Known limits of this API version
- No granular endpoints for attributes/custom fields, SEO, or price schemas: all of that is read via
?extend=but only written by resending the full product payload viaPUT /products/{id}. See Manage products. - No endpoints to delete or reorder media: the full
media[]array is rewritten viaPUT /products/{id}. See Variants and media. - No HTTP "bundle" entity exists: the only product-of-products composition mechanism with its own endpoints is BOM (oriented to manufacturing/consumption).
bundleConfigis a data sub-shape managed inside the product payload, with no/products/{sku}/bundleroutes. See Data model and Materials and BOM.
Quick example
curl "https://api.fenicia.io/products?limit=5" \
-H "Authorization: Bearer $FENICIA_API_KEY"Section map
| Article | Content |
|---|---|
| Catalog and search | GET /products, search, count, available filters. |
| Retrieve a product | Detail by ID/SKU, ?extend=, related products, a product's variants. |
| Data model | Real (Mongoose) shape of Product and ProductVariant, divergences from the public TS type. |
| Manage products | Create, update, delete, SKU rename. |
| Variants and media | Update fields on an existing variant; upload images/media. |
| Fitment compatibility | Catalogs and compatibility nodes for the automotive vertical. |
| Materials and BOM | Materials, supplies, bill of materials (BOM), and food-delivery style modifiers. |
| Bulk operations | Bulk operation rollback, visual flow editor, AI mutations. |
| Bulk CSV | CSV import/export, sync and async, templates. |
| Channel publishing | Marketplace categorization, export/publishing to channel, T1 enrichment. |
| Collections | Product groupings (/collections/...). |
| Categories | Own tree, SAT/T1/Walmart/TikTok classification, mappings to marketplaces (/categories/...). |
| Error catalog | All known error codes in the domain, grouped by their real origin. |