Returning.AIDevelopers
v1

API reference / Store / Products

.md

Create products in bulk

Create up to 100 Store products in one call, each with its voucher codes, from a JSON array or a CSV file.

Last updated 26 Sep 2026API v1

Method
POST
Path
https://api.returning.ai/v1/products/bulk
Permission
store
Retries
No Idempotency-Key; check before retrying

When to use this

  • You load a new Store catalogue, or a new batch of rewards, and have every product's details and codes ready.
  • Your team keeps the catalogue in a spreadsheet and uploads it as CSV.
  • You want a batch to go live in full or not at all.

Authentication#

Header
Authorization: Bearer <API_KEY>
Permission
storeShown in the dashboard as “Store”

Send a Community API key with the store permission. The key decides the community, so never send a community ID. Keep the key on your server.

Behaviour#

Every item is checked, and every category, role, tag, username and status name looked up, before anything is saved. Then all the products and their codes are saved together: if any item fails, nothing is created. Unless an item has isArchived: true, its product is live as soon as the response returns.

Each item always creates a new product. Names don't have to be unique, within the batch or against products you already have. Voucher codes are trimmed, and a code only has to be unique within its own item.

Request#

Headers#

Authorization#stringREQUIRED
Community API key with store.

RuleBearer <API_KEY>

Content-Type#stringREQUIRED
application/json for a JSON array, or multipart/form-data for a CSV upload.

Ruleapplication/json or multipart/form-data

curl --request POST \
  --url https://api.returning.ai/v1/products/bulk \
  --header 'Authorization: Bearer <API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '[
    {
      "name": "$25 Trading Credit",
      "description": "<p>Redeem 500 coins for a $25 trading credit on your live account.</p>",
      "image": "https://cdn.example.com/store/trading-credit-25.png",
      "price": 500,
      "categoryName": "Trading rewards",
      "vouchers": ["TC25-7Q4M-2B9X"],
      "voucherExpireDate": "2026-12-31T23:59:59.000Z",
      "isArchived": false,
      "redemptionInstructions": "<p>Your credit is added to your trading account after approval.</p>",
      "discountPrice": 0,
      "isDiscountEnabled": false,
      "discountStartDate": null,
      "discountEndDate": null,
      "userInformation": {
        "isEnabled": false,
        "shouldCollectName": false,
        "shouldCollectPhone": false,
        "shouldCollectAddress": false
      },
      "shouldDisplayRemainingQuantity": true,
      "shouldPrioritiesExpiringVouchers": false,
      "isProductAccessEnabled": false,
      "productPermission": [],
      "purchaseStatusOverrideName": null
    },
    {
      "name": "$50 Trading Credit",
      "description": "<p>Redeem 1000 coins for a $50 trading credit on your live account.</p>",
      "image": "https://cdn.example.com/store/trading-credit-50.png",
      "price": 1000,
      "categoryName": "Trading rewards",
      "vouchers": ["TC50-4K8P-6D2W"],
      "voucherExpireDate": "2026-12-31T23:59:59.000Z",
      "isArchived": false,
      "redemptionInstructions": "<p>Your credit is added to your trading account after approval.</p>",
      "discountPrice": 0,
      "isDiscountEnabled": false,
      "discountStartDate": null,
      "discountEndDate": null,
      "userInformation": {
        "isEnabled": false,
        "shouldCollectName": false,
        "shouldCollectPhone": false,
        "shouldCollectAddress": false
      },
      "shouldDisplayRemainingQuantity": true,
      "shouldPrioritiesExpiringVouchers": false,
      "isProductAccessEnabled": false,
      "productPermission": [],
      "purchaseStatusOverrideName": null
    }
  ]'

Response#

A 201 returns the new products in data, in the order you sent them, and meta.created counts them. Save each _id: every other product call uses it. stocks counts each product's codes, less any that have already expired; voucher codes are never returned. Branch on the HTTP status and meta.code, never on message.

meta#objectALWAYS
Status details.

Eg{ ... }

status#stringALWAYS
Result of the request.

Rulesuccess

statusCode#integerALWAYS
The HTTP status, repeated.

Eg201

code#stringALWAYS
Machine-readable result code.

RulePRODUCTS_BULK_CREATED

created#integerALWAYS
Number of products created.

Eg2

message#stringALWAYS
Human-readable summary. Do not branch on it.

Eg"Create products success."

data#object[]ALWAYS
The new products, in the order you sent them.

Eg[ ... ]

_id#stringALWAYS
Product ID. Save it: you need it to read or update the product.

Eg"66f000000000000000000101"

name#stringALWAYS
Product name shown in the Store.

