Variants and media

This page covers the only two endpoints with dedicated surface over a product's variants and media: reading/updating an existing variant, and uploading images via a presigned URL. It also explicitly documents what falls outside this surface — because it's more than you'd assume.

Variants

Variants and options summary

Returns a product's variants and options structure

skustringrequired

SKU of the parent product.

200
{
  "sku": "CAM-ROJO-M",
  "variants": [
    {
      "id": "65f3a1b2c4d5e6f7a8b9c0f1",
      "sku": "CAM-ROJO-M-CH",
      "title": { "value": "Camisa Roja - Chica" },
      "price": 499.00,
      "position": 0,
      "options": [{ "id": "opt-talla", "name": "Talla", "value": "CH" }]
    }
  ],
  "options": [{ "id": "opt-talla", "name": "Talla", "values": ["CH", "M", "G"] }],
  "hasVariants": true,
  "hasOptions": true
}
404
{ "code": "not-found", "message": "Product CAM-ROJO-M not found" }

Required permission: products:read

This endpoint is already documented with its full response example in Retrieve a product — it returns {sku, variants[], options[], hasVariants, hasOptions}.

Update fields on an existing variant

Updates one or more fields of an existing variant

productSkustringrequired

SKU of the parent product.

variantSkustringrequired

SKU of the variant to update.

{ "price": 549.00 }
200
{
  "message": "Variant updated",
  "productSku": "CAM-ROJO-M",
  "variantSku": "CAM-ROJO-M-CH",
  "variant": {
    "id": "65f3a1b2c4d5e6f7a8b9c0f1",
    "sku": "CAM-ROJO-M-CH",
    "price": 549.00,
    "position": 0,
    "taxable": true,
    "options": [{ "id": "opt-talla", "name": "Talla", "value": "CH" }]
  }
}
404
{ "code": "not-found", "message": "Product 'CAM-ROJO-M' or variant 'CAM-ROJO-M-CH' not found" }

Required permission: products:update

The request body is a flat object { [field]: value } with any valid variant field (price, title, position, taxable, shipping, minAlertStock, sellIfOutOfStock, countryOfOrigin, etc.).

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

compareAtPrice is NOT validated at the variant level

At the product level, compareAtPrice must be greater than price or the save fails. That same rule does not apply to an individual variant's compareAtPrice — you can save a variant with a compareAtPrice lower than or equal to its price without the request failing.

No create/delete for a variant — only updating an existing one

This endpoint updates fields of a variant that already exists. There is no POST to create a new variant nor a DELETE to remove one. The full structure of the variants[] array (adding, removing, reordering variants) is managed by resending the entire array within the payload of PUT /products/{id}.

Media

Upload an image

Generates a presigned S3 URL to upload a product image

contentTypestringrequired

MIME type of the file. Must be in the list of allowed image types.

filenamestringrequired

File name.

sizenumber

File size in bytes.

{ "contentType": "image/jpeg", "filename": "cam-rojo-m-1.jpg" }
200
{
  "uploadUrl": "https://fenicia-media-uploads.s3.amazonaws.com/...",
  "s3Key": "products/65f2.../cam-rojo-m-1.jpg",
  "publicUrl": "https://cdn.fenicia.io/img/cam-rojo-m-1.jpg",
  "expiresIn": 300
}
400
{
  "code": "bad-request/invalid-content-type",
  "message": "Content type \"application/pdf\" is not allowed. Allowed: JPEG, PNG, GIF, WEBP, MP4, WebM, MOV",
  "parameter": "contentType"
}

Required permission: products:update

This endpoint does not upload the file — it returns a presigned S3 URL (uploadUrl) to which you upload the binary directly via PUT from your client, and a publicUrl which is the one you'll persist in the product's media[].

# 1. Request the presigned URL
curl -X POST https://api.fenicia.io/products/media/upload \
  -H "Authorization: Bearer fkapi_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "contentType": "image/jpeg", "filename": "cam-rojo-m-1.jpg" }'
 
# 2. Upload the binary directly to the returned uploadUrl
curl -X PUT "https://fenicia-media-uploads.s3.amazonaws.com/..." \
  -H "Content-Type: image/jpeg" \
  --data-binary @cam-rojo-m-1.jpg

After uploading the binary, add the entry to the product's media[] array with publicUrl as src, via PUT /products/{id}:

{
  "media": [
    {
      "id": "m2",
      "src": "https://cdn.fenicia.io/img/cam-rojo-m-1.jpg",
      "type": "image",
      "position": 1,
      "alt": { "value": "Camisa roja, vista frontal" }
    }
  ]
}

No endpoint to delete or reorder media

There is no dedicated route to remove an image or change its order. The lambda's source code has explicit TODO comments confirming that supporting this is pending. To delete, reorder, or modify any entry in media[], resend the full media[] array (with the entry removed, reordered, or modified) within the payload of PUT /products/{id}.

Known limitations: what's only managed via full PUT

Several parts of the product model have no granular read/write endpoint of their own — they are managed exclusively by sending the full product (or the relevant subset of fields) to PUT /products/{id}. This is confirmed by explicit TODO comments in the lambda's source code, not an omission of this documentation:

AreaHow it's readHow it's written
Attributes / custom fields (attributes[])As part of the full productResending the full attributes[] via PUT /products/{id}
SEO (seo.metaTitle, metaDescription, slug)?extend=seo on GET /products/{id}Resending seo via PUT /products/{id}/products/{sku}/seo does not exist
Price schemas (priceSchemas[], per-segment/channel pricing)As part of the full productResending the full priceSchemas[] via PUT /products/{id}
Media — create, delete, reorderAs part of the full productResending the full media[] via PUT /products/{id} (uploading the binary does have its own endpoint, see above)

bundleConfig follows the same pattern

There is no "bundle" entity with its own endpoints in the public API. bundleConfig (the components of a bundle-type product) is another sub-shape of Product, also managed within the full payload of PUT /products/{id} — there is no route like /products/{sku}/bundle. The only product-of-products composition mechanism with its own endpoints is BOM (bill of materials), oriented to manufacturing/consumption, not "sale kits".

Errors

CodeStatusDescription
not-found404The specified product or variant doesn't exist.
bad-request400The variant update body is empty or invalid.
bad-request/missing-content-type / bad-request/missing-filename400contentType/filename missing when requesting the upload URL.
bad-request/invalid-media400contentType/size fail file validation (for example, size exceeds the allowed maximum).
bad-request/invalid-content-type400contentType is not in the list of allowed types (JPEG, PNG, GIF, WEBP, MP4, WebM, MOV).
internal-error/media-upload500Internal error generating the presigned URL.

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

Next steps