Importación y exportación masiva (CSV)

Este dominio soporta dos formas de mover tu catálogo por CSV: un flujo síncrono heredado (útil para archivos pequeños, responde en la misma petición) y un flujo asíncrono (sube el archivo a S3, encola el procesamiento y consultas el progreso por jobId) tanto para importar como para exportar.

¿Cuándo uso el flujo síncrono y cuándo el asíncrono?

El flujo síncrono (POST /products/bulk/csv/import sin s3Key, POST /products/export-csv) es simple pero está limitado por el timeout de la petición HTTP — úsalo solo para archivos pequeños. El flujo asíncrono (s3Key de por medio, respuesta 202 + jobId) es el recomendado para catálogos grandes: sube el archivo a S3 con una URL prefirmada, encola el trabajo y haz polling del estado.

Flujo asíncrono de importación (recomendado)

1. POST /products/bulk/csv/upload-url         → { url, fields, s3Key }
2. (el cliente sube el CSV directo a S3 con esos campos)
3. POST /products/bulk/csv/validate           → { rows, validRows, invalidRows[], ... }  (opcional, recomendado)
4. POST /products/bulk/csv/import { s3Key }   → 202 { jobId }
5. GET  /products/bulk/csv/import/{jobId}     → polling hasta que status deje de estar en curso

Flujo asíncrono de exportación

1. POST /products/bulk/csv/export             → 202 { jobId }
2. GET  /products/bulk/csv/export/{jobId}     → polling; downloadUrl aparece cuando termina

Obtener la URL de subida (paso 1 del import asíncrono)

Genera una URL prefirmada de S3 para subir el CSV directamente desde el cliente, sin pasar el archivo por el lambda.

{}
200
{
  "url": "https://fenicia-csv-imports-prod.s3.amazonaws.com/",
  "fields": {
    "key": "uploads/65f2a0b1c4d5e6f7a8b9c0aa/9c1e2f3a-....csv",
    "policy": "eyJleHBpcmF0aW9uIjoi...",
    "x-amz-signature": "..."
  },
  "s3Key": "uploads/65f2a0b1c4d5e6f7a8b9c0aa/9c1e2f3a-....csv"
}

Permiso requerido: products:import

Límites de la URL prefirmada

La URL expira a los 600 segundos. El archivo subido no puede exceder 25 MiB. El bucket fuerza cifrado del lado del servidor (SSE) en la subida. La key siempre debe empezar con el prefijo uploads/{tenantId}/ — si construyes tu propia s3Key en vez de usar la que te devuelve este endpoint, cualquier otra ruta será rechazada como acceso no autorizado (ver errores forbidden/invalid-s3-key).

curl -X POST https://api.fenicia.io/products/bulk/csv/upload-url \
  -H "Authorization: Bearer fkapi_tu_api_key"

Validar el CSV antes de importar (opcional, recomendado)

Valida estructuralmente el CSV ya subido a S3, sin aplicar cambios. Detecta si excede los límites de filas o de variantes por producto.

s3Keystringrequired

Key de S3 devuelta por upload-url.

configobject

Configuración de importación (ver POST /products/bulk/csv/import).

{
  "s3Key": "uploads/65f2a0b1c4d5e6f7a8b9c0aa/9c1e2f3a-....csv",
  "config": { "format": "fenicia", "createIfNotExists": true }
}
200
{
  "rows": 4200,
  "validRows": 4180,
  "invalidRows": [
    { "row": 87, "sku": "CAM-ROJO-M", "errors": ["price is required"] }
  ],
  "exceedsRowCap": false,
  "exceedsVariantCap": false,
  "structuralErrors": []
}
403
{ "code": "forbidden/invalid-s3-key", "message": "s3Key does not belong to this tenant" }

Permiso requerido: products:import

Shape de invalidRows[] parcialmente confirmado

El wrapper (rows, validRows, invalidRows, exceedsRowCap, exceedsVariantCap, structuralErrors) está confirmado. La forma exacta de cada elemento dentro de invalidRows[] no fue verificada línea por línea — trátala como orientativa.