Eg"$25 Trading Credit"

price#numberALWAYS
Price in coins.

Eg500

categoryID#stringALWAYS
Category ID, or null when the product has no category.
category#stringALWAYS
Category name, or null when the product has no category or its category was deleted.

Eg"Trading rewards"

stocks#integerALWAYS
Vouchers left to buy: codes that are unsold and not expired. Voucher codes are never returned.

Eg1

isArchived#boolean
true when the product is archived and left out of List products and the Store.

Egfalse

image#string
Image URL, or null when there is no image.

Eg"https://cdn.example.com/store/trading-credit-25.png"

description#string
Product description as HTML. Only basic formatting tags are kept.

Eg"<p>Redeem 500 coins for a $25 trading credit on your live account.</p>"

redemptionInstructions#string
Redemption instructions for the trader, or null.

Eg"<p>Your credit is added to your trading account after approval.</p>"

discountPrice#number
Discounted price in coins, charged between the discount dates while the discount is on.

Eg0

isDiscountEnabled#boolean
true when the discount is switched on.

Egfalse

discountStartDate#string
When the discount starts, ISO 8601 UTC, or null.

Egnull

discountEndDate#string
When the discount ends, ISO 8601 UTC, or null.

Egnull

userInformation#object
What the trader is asked for when they redeem.

Eg{ ... }

isEnabled#boolean
true when the trader is asked for details.

Egfalse

shouldCollectName#boolean
true to ask for their name.

Egfalse

shouldCollectPhone#boolean
true to ask for their phone number.

Egfalse

shouldCollectAddress#boolean
true to ask for their address.

Egfalse

shouldDisplayRemainingQuantity#boolean
true to show traders how many are left.

Egtrue

shouldPrioritiesExpiringVouchers#boolean
true to prioritise the vouchers closest to expiry.

Egfalse

expiringVoucherHighlight#object
Highlight for vouchers close to expiry.

Eg{"isEnabled": false, "duration": 0, "timeframe": "days"}

isEnabled#boolean
true when the highlight is on.

Egfalse

duration#integer
How close to expiry, in timeframe units.

Eg0

timeframe#string
Unit for duration, such as days.

Eg"days"

isProductAccessEnabled#boolean
true when the product's own rules apply; false when it follows its category.

Egfalse

productPermission#object[]ALWAYS
Access rules. An empty array means the product has no rules of its own.

Eg[]

isEnabled#boolean
true when the rule is active.

Egtrue

type#string
What the rule matches.

Ruleuser, role, tag or role_combination

Eg"role"

permission#object
The traders the rule covers, by readable name.
roleNames#string[]
Role names.
tagNames#string[]
Tag names.
usernames#string[]
Usernames.
userIDs#string[]
Record IDs of the traders the rule names. Send usernames when you write rules.
combination#object[]
Role or tag groups a trader must hold together.
type#string
role or tag.

Eg"role"

values#string[]
Role or tag names in the group.
isSelectAll#boolean
true when the rule covers all traders.
purchaseAccess#object
Special price for traders the rule covers.
specialPrice#number
Special price in coins.

Eg80

isSpecialPriceEnabled#boolean
true when the special price applies.

Egfalse

purchaseLimit#object
How often a covered trader can buy.
isPurchaseLimitEnabled#boolean
true when the limit applies.

Egfalse

quantity#number
Purchases allowed in each window.

Eg1

intervalCount#number
Length of the window, in intervalUnits.

Eg1

intervalUnit#string
Unit of the window, such as months.

Eg"months"

accessLevel#string
What covered traders can do.

Rulefull-access, view-only or access-denied

Eg"full-access"

isExclusiveTagEnabled#boolean
true to show the exclusive tag.

Egfalse

purchaseStatusOverride#objectALWAYS
The status new orders for this product start in, when set.

Eg{"isEnabled": false, "status": null}

isEnabled#boolean
true when new orders start in status.

Egfalse

status#string
Order status name, or null when the category or community default applies.

Egnull

totalOrders#integer
Orders placed for this product.

Eg0

communityID#string
Your community ID.

Eg"66f000000000000000000010"

createdAt#string
When the product was created, ISO 8601 UTC.

Eg"2026-09-26T08:30:00.000Z"

updatedAt#string
When the product last changed, ISO 8601 UTC.

Eg"2026-09-26T08:30:00.000Z"

