Skip to content

Productos

CRUD sobre los productos de tu catálogo (servicios, productos simples, con variantes, compuestos). Todas las requests requieren Authorization: Bearer finova_sk_<secret>.

El objeto Product

json
{
  "id":                "65a0c0f1234567890abcdef0",
  "name":              "Lentes oftálmicos premium",
  "slug":              "lentes-oftalmicos-premium",
  "sku":               "LEN-PREM-001",
  "description":       "Lentes graduados con armazón de titanio.",
  "product_type":      "simple",
  "is_active":         true,
  "tags":              ["lentes", "premium"],
  "category_name":     "Lentes",
  "product_category": {
    "id":   "65c2b2c234567890abcdef22",
    "name": "Lentes",
    "slug": "lentes"
  },
  "brand": {
    "id":   "65b1a1b234567890abcdef00",
    "name": "Polaroid",
    "slug": "polaroid"
  },
  "gtin":              "0049000028911",
  "mpn":               "POL-PREM-12345",
  "condition":         "new",
  "prices_cents":      { "mxn": 450000, "usd": 25000 },
  "inventory_enabled": false,
  "inventory_mode":    "materials",
  "taxable":           true,
  "tax_rate_percent":  16.0,
  "image_url":         "https://cdn.fi-nova.com/cdn/products/65a0…/image?v=9f3ac1b204",
  "thumbnail_url":     "https://cdn.fi-nova.com/cdn/products/65a0…/image?v=9f3ac1b204",
  "images": [
    { "url": "https://cdn.fi-nova.com/cdn/products/65a0…/image?v=9f3ac1b204", "alt": "Lentes oftálmicos premium", "position": 0 },
    { "url": "https://cdn.fi-nova.com/…/1.jpg", "alt": "Vista lateral", "position": 1 }
  ],
  "material":    "Titanio",
  "graduado":    true,
  "voltaje":     110,
  "colores":     ["rojo", "azul"],
  "dimensiones": { "alto_mm": 40, "ancho_mm": 140 },
  "ficha_tecnica": { "asset_id": "65e…", "url": "https://cdn.fi-nova.com/…/ficha.pdf", "name": "Ficha técnica", "content_type": "application/pdf" },
  "seo": {
    "seo_title":               "Lentes oftálmicos premium — Óptica XYZ",
    "seo_title_translations":  { "es": "Lentes oftálmicos premium — Óptica XYZ", "en": "Premium prescription glasses — XYZ Optics" },
    "meta_description":              "Lentes premium con armazón de titanio. Envío gratis.",
    "meta_description_translations": { "es": "Lentes premium…", "en": "Premium glasses…" },
    "meta_keywords":              "lentes, premium, titanio",
    "meta_keywords_translations": { "es": "lentes, premium", "en": "glasses, premium" },
    "short_description":              "Resumen breve para listados.",
    "short_description_translations": { "es": "Resumen breve…", "en": "Short summary…" },
    "description":              "<p>Contenido HTML largo…</p>",
    "description_translations": { "es": "<p>…</p>", "en": "<p>…</p>" },
    "og_title":               "Lentes premium",
    "og_title_translations":  { "es": "Lentes premium", "en": "Premium glasses" },
    "og_description":              "Compártelos en redes sociales.",
    "og_description_translations": { "es": "Compártelos…", "en": "Share them…" },
    "indexable": true,
    "og_image":  { "url": "https://cdn.fi-nova.com/…/og.jpg", "alt_translations": { "es": "Lentes premium" } },
    "banner":    { "url": "https://cdn.fi-nova.com/…/banner.jpg" },
    "video":     {
      "provider":      "youtube",
      "video_id":      "dQw4w9WgXcQ",
      "url":           "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "embed_url":     "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ",
      "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
      "content_type":  "text/html"
    },
    "gallery":   [{ "url": "https://cdn.fi-nova.com/…/1.jpg", "alt_translations": {} }],
    "og_image_url": "https://cdn.fi-nova.com/…/og.jpg",
    "banner_url":   "https://cdn.fi-nova.com/…/banner.jpg",
    "gallery_urls": ["https://cdn.fi-nova.com/…/1.jpg"]
  },
  "variants": [
    {
      "id":          "65c2d2c345678901abcdef00",
      "name":         "Armazón rojo / Talla M",
      "sku":          "LEN-PREM-R-M",
      "prices_cents": { "mxn": 470000 },
      "is_active":    true,
      "options":     { "color": "red", "size": "M" },
      "image_url":   "https://cdn.fi-nova.com/cdn/products/65a0…/variants/65c2…/image?v=41b7de0c88"
    }
  ],
  "option_types": [
    {
      "name": "color", "presentation": "Color", "position": 1,
      "values": [
        { "name": "Rojo", "value": "red", "presentation": "Rojo", "position": 1, "extra_price_cents": 0 }
      ]
    }
  ],
  "components": [
    { "id": "65c2…", "name": "Estuche rígido", "component_product_id": "65aa…", "quantity": 1, "is_required": true, "extra_prices_cents": { "mxn": 0 } }
  ],
  "inventory": {
    "enabled":       true,
    "unit":          "pcs",
    "on_hand_qty":   "120.0",
    "reserved_qty":  "8.0",
    "available_qty": "112.0",
    "by_location": [
      {
        "warehouse_id":   "65d0…", "warehouse_name": "Bodega Central",
        "rack_id":        "65d1…", "rack_name":      "A-12",
        "on_hand_qty":    "120.0", "reserved_qty":   "8.0", "available_qty": "112.0"
      }
    ]
  },
  "created_at":        "2026-01-15T14:23:11Z",
  "updated_at":        "2026-05-14T09:01:42Z"
}

