API ReferenceEndpointsProductsCreate many products in one call (queued)

Create many products in one call (queued)

Queues a batch of products for creation and returns a jobId immediately. It does NOT wait for the import to finish. Every field accepted by POST /v1/products is accepted per row, including variants, categories, brands, tags, images, translations and tax behavior. Max 1000 products per request (500 recommended). For a 3,000-50,000 product catalog, send several requests of that size carrying the same importId, then poll GET /v1/products/bulk/import/{importId} for the whole import. Rows whose sku or externalId already exist are skipped by default (see conflictStrategy), so re-sending a batch after a timeout does not duplicate the catalog. The Idempotency-Key header is honored as well and returns the original jobId on replay.

POST
/v1/products/bulk
AuthorizationBearer <token>

Admin API key (server-to-server). Issue via the dashboard at Settings → API keys. Plain-text key is shown once — store in a secret manager. Format: Authorization: Bearer brainerce_xxxxxxxxx.

In: header

Header Parameters

Idempotency-Key?string

Client-supplied key (a UUID v4 is the recommended form, max 255 characters) that makes this mutation safe to retry.

  • Replay window: 24 hours. The first response for a key is cached; a retry inside the window with the same request body returns the original status and body without re-running the handler.
  • Reusing a key with a different body returns 409 Conflict with code: "IDEMPOTENCY_KEY_REUSED". The request fingerprint (method + path + body hash) is compared against the stored one; a mismatch is refused rather than served the old response.
  • Keys are scoped to the calling credential and store — two API keys may safely use the same key value.
  • Error responses are cached too, so a retry of a request that failed validation returns the same 400 immediately.
  • Sending this header on a GET returns 400 with code: "IDEMPOTENCY_KEY_NOT_SUPPORTED" — GETs are already idempotent.
  • Support is per-route and this parameter is the authoritative signal: a key sent to a route that does not declare it is accepted and silently ignored.

See /docs/api/idempotency.

Lengthlength <= 255
productsarray<CreateProductDto>

Products to create. Each entry accepts every field POST /v1/products accepts. Max 1000 per request (500 recommended), so chunk larger catalogs and pass the same importId on every chunk. Rows are validated individually: an invalid row is reported as a row failure and does NOT reject the batch.

Items1 <= items <= 1000
importId?string

Groups several chunked requests into one logical import so status can be read for the whole catalog rather than per chunk. Generate one value client-side and send it on every chunk. Any string you choose; it is not validated against anything.

Lengthlength <= 191
conflictStrategy?string

How to handle a row whose sku or externalId already exists in the store. skip (default) counts it under skipped and moves on, which is the safe choice for retries. error records it as a failure instead, which is what you want when a duplicate means the source file is wrong.

Default"skip"
Value in"skip" | "error"
syncMode?string

Channel sync behavior. coalesced (default) suppresses the per-product connector push and files ONE sync per affected sales channel when the import finishes. That is necessary because connectors are rate-limited per catalog (Meta allows 100 batch requests/hour). none writes to Brainerce only and leaves channels to the next reconciliation.

Default"coalesced"
Value in"coalesced" | "none"
idempotencyKey?string

Durable dedup key for this batch. Re-sending the same key returns the ORIGINAL jobId instead of importing again. On HTTP the Idempotency-Key header does the same thing and is preferred; this field exists because it is stored in the database rather than in Redis with a 24h TTL, and because the MCP tool has no HTTP headers to carry one.

Lengthlength <= 191
curl -X POST "https://api.brainerce.com/api/v1/products/bulk" \  -H "Idempotency-Key: string" \  -H "Content-Type: application/json" \  -d '{    "products": [      {        "name": "Premium Wireless Headphones",        "basePrice": 299.99,        "type": "SIMPLE"      }    ]  }'
{
  "jobId": "clx1a2b3c4d5e6f7g8h9",
  "importId": "clx1a2b3c4d5e6f7g8h9",
  "status": "QUEUED",
  "total": 500,
  "replayed": true
}

{
  "statusCode": 400,
  "code": "VALIDATION_FAILED",
  "message": "name should not be empty, price must be a positive number",
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/products/bulk"
}

{
  "statusCode": 401,
  "code": "UNAUTHORIZED",
  "message": "Invalid or expired credential",
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/products/bulk"
}

{
  "statusCode": 403,
  "code": "INSUFFICIENT_SCOPE",
  "message": "API key does not have the required scope: products:write",
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/products/bulk"
}

{
  "statusCode": 409,
  "code": "CONFLICT",
  "message": "The request conflicts with the current state of the resource",
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/products/bulk"
}

{
  "statusCode": 429,
  "code": "RATE_LIMITED",
  "message": "Too many requests",
  "details": {
    "retryAfterSeconds": 12
  },
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/store"
}