Importar por CSV

Importa productos desde CSV. El body determina si la ejecución es síncrona (contenido inline) o asíncrona (s3Key de un archivo ya subido).

s3Keystring

Key de S3 del archivo ya subido. Si viene presente y no vacío, la ejecución es asíncrona (202).

configobject

Solo modo asíncrono. Allowlist saneada en el servidor: format, mappings, fieldsToUpdate, skipErrorRows, operationName, createIfNotExists, enableSideEffects.

contentstring

Solo modo síncrono. Contenido crudo del CSV.

formatstring

Solo modo síncrono. fenicia, shopify o mercadolibre.

mappingsobject

Solo modo síncrono. Mapeo de columnas del CSV a campos del producto.

createIfNotExistsboolean

Solo modo síncrono. Crea el producto si el SKU no existe. Default: false.

skipErrorRowsboolean

Solo modo síncrono. Continúa la importación saltando filas con error. Default: true.

batchSizenumber

Solo modo síncrono. Tamaño de lote de procesamiento. Default: 100.

operationNamestring

Nombre visible de la operación en el historial de bulk operations.

fieldsToUpdatestring[]

Restringe la actualización a estos campos únicamente.

{
  "content": "sku,title,price\nCAM-ROJO-M,Camisa Roja M,499.00\n",
  "format": "fenicia",
  "createIfNotExists": true
}
{
  "s3Key": "uploads/65f2a0b1c4d5e6f7a8b9c0aa/9c1e2f3a-....csv",
  "config": { "format": "fenicia", "createIfNotExists": true }
}
200Síncrono — éxito total.
{
  "totalProcessed": 320,
  "created": 12,
  "updated": 305,
  "skipped": 3,
  "errors": []
}
207Síncrono — éxito parcial. Revisa errors[] fila por fila.
{
  "totalProcessed": 320,
  "created": 10,
  "updated": 298,
  "skipped": 4,
  "errors": [
    { "row": 45, "sku": "PAN-AZUL-32", "message": "duplicate-sku" }
  ]
}
202Asíncrono — el trabajo se encoló para procesarse en background.
{ "jobId": "csvimp_65f3a1b2c4d5e6f7a8b9c0e1" }

Permiso requerido: products:import

207 Multi-Status = éxito parcial, no lo trates como error genérico

Cuando la ejecución síncrona termina con algunas filas fallidas, la respuesta es 207 Multi-Status, no 200 ni 4xx. Si tu cliente HTTP solo distingue "2xx = éxito" de "4xx/5xx = error" sin diferenciar el código exacto, vas a tratar una importación parcialmente fallida como un éxito total. Revisa siempre errors[], incluso en 2xx.

El discriminador es s3Key, no un parámetro explícito de modo

No existe un campo mode: 'sync' | 'async'. El servidor decide por sí mismo: si s3Key viene presente y no vacío, ejecuta el flujo asíncrono e ignora content; si no, ejecuta el flujo síncrono legado con content. Los caps de costo del modo asíncrono (maxRows, maxVariantsPerProduct, chunkSize) los fija el servidor — nunca se aceptan del cliente, aunque los envíes en config.

curl -X POST https://api.fenicia.io/products/bulk/csv/import \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "sku,title,price\nCAM-ROJO-M,Camisa Roja M,499.00\n",
    "format": "fenicia",
    "createIfNotExists": true
  }'

Consultar el estado de una importación asíncrona

Consulta el progreso de una importación asíncrona encolada.

jobIdstringrequired

ID del trabajo devuelto por POST /products/bulk/csv/import.

200
{
  "jobId": "csvimp_65f3a1b2c4d5e6f7a8b9c0e1",
  "status": "processing",
  "percent": 62,
  "processedRows": 2604,
  "totalRows": 4200,
  "created": 80,
  "updated": 2500,
  "failed": 24,
  "skipped": 0,
  "errorReportUrl": null
}
404
{ "code": "not-found", "message": "Import job not found" }