Lista vs detalle

variants, option_types, components e inventory solo se incluyen en GET /products/:id (detalle). El listado (GET /products) omite estos campos para mantener cada fila ligera, pero sí incluye image_url, thumbnail_url e images.

URLs de imagen permanentes

image_url, thumbnail_url, las URLs en images[], variants[].image_url y las de seo (og:image, banner, galería) son públicas y permanentes: no llevan firma ni expiración, así que puedes guardarlas, ponerlas en tu sitemap, en un feed de Merchant Center o en <meta property="og:image"> sin que se rompan.

El parámetro ?v= es un identificador del archivo almacenado: se mantiene igual mientras la imagen no cambie y se renueva solo cuando se sube otra, para que ningún caché sirva la versión vieja.

thumbnail_url apunta a la imagen completa (no se genera un derivado); redimensiona del lado de tu sitio.

Cambio respecto a versiones anteriores

Estas URLs antes eran firmadas (?exp=…&sig=…) y expiraban en horas, lo que rompía la indexación en buscadores. Las ligas firmadas que ya tengas publicadas siguen funcionando (los parámetros se ignoran), pero lee el producto una vez para quedarte con la URL permanente.

Campos

CampoTipoDescripción
idstringObjectId.
namestringNombre comercial.
slugstringIdentificador amigable opcional. Define la URL pública del producto en tu sitio.
skustringSKU principal (para simple / service).
descriptionstringDescripción libre (no localizada). Para el contenido SEO localizado usa seo.description.
product_typeenumsimple, service, variable, composite.
is_activeboolSi está visible/comprable.
tagsstring[]Etiquetas libres.
category_namestring(deprecado, pero presente) Nombre de la categoría de catálogo (denormalizado). Usa product_category para la referencia estructurada.
product_categoryobject{ id, name, slug } — referencia denormalizada a la categoría de catálogo. Edita la categoría completa en /product-categories. Ver Categorías de producto. No confundir con la categoría financiera de ingresos/egresos.
brandobject{ id, name, slug } — referencia denormalizada. Edita la marca completa en /brands.
gtinstringGTIN / EAN / UPC / ISBN (8, 12, 13 o 14 dígitos). Útil para Google Merchant.
mpnstringManufacturer Part Number.
conditionenumnew, used, refurbished, damaged. Default new.
prices_centsobjectMapa multi-moneda { <iso4217_lowercase>: <centavos> }, ej. { "mxn": 450000, "usd": 25000 }. Reemplaza a los antiguos currency/price_cents escalares.
inventory_enabledboolSi lleva control de inventario.
inventory_modeenummaterials, self, hybrid.
taxableboolSi causa impuesto.
tax_rate_percentfloatTasa de impuesto (ej. 16.0).
image_urlstring | nullURL pública y permanente de la imagen principal. null si no hay imagen.
thumbnail_urlstring | nullIgual que image_url (no se genera un derivado; redimensiona en tu sitio).
imagesobject[]Galería plana: { url, alt, position }. La imagen principal va en position: 0, seguida de la galería SEO.
(campos personalizados)anyCada campo personalizado se devuelve aplanado como una clave de primer nivel del producto (ej. "voltaje": 110), no dentro de un array custom_properties. Ver Campos personalizados.
seoobjectContenido localizado + multimedia para tu sitio público. Ver El objeto SEO.
variantsobject[](solo detalle) Variantes del producto. Ver Variantes, opciones y componentes.
option_typesobject[](solo detalle) Tipos de opción (ej. color, talla) y sus valores.
componentsobject[](solo detalle) Componentes de un producto composite.
inventoryobject | null(solo detalle) Disponibilidad: total y desglose por almacén/rack. null si el producto no lleva inventario. Nunca incluye costos.
created_at / updated_atstringISO 8601 UTC.

