Returning.AIDevelopers
v1

API reference / Store / Products

.md

Update products in bulk

Change up to 100 Store products in one call, sending only the fields that change for each, from a JSON array or a CSV file.

Last updated 26 Sep 2026API v1

Method
PUT
Path
https://api.returning.ai/v1/products/bulk
Permission
store
Retries
All-or-nothing; re-sent codes return 409

When to use this

  • You reprice or archive many products at once.
  • You restock several products with new voucher codes in one request.
  • Your team keeps the catalogue in a spreadsheet and uploads it as CSV.

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 before anything is saved, then all the products and codes are saved together: if any item fails, nothing changes. Fields an item leaves out keep their current values. New codes are added as stock; existing codes are never removed or replaced.

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 PUT \
  --url https://api.returning.ai/v1/products/bulk \
  --header 'Authorization: Bearer <API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '[
    {
      "productID": "66f000000000000000000101",
      "price": 450
    }
  ]'

Response#

A 200 returns the updated products in data, in the order you sent them, and meta.updated counts them. stocks includes any codes you added, unless they have already expired. 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.

Eg200

code#stringALWAYS
Machine-readable result code.

RulePRODUCTS_BULK_UPDATED

updated#integerALWAYS
Number of products updated.

Eg1

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

Eg"Update products success."

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

Eg[ ... ]

_id#stringALWAYS
Product ID. Use it to read or update the product.

Eg"66f000000000000000000101"

name#string
Product name shown in the Store.

Eg"$25 Trading Credit"

price#number
Price in coins.

Eg450

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.

Eg2

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": 7, "timeframe": "days"}

isEnabled#boolean
true when the highlight is on.

Egfalse

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

Eg7

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-26T10:00:00.000Z"

{
  "meta": {
    "status": "success",
    "statusCode": 200,
    "updated": 1,
    "code": "PRODUCTS_BULK_UPDATED"
  },
  "message": "Update 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": 450,
      "categoryID": "66f000000000000000000201",
      "category": "Trading rewards",
      "stocks": 2,
      "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": 7,
        "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-26T10:00:00.000Z"
    }
  ]
}

Errors#

Every error carries its code in meta.code, with the reason in detail as a sentence. An item that breaks a rule is named by position, such as Bulk product item 2 validation failed:, and a CSV error names its row.

Fix the request04

400VALIDATION_FAILEDFix the request
The body isn't a raw array, has over 100 items or a repeated productID, or an item breaks a rule or names something with no match. detail says which. Nothing was saved.
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.

Fix the data02

404STORE_RESOURCE_NOT_FOUNDFix the data
One productID isn't in your community, so nothing was saved. Correct or remove that item and send the batch again.
409STORE_RESOURCE_CONFLICTFix the data
An item's vouchers has codes that product already has. Nothing was saved; drop those codes and send again.

Retry with backoff03

500INTERNAL_ERRORRetry with backoff
The batch is saved in full or not at all. Read one of the products back to see which, then send the same batch again if needed.
502STORE_DEPENDENCY_UNAVAILABLERetry with backoff
The Store was unreachable. Read one of the products back, then send the same batch again with backoff.
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": "Update products error.",
  "detail": "Bulk product JSON body must be a raw array, not an object wrapper.",
  "solution": "Check the request parameters and try again."
}

Next step#

List productsGET/v1/productsRead the catalogue back with limit=100 to confirm every change.