API ez-catalog

REST + JSON. Todas las rutas viven bajo /v1. Base URL: https://api.ez-catalog.huggian.com

Quick start

  1. Registrate en huggian.com/catalog/signup. El plan Free no pide tarjeta.
  2. Guardá la API key que aparece en pantalla. No se muestra de nuevo. También llega por email.
  3. Mandala en el header X-Api-Key en cada request.

cURL

curl https://api.ez-catalog.huggian.com/v1/products/barcode/7790895000997 \
  -H "X-Api-Key: ezc_free_xxxxxxxx"

Autenticación

Toda request a /v1/* requiere el header X-Api-Key. La ruta /health es pública. Las imágenes se sirven desde images.huggian.com sin autenticación.

  • 401 Unauthorized: falta la key o es inválida.
  • 403 Forbidden: key válida pero sin permiso (plan insuficiente o scope faltante).
  • 429 Too Many Requests: rate limit excedido. Esperá el próximo minuto o mirá Retry-After.

Planes y límites

PlanRate limitCuota mensualLecturaEscrituraFull-text
Free10 rpm50Barcode, marca, familia, categoríaPropuestasNo
Basic60 rpm25.000TodoProductos e imágenesSí
Pro300 rpm200.000TodoTodo, incluido borradoSí

El rate limit es por API key, por minuto, con ventana deslizante. La cuota mensual se reinicia el día 1 a las 00:00 UTC y no se resetea al cambiar de plan.

Errores

Todos los errores responden JSON con el campo error. Algunos agregan message con una pista.

{
  "error": "full_text_search_not_available",
  "message": "Full-text search requires a Basic or Pro plan. Use /v1/products/barcode/:ean for barcode lookup."
}
  • 400: body inválido.
  • 401: auth faltante o inválida.
  • 403: plan insuficiente.
  • 404: el recurso no existe.
  • 409: conflicto (barcode o marca duplicada).
  • 429: rate limit.
  • 500: interno. Reintentá con backoff.

Productos

GET /v1/products/barcode/{ean} Free+

Buscar producto por código de barras EAN.

Response

{
  "id": "8a3e...",
  "barcode": "7790895000997",
  "barcode_type": "EAN13",
  "name": "Yerba Mate Taragüi 500g",
  "brand": "Taragüi",
  "brand_id": "f1c2...",
  "brand_logo_url": "https://images.huggian.com/brands/taragui.png",
  "image_url": "https://images.huggian.com/products/7790895000997.jpg",
  "category_id": "ab12...",
  "country_of_origin": "AR",
  "verified": true,
  "product_type": "good",
  "presentation": "500g",
  "unit_of_measure": "g",
  "ncm_code": "0903.00.10",
  "afip_category": null,
  "family_id": "c9d0...",
  "attributes": null,
  "source": "manual",
  "created_at": "2026-01-12T10:00:00Z",
  "updated_at": "2026-04-30T09:00:00Z"
}
  • Devuelve 404 si el EAN no está en el catálogo.
GET /v1/products/{id} Free+

Buscar producto por UUID.

Response

Mismo shape que /barcode/{ean}.
GET /v1/products/search?q=&brand=&family=&category=&page=&per_page= Basic+

Búsqueda full-text con filtros y paginación.

Response

{
  "items": [ /* Product[] */ ],
  "total": 142,
  "page": 1,
  "per_page": 20
}
  • page por defecto 1 (mín 1). per_page por defecto 20 (1..100).
  • brand, family, category aceptan UUID.
  • Free recibe 403 con error full_text_search_not_available.
POST /v1/products Basic+

Crear producto.

Request body

{
  "barcode": "7790895000997",
  "barcode_type": "EAN13",
  "name": "Yerba Mate Taragüi 500g",
  "description": null,
  "category_id": "ab12...",
  "brand": "Taragüi",
  "brand_id": null,
  "image_url": null,
  "ncm_code": "0903.00.10",
  "afip_category": null,
  "country_of_origin": "AR",
  "source": "manual",
  "presentation": "500g",
  "subcategory": null,
  "unit_of_measure": "g",
  "product_type": "good",
  "attributes": null
}