Datos no expuestos. Por privacidad, el API público nunca devuelve cifras de venta (sold_units, sold_amount), costos fijos (fixed_costs) ni la lista de materiales/costos (material_requirements). El objeto inventory expone solo cantidades disponibles, no valores monetarios.

El objeto SEO

Anidado bajo seo. Diseñado para alimentar el <head> y el cuerpo de la página pública del producto en tu sitio.

CampoTipoDescripción
seo_titlestringSEO title resuelto al locale actual.
seo_title_translationsobject{ "es": "…", "en": "…" }.
meta_description / _translationsstring / objectMeta description para buscadores.
meta_keywords / _translationsstring / objectKeywords (legacy SEO, opcional).
short_description / _translationsstring / objectResumen corto para listados.
description / _translationsstring / objectContenido largo (HTML permitido).
og_title / _translationsstring / objectOpen Graph title para redes sociales.
og_description / _translationsstring / objectOpen Graph description.
indexableboolfalse → emite <meta name="robots" content="noindex,nofollow">.
og_imageobject{ asset_id, url, name, content_type, size_bytes, file_filename, alt_translations }.
bannerobjectMismo shape que og_image.
videoobjectVideo del producto. Puede ser un archivo subido a Finova o una liga de YouTube/Vimeo. Ver abajo. Útil para schema.org/Product.video.

El objeto video

Además de los campos de og_image, el slot de video trae:

