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.
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
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 keysiempre 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"
{ "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.
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}
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.
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.
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.
{ "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.
{ "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.
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.