Permiso requerido: products:read

Tip

Cuando el job termina con filas fallidas, errorReportUrl deja de ser null y apunta a un CSV descargable con el detalle fila por fila de cada error.


Descargar una plantilla CSV

Descarga una plantilla CSV vacía con las columnas correctas para el formato solicitado.

formatstringrequired

fenicia, shopify o mercadolibre.

{ "format": "fenicia" }
200CSV crudo, no JSON.
Content-Type: text/csv
Content-Disposition: attachment; filename="template-fenicia.csv"
 
sku,title,price,status,...
400
{ "code": "bad-request", "message": "Unsupported format" }

Permiso requerido: sin permiso adicional confirmado más allá de la sesión autenticada.


Previsualizar una importación (dry-run)

Interpreta el CSV y devuelve el resultado sin persistir nada — para revisar antes de importar.

contentstringrequired

Contenido crudo del CSV.

formatstring

fenicia, shopify o mercadolibre.

mappingsobject

Mapeo de columnas a campos del producto.

createIfNotExistsboolean

Simula creación si el SKU no existe.

fieldsToUpdatestring[]

Restringe la previsualización a estos campos.

{
  "content": "sku,title,price\nCAM-ROJO-M,Camisa Roja M,499.00\n",
  "format": "fenicia",
  "createIfNotExists": true
}
200
{
  "toCreate": 12,
  "toUpdate": 305,
  "toSkip": 3,
  "errors": []
}

Permiso requerido: products:import

Shape orientativo, no verificado campo a campo

El reporte de auditoría confirma que este endpoint devuelve un CSVPreviewResult, pero no detalla sus claves exactas. El ejemplo de arriba es orientativo — confírmalo contra tu propia respuesta.

Existe un gate anti prototype-pollution en mappings

Si envías un mappings malicioso (por ejemplo, claves como __proto__), el servidor lo rechaza con un ValidationError antes de procesar el archivo.


Previsualizar una edición masiva por CSV (dry-run obligatorio)

Como preview, pero para el flujo de edición masiva: siempre debes correr este dry-run antes de aplicar los cambios.

contentstringrequired

Contenido crudo del CSV con los cambios a aplicar.

formatstring

fenicia, shopify o mercadolibre.

mappingsobject

Mapeo de columnas a campos del producto.

createIfNotExistsboolean

Simula creación si el SKU no existe.

{
  "content": "sku,price\nCAM-ROJO-M,549.00\n",
  "format": "fenicia"
}
200
{
  "products": [],
  "invalidRows": [],
  "summary": { "toUpdate": 305, "toCreate": 12, "invalid": 3 }
}

Permiso requerido: products:update

Este dry-run no es opcional en el flujo de edición masiva

El diseño de este flujo (documentado internamente como ADR-019) obliga a correr este endpoint antes de aplicar una edición masiva vía CSV — es la forma de revisar el impacto (summary) y las filas inválidas antes de comprometer los cambios.


Exportar el catálogo a CSV (síncrono)

Exporta productos a CSV de forma síncrona. Único formato soportado: fenicia.

formatstringrequired

Único valor soportado: fenicia.

selectionstringrequired

selected, filter o all.

productIdsstring[]

Requerido cuando selection es selected.

filtersobject

Requerido cuando selection es filter.

{ "format": "fenicia", "selection": "all" }
200CSV crudo, no JSON.
Content-Type: text/csv
Content-Disposition: attachment; filename="productos-2026-04-11.csv"
 
sku,title,price,status
CAM-ROJO-M,Camisa Roja M,499.00,active
400
{ "code": "bad-request", "message": "Unsupported format" }
404
{ "code": "not-found/no-products", "message": "No products matched the selection" }

Permiso requerido: products:export

Este endpoint es síncrono, no confundir con /products/bulk/csv/export

