Create, update, and delete products

These endpoints cover a product's write lifecycle: creation, update (partial or full), and deletion. SKU renaming has no endpoint of its own — it's triggered implicitly on update. Deletion is permanent.

Required permissions: products:create to create, products:update to update, products:delete to delete.

Create a product

Creates a new product in the authenticated tenant

skustringrequired

SKU unique within the tenant. Normalized to UPPERCASE on save.

titleobjectrequired

{ value: string, translations?: [] } — value max 500 characters.

pricenumberrequired

Base price, minimum 0.

currencystringrequired

ISO 4217 currency code.

statusstringrequired

active, disabled, or draft.

compareAtPricenumber

Compare-at price. If sent, must be GREATER than price — otherwise the save fails.

productTypestring

physical, digital, service, bundle, supply, or material. See the type vs productType note in the data model.

variantsarray

Array of variants. Maximum 100 on an update.

mediaarray

Array of images/media. See Variants and media.

bindingsarray

Sales channel links. Maximum 50 on an update.

{
  "sku": "CAM-ROJO-M",
  "title": { "value": "Camisa Roja Talla M" },
  "price": 499.00,
  "currency": "MXN",
  "status": "active",
  "productType": "physical"
}
201
{
  "id": "65f3a1b2c4d5e6f7a8b9c0e1",
  "sku": "CAM-ROJO-M",
  "status": "active",
  "productType": "physical",
  "title": { "value": "Camisa Roja Talla M", "translations": [] },
  "price": 499.00,
  "currency": "MXN",
  "taxable": true,
  "media": [],
  "variants": [],
  "bindings": []
}
409
{
  "code": "duplicate-sku",
  "message": "Product with SKU 'CAM-ROJO-M' already exists",
  "field": "sku"
}

Permission required: products:create

Required fields

FieldTypeDescription
skustringMaximum 100 characters. Unique per tenant (composite index tenantId + sku).
title.valuestringMaximum 500 characters.
pricenumberMinimum 0.
currencystringISO 4217 code.
statusstringactive, disabled, or draft — see the note on the real enum below.

The real status has 3 values, not 5

The persisted (Mongoose) schema only accepts active, disabled, and draft, with default draft. If your integration comes from another source that assumes the 5 values of the public TS type (active/disabled/inactive/draft/out_of_stock), be careful: inactive and out_of_stock are rejected on save.

Example

curl -X POST https://api.fenicia.io/products \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "sku": "CAM-ROJO-M",
    "title": { "value": "Camisa Roja Talla M" },
    "price": 499.00,
    "currency": "MXN",
    "status": "active",
    "productType": "physical"
  }'

Update a product

Updates an existing product, partially or fully

idstringrequired

_id or SKU of the product to update.

{
  "price": 549.00,
  "status": "active"
}
200
{
  "id": "65f3a1b2c4d5e6f7a8b9c0e1",
  "sku": "CAM-ROJO-M",
  "status": "active",
  "price": 549.00,
  "currency": "MXN",
  "...": "resto de campos del producto actualizado"
}
404
{ "code": "not-found", "message": "Product not found" }
409
{ "code": "duplicate-sku", "message": "Product with SKU 'CAM-ROJO-MED' already exists", "field": "sku" }

Permission required: products:update

The body accepts any subset of the product's fields — you don't need to resend the full object. Two special behaviors of the validation library:

  • compareAtPrice: null is interpreted as "clear the field" explicitly, distinct from simply omitting it.
  • Array size limits: variants.length ≤ 100, bindings.length ≤ 50.

A legacy variant also exists: PUT /products (without {id} in the route, with the SKU inside the body). Same permission and same behavior — documented here for completeness, but use PUT /products/{id} for new integrations.

SKU renaming is implicit

There is no dedicated endpoint to rename a SKU

To change a product's SKU, send the new value in the sku field of PUT /products/{id}'s body. If body.sku differs from the stored SKU, the lambda internally triggers a cascading rename — there is no separate route like POST /products/{id}/rename.

The cascading rename updates, in the same operation: the associated inventory records, inventory_tracks, references from BOM/bundle/modifiers that point to the old SKU, and it emits the ProductSkuChanged event. Treat it as a higher-impact operation than a simple field update — verify the new SKU doesn't collide with an existing one (it will respond 409 duplicate-sku if it does).

curl -X PUT https://api.fenicia.io/products/CAM-ROJO-M \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "sku": "CAM-ROJO-MED" }'

Delete a product

Permanently deletes a product

idstringrequired

_id or SKU of the product to delete.

200
{ "success": true, "message": "Product deleted" }
404
{ "code": "not-found", "message": "Product not found" }

Permission required: products:delete

Irreversible hard delete — no trash, no soft-delete

This endpoint executes a direct deleteOne against the MongoDB collection. There is no recycle bin nor a recoverable state. Once deleted, the product and its history stop existing in the database — you cannot restore it from the API.

This is different from setting status: 'disabled', which is a reversible logical state (the product still exists, it's just hidden/deactivated). If your flow needs the ability to "undo", use PUT /products/{id} with {"status": "disabled"} instead of DELETE.

curl -X DELETE https://api.fenicia.io/products/CAM-ROJO-M \
  -H "Authorization: Bearer fkapi_your_api_key"

Errors

CodeStatusDescription
bad-request/missing-body400The request did not include a body.
bad-request/invalid-json400The body is not valid JSON.
validation-error400/409The product does not meet the validation rules. Includes fieldErrors[] with per-field detail.
duplicate-sku409A product with that SKU already exists in the tenant (on create, or when renaming into an occupied SKU).
not-found404No product resolves the given id/SKU (on PUT, only when the identifier is an _id that doesn't resolve; on DELETE, whenever it doesn't exist).

Authentication codes (invalid token, missing permission) are the same across the whole platform — see the full error catalog.

Next steps