CampoTipoDescripción
providerenumfile (archivo subido a Finova), youtube, vimeo o external (otra liga).
video_idstring | nullId del video en el proveedor. null para file / external.
urlstringLiga canónica: la URL del CDN para file, o la del proveedor (https://www.youtube.com/watch?v=…, https://vimeo.com/…).
embed_urlstring | nullLiga lista para un <iframe>. YouTube usa el dominio -nocookie. null para file / external.
thumbnail_urlstring | nullMiniatura del proveedor cuando existe (YouTube).

Con provider: "file", url apunta al CDN de Finova y content_type es el del archivo (video/mp4, video/webm). | gallery | object[] | Array con el mismo shape que og_image. | | og_image_url / banner_url / gallery_urls | string / string[] | Legacy URL-only. Espejos del campo enriquecido — preferí los objetos. |

Al escribir vía POST o PATCH, pasa el objeto seo como hijo de data. Los campos localizados se escriben vía *_translations (un hash { locale: valor }). Pasar null/string vacío en una traducción borra esa entrada.

Campos personalizados (specs)

Los campos personalizados son atributos estructurados definidos por el usuario — pensados para la tabla de "Especificaciones técnicas" de la ficha del producto (dimensiones, material, voltaje, etc.).

En la lectura se devuelven aplanados: cada campo aparece como una clave de primer nivel del producto, con su valor directo.

json
{
  "id": "65a0…",
  "name": "Lentes oftálmicos premium",
  "voltaje": 110,
  "material": "Titanio",
  "colores": ["rojo", "azul"],
  "ficha_tecnica": { "asset_id": "65e…", "url": "https://…/ficha.pdf", "name": "Ficha técnica" }
}

Claves reservadas

La key es un identificador estilo variable (minúsculas, números y guion bajo; sin espacios ni acentos). Si una clave personalizada coincide con un campo propio del recurso (id, name, slug, sku, prices_cents, seo, brand, …) se descarta al serializar: un campo personalizado nunca puede sobrescribir ni duplicar un campo real. Elige claves propias (ej. voltaje_maximo, material_marco).

En la escritura se pasan como el array custom_properties dentro de data. Cada entrada lleva:

CampoTipoDescripción
keystringRequerido. Identificador del atributo. Las entradas con key vacío se descartan.
typeenumCómo interpretar value (ver abajo). Si se omite, se infiere del value.
valueanyEl dato; su forma depende de type.

Tipos (type) y la forma de value:

typevalue
stringTexto.
numberEntero o decimal.
booleantrue / false.
arrayLista de escalares (["rojo", "azul"]).
objectObjeto JSON ({ "alto_mm": 40 }).
object_arrayLista de objetos.
fileUn media item: { asset_id, url, name, content_type, size_bytes, file_filename, alt_translations } — mismo shape que la multimedia de SEO.
file_arrayLista de media items.

Es reemplazo completo: el array que mandas sustituye al anterior; un array vacío [] los borra; omitir la clave los deja intactos. Los valores se sanitizan en el servidor (estructuras anidadas se limpian a JSON seguro; máximo 200 propiedades).

json
{
  "data": {
    "custom_properties": [
      { "key": "material", "type": "string", "value": "Titanio" },
      { "key": "voltaje", "type": "number", "value": 110 },
      { "key": "ficha_tecnica", "type": "file", "value": { "asset_id": "65e…", "url": "https://…/ficha.pdf", "name": "Ficha técnica" } }
    ]
  }
}

Variantes, opciones y componentes

Estas colecciones se leen en el detalle del producto y se escriben (crear/actualizar) anidadas dentro de data, con semántica de nested attributes:

  • Crear un item: inclúyelo sin id.
  • Actualizar un item existente: inclúyelo con su id (los demás items no enviados quedan intactos).
  • Eliminar un item: envíalo con { "id": "…", "_destroy": true }.

Los id se preservan entre actualizaciones, por lo que la imagen de una variante (ligada a su id) sobrevive a las ediciones.

variants (para product_type: "variable")

CampoTipoDescripción
idstringOmitir para crear.
namestringNombre de la variante.
skustringSKU de la variante.
prices_centsobjectMapa multi-moneda { <iso4217>: <centavos> }. Si se omite (o falta una moneda), hereda el precio del producto en esa moneda.
is_activeboolVisibilidad.
optionsobjectMapa { tipo: valor }, ej. { "color": "red", "size": "M" }.
image_urlstring(solo lectura) Súbela con el endpoint de imagen de variante.

option_types

CampoTipoDescripción
namestringClave interna (ej. color).
presentationstringEtiqueta para UI (ej. Color).
positionintegerOrden.
valuesobject[]{ id?, name, value, presentation, position, extra_price_cents, _destroy? }.

components (para product_type: "composite")

CampoTipoDescripción
idstringOmitir para crear.
namestringNombre del componente.
component_product_idstringProducto referenciado (opcional).
quantityintegerCantidad incluida.
is_requiredboolSi es obligatorio.
extra_prices_centsobjectMapa multi-moneda { <iso4217>: <centavos> } con el ajuste de precio del componente.
json
{
  "data": {
    "name": "Playera premium",
    "product_type": "variable",
    "prices_cents": { "mxn": 30000 },
    "option_types": [
      { "name": "color", "presentation": "Color", "position": 1,
        "values": [{ "name": "Rojo", "value": "red", "presentation": "Rojo", "position": 1 }] }
    ],
    "variants": [
      { "name": "Rojo / M", "sku": "PLY-R-M", "prices_cents": { "mxn": 32000 }, "options": { "color": "red", "size": "M" } }
    ]
  }
}

Listar productos

GET/api/v1/productsread-products

Lista paginada de los productos de tu empresa. Ordenada por created_at descendente (más nuevos primero). Para catálogos grandes (≥ 5 000 productos) usa cursores; ver la guía de paginación para detalles.

Query params

ParamDefaultDetalle
limit25Items por página (máx 100).
search (alias q)Búsqueda full-text/autocomplete sobre name, slug, sku y description. Devuelve hasta los 100 resultados más relevantes, luego paginados.
brand (alias brand_id)Filtra por marca. Un solo valor o lista separada por comas mezclando ObjectIds y slugs (ej. acme,5f...). Trae los productos de cualquiera de las marcas listadas. Slugs matchean el snapshot denormalizado del producto (sin lookup extra). Un id/slug desconocido no aporta resultados.
category (alias product_category_id)Filtra por categoría de catálogo. Un solo valor o lista separada por comas de ObjectIds y/o slugs (ej. comida,bebidas). Trae los productos de cualquiera de las categorías listadas. El filtro es jerárquico: filtrar por una categoría padre también trae los productos asignados a cualquiera de sus subcategorías (a cualquier profundidad); filtrar por una subcategoría no trae los productos del padre. Ej.: si el producto A está en 1.1 (hija de 1), category=1 y category=1.1 traen A; pero un producto B en 1 no aparece con category=1.1. Un id/slug desconocido no aporta resultados.
activetrueFiltra por visibilidad pública. Por default solo trae productos activos (is_active: true). active=false trae solo los inactivos; active=all trae ambos. Cualquier otro valor equivale al default (solo activos).
starting_afterCursor — id del último item de la página previa.
ending_beforeCursor — id del primer item de la página previa (retrocede).
include_totalfalseSi es true, agrega total y total_pages a meta. Cuenta todo el catálogo, ignora los filtros (costo extra).
include_filtered_totalfalseSi es true, agrega filtered_total y filtered_total_pages a meta con el conteo del resultado ya filtrado (search/q, brand, category, active). Acotado a 10000 (si lo supera, se satura ahí y filtered_total_capped viene en true); con search/q el tope es 100.
page1Legacy offset. Máx 200 — más profundo devuelve 400 offset_too_deep.

El meta siempre incluye has_more, next_cursor y previous_cursor. total/total_pages solo con include_total; filtered_total/filtered_total_pages (y filtered_total_capped) solo con include_filtered_total. Ver la guía de paginación para la diferencia entre ambos totales.

bash
# Primera página
curl 'https://developers.fi-nova.com/api/v1/products?limit=25' \
  -H 'Authorization: Bearer finova_sk_TU_SECRETO'

# Siguiente página (usar next_cursor de la respuesta anterior)
curl 'https://developers.fi-nova.com/api/v1/products?limit=25&starting_after=65a0c0f1234567890abcdef0' \
  -H 'Authorization: Bearer finova_sk_TU_SECRETO'

# Filtrar por categoría (slug) + marca, con conteo del resultado filtrado
curl 'https://developers.fi-nova.com/api/v1/products?category=comida,bebidas&brand=acme&include_filtered_total=true' \
  -H 'Authorization: Bearer finova_sk_TU_SECRETO'

# Incluir también los productos inactivos (no visibles en el sitio)
curl 'https://developers.fi-nova.com/api/v1/products?active=all' \
  -H 'Authorization: Bearer finova_sk_TU_SECRETO'
javascript
let cursor = null;
do {
  const url = new URL('https://developers.fi-nova.com/api/v1/products');
  url.searchParams.set('limit', '100');
  if (cursor) url.searchParams.set('starting_after', cursor);

  const res  = await fetch(url, { headers: { Authorization: 'Bearer finova_sk_TU_SECRETO' } });
  const body = await res.json();
  // ...do something with body.data...
  cursor = body.meta.has_more ? body.meta.next_cursor : null;
} while (cursor);
ruby
cursor = nil
loop do
  uri = URI('https://developers.fi-nova.com/api/v1/products')
  uri.query = URI.encode_www_form({ limit: 100, starting_after: cursor }.compact)
  req = Net::HTTP::Get.new(uri, 'Authorization' => 'Bearer finova_sk_TU_SECRETO')
  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
  body = JSON.parse(res.body)
  # ...do something with body['data']...
  break unless body.dig('meta', 'has_more')
  cursor = body.dig('meta', 'next_cursor')
end

Obtener un producto

GET/api/v1/products/:idread-products

Devuelve un único producto. El segmento :id acepta el id (ObjectId), el slug o el sku del producto — se resuelven en ese orden y siempre dentro de tu empresa. Útil para URLs amigables (/catalog/lentes-premium) sin tener que listar y filtrar. Ver Identificar un recurso.

bash
curl https://developers.fi-nova.com/api/v1/products/lentes-premium \
  -H 'Authorization: Bearer finova_sk_TU_SECRETO'

Si ningún identificador coincide (o pertenece a otra empresa), recibes 404 not_found. La respuesta incluye los campos de detalle (variants, option_types, components, inventory) además de los de la lista.


Crear un producto

POST/api/v1/productscreate-products

Campos aceptados en data

name, slug, sku, description, product_type, is_active, product_category_id, brand_id, gtin, mpn, condition, prices_cents (mapa multi-moneda { <iso4217>: <centavos> }), inventory_enabled, inventory_mode, taxable, tax_rate_percent, tags, un objeto anidado seo (ver El objeto SEO), el array custom_properties (ver Campos personalizados), y las colecciones anidadas variants, option_types y components (ver Variantes, opciones y componentes).

Para asignar (o reasignar) la categoría de catálogo de un producto, manda product_category_id con el id de una categoría de la misma empresa (ver Categorías de producto). Mándalo vacío (""/null) para quitar la categoría. Un product_category_id que no exista o pertenezca a otra empresa devuelve 422 invalid_product_category. El campo category_name está denormalizado y se recalcula solo a partir de product_category_id; mandarlo no tiene efecto (se conserva en la respuesta solo por compatibilidad).

Los campos company_id, owner_id se asignan automáticamente a partir de la API key autenticada. brand_name y brand_slug se sincronizan automáticamente desde el Brand referenciado por brand_id. Las imágenes (binarios) no se mandan en el JSON: súbelas con los endpoints de imagen.

bash
curl -X POST https://developers.fi-nova.com/api/v1/products \
  -H 'Authorization: Bearer finova_sk_TU_SECRETO' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: prod-import-row-42' \
  -d '{
    "data": {
      "name": "Lentes oftálmicos premium",
      "sku":  "LEN-PREM-001",
      "product_type": "simple",
      "prices_cents": { "mxn": 450000, "usd": 25000 },
      "is_active":    true,
      "taxable":      true,
      "tax_rate_percent": 16
    }
  }'
javascript
await fetch('https://developers.fi-nova.com/api/v1/products', {
  method: 'POST',
  headers: {
    Authorization:    'Bearer finova_sk_TU_SECRETO',
    'Content-Type':   'application/json',
    'Idempotency-Key': 'prod-import-row-42',
  },
  body: JSON.stringify({
    data: {
      name: 'Lentes oftálmicos premium',
      sku:  'LEN-PREM-001',
      product_type: 'simple',
      prices_cents: { mxn: 450000, usd: 25000 },
      is_active:    true,
      taxable:      true,
      tax_rate_percent: 16,
    },
  }),
});

Actualizar un producto

PATCH/api/v1/products/:idupdate-products

Acepta los mismos campos que create. Solo se actualizan los que mandes — los demás quedan intactos.

bash
curl -X PATCH https://developers.fi-nova.com/api/v1/products/65a0c0f1234567890abcdef0 \
  -H 'Authorization: Bearer finova_sk_TU_SECRETO' \
  -H 'Content-Type: application/json' \
  -d '{ "data": { "prices_cents": { "mxn": 480000 } } }'

Eliminar un producto

DELETE/api/v1/products/:iddelete-products

Borra el producto. Devuelve 204 No Content. Los productos relacionados con ventas o movimientos de inventario existentes NO se pueden eliminar — recibirás 422 validation_failed y debes archivarlos desde la UI.


Imágenes

Las imágenes son binarios y se gestionan con endpoints dedicados (multipart/form-data), no en el JSON del producto. El campo del archivo se llama file. Tras subir, el producto reflejará image_url (y la entrada en images[]); las variantes reflejarán variants[].image_url.

Las URLs devueltas son públicas y permanentes; el ?v= cambia solo cuando reemplazas la imagen, así que puedes cachearlas indefinidamente.

Imagen principal del producto

POST/api/v1/products/:id/imageupdate-products
DELETE/api/v1/products/:id/imageupdate-products
bash
# Subir / reemplazar
curl -X POST https://developers.fi-nova.com/api/v1/products/65a0…/image \
  -H 'Authorization: Bearer finova_sk_TU_SECRETO' \
  -F 'file=@./lentes.jpg'

# Eliminar
curl -X DELETE https://developers.fi-nova.com/api/v1/products/65a0…/image \
  -H 'Authorization: Bearer finova_sk_TU_SECRETO'

Respuesta: { "data": { "id": "65a0…", "image_url": "https://…" } } (o image_url: null al eliminar).

Formatos aceptados: jpg, jpeg, png, webp, gif. Un archivo inválido devuelve 422 invalid_file; sin archivo, 422 missing_file.

Imagen de una variante

POST/api/v1/products/:id/variants/:variant_id/imageupdate-products
DELETE/api/v1/products/:id/variants/:variant_id/imageupdate-products

Igual que la imagen principal, pero apuntando a una variante existente. Si la variante no existe (o pertenece a otro producto), recibes 404 not_found. La imagen sigue a la variante por su id, así que se conserva al editar el producto.

bash
curl -X POST https://developers.fi-nova.com/api/v1/products/65a0…/variants/65c2…/image \
  -H 'Authorization: Bearer finova_sk_TU_SECRETO' \
  -F 'file=@./variante-roja.jpg'

Respuesta: { "data": { "id": "65a0…", "variant_id": "65c2…", "image_url": "https://…" } }.


Errores específicos

errorCuándo
validation_failed (422)El payload no pasa validaciones (precio negativo, SKU duplicado, etc.).
not_found (404)El id (o variant_id) no existe o pertenece a otra empresa.
invalid_product_category (422)El product_category_id no existe o pertenece a otra empresa.
missing_file (422)Endpoint de imagen sin archivo file.
invalid_file (422)Imagen con formato no permitido.
insufficient_scope (403)La API key no tiene el scope requerido por el endpoint.

Hecho con cuidado por Finova.