Response

201 Created → Product completo.
  • Solo `name` es obligatorio.
  • 409 barcode already exists si el EAN choca con otro producto.
  • 409 brand name already exists si la marca textual ya existe normalizada.
PUT /v1/products/{id} Basic+

Actualizar producto (todos los campos opcionales, parche).

Request body

{ "name": "Nuevo nombre", "verified": true }

Response

200 → Product actualizado.
DELETE /v1/products/{id} Basic+

Borrar producto.

Response

204 No Content.

Marcas

GET /v1/brands Free+

Listar marcas.

Response

[
  { "id": "f1c2...", "name": "Taragüi", "slug": "taragui", "logo_url": "...", "created_at": "..." }
]
GET /v1/brands/{id} Free+

Obtener marca por UUID.

Response

Brand.
POST /v1/brands Basic+

Crear marca (normaliza acentos, sufijos corporativos, mayúsculas).

Request body

{ "name": "La Serenísima", "logo_url": null }

Response

201 → Brand. 409 si la marca normalizada ya existe.

Familias

GET /v1/families Free+

Listar familias de producto (variantes de un mismo SKU base).

Response

ProductFamily[].
GET /v1/families/{id} Free+

Obtener familia por UUID.

Response

ProductFamily.

Categorías

GET /v1/categories Free+

Listar categorías (árbol jerárquico, `parent_id` apunta al padre o `null`).

Response

Category[].

Imágenes

GET /v1/products/{ean}/images Free+

Listar imágenes de un producto por EAN.

Response

{ "images": [ { "key": "products/7790.../v1.jpg", "url": "https://images.huggian.com/products/..." } ] }
POST /v1/products/{ean}/images Basic+

Subir imagen (multipart/form-data con campo `file`).

Response

201 → { "key": "...", "url": "..." }.
  • Acepta JPEG, PNG, WebP. Tamaño máximo configurable por entorno.
GET /i/{key} Free+

Proxy de imagen pública. Sirve los bytes desde el bucket. Sin auth — la URL es la credencial.

Response

Image bytes (Content-Type real). Cacheable por CDN.
  • No requiere X-Api-Key.
  • El servicio rechaza keys que escapen del prefijo `products/`.

Health check

GET /health Free+

Healthcheck no autenticado.

Response

{ "ok": true }

Schemas

Product
{
  id: uuid,
  barcode: string | null,
  barcode_type: string,           // "EAN13" | "EAN8" | "UPC" | ...
  name: string,
  description: string | null,
  category_id: uuid | null,
  brand: string | null,
  brand_id: uuid | null,
  brand_logo_url: string | null,
  image_url: string | null,
  ncm_code: string | null,
  afip_category: string | null,
  country_of_origin: string,       // ISO 3166-1 alpha-2
  verified: boolean,
  source: string,                  // "manual" | "openff" | ...
  presentation: string | null,     // "500g", "1L", ...
  subcategory: string | null,
  unit_of_measure: string | null,  // "g" | "ml" | "u"
  family_id: uuid | null,
  product_type: string,            // "good" | "service"
  attributes: object | null,       // free-form JSON
  created_at: datetime,
  updated_at: datetime
}
Brand
{
  id: uuid,
  name: string,
  slug: string,                    // normalizado: sin acentos, lowercase
  logo_url: string | null,
  created_at: datetime
}
ProductFamily
{
  id: uuid,
  name: string,
  slug: string,
  brand_id: uuid | null,
  created_at: datetime
}
Category
{
  id: uuid,
  slug: string,
  name: string,
  parent_id: uuid | null,          // null para categorías raíz
  created_at: datetime
}