Es una ruta distinta (/products/export-csv, sin bulk) al flujo asíncrono de exportación descrito abajo. Está pensado para exportaciones chicas que caben dentro de una sola petición HTTP — para catálogos grandes usa el flujo asíncrono.


Exportar el catálogo a CSV (asíncrono)

Encola una exportación masiva a CSV. Responde siempre 202 con un jobId.

formatstringrequired

Único valor soportado: fenicia.

selectionstringrequired

selected, filter o all.

productIdsstring[]

Requerido cuando selection es selected. Máximo 5000 IDs.

filtersobject

Requerido cuando selection es filter.

{ "format": "fenicia", "selection": "all" }
202
{ "jobId": "csvexp_65f3a1b2c4d5e6f7a8b9c0e2" }
400
{ "code": "too-many-products", "message": "productIds exceeds the maximum of 5000" }

Permiso requerido: products:export

curl -X POST https://api.fenicia.io/products/bulk/csv/export \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "format": "fenicia", "selection": "all" }'

Consultar el estado de una exportación asíncrona

Consulta el progreso de una exportación asíncrona encolada. Cuando termina, incluye la URL de descarga.

jobIdstringrequired

ID del trabajo devuelto por POST /products/bulk/csv/export.

200
{
  "jobId": "csvexp_65f3a1b2c4d5e6f7a8b9c0e2",
  "status": "completed",
  "percent": 100,
  "processedRows": 8300,
  "totalRows": 8300,
  "count": 8300,
  "downloadUrl": "https://fenicia-csv-imports-prod.s3.amazonaws.com/exports/65f2a0b1c4d5e6f7a8b9c0aa/csvexp_.../productos.csv?X-Amz-Signature=..."
}
404
{ "code": "not-found", "message": "Export job not found" }

Permiso requerido: products:read

downloadUrl es una URL prefirmada de S3

downloadUrl solo aparece cuando status indica que el trabajo terminó. Es una URL de descarga directa (GET prefirmado) — no necesitas tu API key de Fenicia para descargarla, solo tenerla vigente antes de que expire.


Errores

CódigoStatusDescripción
bad-request400Falta un campo requerido en el body (según el endpoint).
bad-request/missing-s3-key400El body de importación asíncrona no trae s3Key.
forbidden/invalid-s3-key403La s3Key no empieza con el prefijo de tu tenant (uploads/{tenantId}/) — no te pertenece.
not-found/s3-object404El objeto referenciado por s3Key no existe en el bucket.
bad-request/file-too-large400El CSV subido excede el tamaño máximo permitido.
bad-request/invalid-config400La configuración de importación (config) es inválida.
bad-request/invalid-import-file400El archivo no tiene una estructura de CSV válida.
rate-limited429Se alcanzó el límite de operaciones asíncronas concurrentes/por ventana de tiempo.
unsupported-format400El format solicitado no está soportado (exportación asíncrona solo soporta fenicia).
invalid-selection400El valor de selection no es selected, filter ni all.
missing-products400selection: 'selected' sin productIds.
too-many-products400productIds excede el máximo de 5000.
missing-filters400selection: 'filter' sin filters.
not-found/no-products404(exportación síncrona) La selección no resolvió ningún producto.
duplicate-sku409El SKU de una fila ya existe (cuando createIfNotExists no lo permite sobreescribir).
not-found404No existe un trabajo (jobId) con ese identificador.
auth:invalid_token401La API key es inválida o fue revocada.
auth:permission_denied403La API key no tiene el permiso requerido (products:import, products:export o products:update según el endpoint).

Consulta el catálogo completo de errores para el resto de los códigos posibles.

207 Multi-Status aparece solo en la importación síncrona

De todos los endpoints de esta página, únicamente POST /products/bulk/csv/import en su rama síncrona responde 207 en éxito parcial. Los endpoints asíncronos no tienen ese concepto en la respuesta inmediata — el éxito parcial de un job asíncrono se refleja en los contadores failed/skipped de GET .../import/{jobId}, con status 200.

Siguientes pasos