{
  "meta": {
    "status": "success",
    "statusCode": 201,
    "created": 2,
    "code": "PRODUCTS_BULK_CREATED"
  },
  "message": "Create products success.",
  "data": [
    {
      "_id": "66f000000000000000000101",
      "communityID": "66f000000000000000000010",
      "name": "$25 Trading Credit",
      "description": "<p>Redeem 500 coins for a $25 trading credit on your live account.</p>",
      "image": "https://cdn.example.com/store/trading-credit-25.png",
      "price": 500,
      "categoryID": "66f000000000000000000201",
      "category": "Trading rewards",
      "stocks": 1,
      "discountPrice": 0,
      "isDiscountEnabled": false,
      "discountStartDate": null,
      "discountEndDate": null,
      "userInformation": {
        "isEnabled": false,
        "shouldCollectName": false,
        "shouldCollectPhone": false,
        "shouldCollectAddress": false
      },
      "shouldPrioritiesExpiringVouchers": false,
      "shouldDisplayRemainingQuantity": true,
      "expiringVoucherHighlight": {
        "isEnabled": false,
        "duration": 0,
        "timeframe": "days"
      },
      "isArchived": false,
      "totalOrders": 0,
      "redemptionInstructions": "<p>Your credit is added to your trading account after approval.</p>",
      "productPermission": [],
      "isProductAccessEnabled": false,
      "purchaseStatusOverride": {
        "isEnabled": false,
        "status": null
      },
      "createdAt": "2026-09-26T08:30:00.000Z",
      "updatedAt": "2026-09-26T08:30:00.000Z"
    },
    {
      "_id": "66f000000000000000000102",
      "communityID": "66f000000000000000000010",
      "name": "$50 Trading Credit",
      "description": "<p>Redeem 1000 coins for a $50 trading credit on your live account.</p>",
      "image": "https://cdn.example.com/store/trading-credit-50.png",
      "price": 1000,
      "categoryID": "66f000000000000000000201",
      "category": "Trading rewards",
      "stocks": 1,
      "discountPrice": 0,
      "isDiscountEnabled": false,
      "discountStartDate": null,
      "discountEndDate": null,
      "userInformation": {
        "isEnabled": false,
        "shouldCollectName": false,
        "shouldCollectPhone": false,
        "shouldCollectAddress": false
      },
      "shouldPrioritiesExpiringVouchers": false,
      "shouldDisplayRemainingQuantity": true,
      "expiringVoucherHighlight": {
        "isEnabled": false,
        "duration": 0,
        "timeframe": "days"
      },
      "isArchived": false,
      "totalOrders": 0,
      "redemptionInstructions": "<p>Your credit is added to your trading account after approval.</p>",
      "productPermission": [],
      "isProductAccessEnabled": false,
      "purchaseStatusOverride": {
        "isEnabled": false,
        "status": null
      },
      "createdAt": "2026-09-26T08:30:00.000Z",
      "updatedAt": "2026-09-26T08:30:00.000Z"
    }
  ]
}

Errors#

Every error carries its code in meta.code, with the reason in detail as a sentence. A rule broken inside an item is named by position, such as Bulk product item 2 validation failed: price must not be less than 0, and a CSV error names its row, such as row 3: Price must be a number. A name with no match or a voucher problem doesn't say which item, for example Category not found for name: Trading prizes. A body that isn't valid JSON returns 400 without a meta.code.

Fix the request04

400VALIDATION_FAILEDFix the request
The body isn't a raw array or has over 100 items, an item or CSV row breaks a rule, or a name has no match: an unknown category, role, tag, username or status, an empty or repeated voucher code in one item, or codes without voucherExpireDate. detail says which. Nothing was created.
401AUTH_API_KEY_REQUIREDFix the request
No key was sent. Send Authorization: Bearer <API_KEY>.
401AUTH_API_KEY_INVALIDFix the request
The key is unknown, expired or malformed. Use a current Community API key.
403AUTH_PERMISSION_REQUIREDFix the request
The key lacks store. Add the permission in Settings > Integration > API Keys.

Retry with backoff03

500INTERNAL_ERRORRetry with backoff
The outcome is unclear, but the batch was saved in full or not at all. Search List products for one of its names before you send it again.
502STORE_DEPENDENCY_UNAVAILABLERetry with backoff
The Store was unreachable. Search List products for one of the batch's names, then retry with backoff if it isn't there.
401AUTH_API_KEY_VALIDATION_FAILEDRetry with backoff
The key could not be checked just now. Retry with backoff; the key itself may be fine.
{
  "meta": {
    "status": "error",
    "statusCode": 400,
    "code": "VALIDATION_FAILED"
  },
  "message": "Create products error.",
  "detail": "Category not found for name: Trading prizes",
  "solution": "Check your input parameters and try again."
}

Next step#

List productsGET/v1/productsRead the catalogue back with limit=100 to confirm every new product and its stocks.