# Read product

Read one Store product in your community by its ID, with price, category, access rules and current stock.

- Endpoint: `GET https://api.returning.ai/v1/products/{productID}`
- Section: Store and rewards / Products
- Authentication: `Authorization: Bearer <API_KEY>` (Community API key)
- Permission: `store` (Shown in the dashboard as "Store")
- Retries: Read-only; exact retries are safe
- Guide: hand-written
- Verified: live, 2026-09-27
- Last updated: 26 Sep 2026
- Web page: https://docs.returning.ai/api-reference/store-products/read-product

## When to use this

- Confirm a product and its `stocks` after you create or update it.
- Fetch the current settings before a full update, so you only change what you mean to.
- Check an archived product, which List products leaves out.

**Instead:** Use [List products](https://docs.returning.ai/api-reference/store-products/list-products.md) instead to find a product ID by name or category.

## Authentication

- Header: `Authorization: Bearer <API_KEY>`
- Permission: `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.

## Request

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `productID` | `string` | Yes | Product ID, from List products or Create product with vouchers. (24-character hex ID) |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `fields` | `string` | No | Comma-separated fields to return. Include `_id` when you need the ID. `categoryID` also returns `category`; `category` itself is not accepted. Without it, every field is returned. (Product field names) |
| `lang` | `string` | No | Language code for translated names and descriptions, such as `th`. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | Yes | Community API key with `store`. (`Bearer <API_KEY>`) |

### Example request

```bash
curl --request GET \
  --url https://api.returning.ai/v1/products/66f000000000000000000101 \
  --header 'Authorization: Bearer <API_KEY>'
```

## Response

A `200` returns the product in `data`, including archived products (`isArchived: true`). `stocks` counts voucher codes that are unsold and not expired; the codes themselves are never returned. Branch on the HTTP status and `meta.code`, never on `message`.

**Note:** The product also includes redemption method, display field and refund settings managed in the dashboard. Build against the fields listed here.

### Response fields

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| `meta` | `object` | always | Status details. |
| `meta.status` | `string` | always | Result of the request. (`success`) |
| `meta.statusCode` | `integer` | always | The HTTP status, repeated. |
| `meta.code` | `string` | always | Machine-readable result code. (`PRODUCT_RETRIEVED`) |
| `message` | `string` | always | Human-readable summary. Do not branch on it. |
| `data` | `object` | always | The product. |
| `data._id` | `string` | - | Product ID. Use it to read or update the product. |
| `data.name` | `string` | - | Product name shown in the Store. |
| `data.price` | `number` | - | Price in coins. |
| `data.categoryID` | `string` | - | Category ID, or `null` when the product has no category. |
| `data.category` | `string` | - | Category name, or `null` when the product has no category or its category was deleted. |
| `data.stocks` | `integer` | - | Vouchers left to buy: codes that are unsold and not expired. Voucher codes are never returned. |
| `data.isArchived` | `boolean` | - | `true` when the product is archived and left out of List products and the Store. |
| `data.image` | `string` | - | Image URL, or `null` when there is no image. |
| `data.description` | `string` | - | Product description as HTML. Only basic formatting tags are kept. |
| `data.redemptionInstructions` | `string` | - | Redemption instructions for the trader, or `null`. |
| `data.discountPrice` | `number` | - | Discounted price in coins, charged between the discount dates while the discount is on. |
| `data.isDiscountEnabled` | `boolean` | - | `true` when the discount is switched on. |
| `data.discountStartDate` | `string` | - | When the discount starts, ISO 8601 UTC, or `null`. |
| `data.discountEndDate` | `string` | - | When the discount ends, ISO 8601 UTC, or `null`. |
| `data.userInformation` | `object` | - | What the trader is asked for when they redeem. |
| `data.userInformation.isEnabled` | `boolean` | - | `true` when the trader is asked for details. |
| `data.userInformation.shouldCollectName` | `boolean` | - | `true` to ask for their name. |
| `data.userInformation.shouldCollectPhone` | `boolean` | - | `true` to ask for their phone number. |
| `data.userInformation.shouldCollectAddress` | `boolean` | - | `true` to ask for their address. |
| `data.shouldDisplayRemainingQuantity` | `boolean` | - | `true` to show traders how many are left. |
| `data.shouldPrioritiesExpiringVouchers` | `boolean` | - | `true` to prioritise the vouchers closest to expiry. |
| `data.expiringVoucherHighlight` | `object` | - | Highlight for vouchers close to expiry. |
| `data.expiringVoucherHighlight.isEnabled` | `boolean` | - | `true` when the highlight is on. |
| `data.expiringVoucherHighlight.duration` | `integer` | - | How close to expiry, in `timeframe` units. |
| `data.expiringVoucherHighlight.timeframe` | `string` | - | Unit for `duration`, such as `days`. |
| `data.isProductAccessEnabled` | `boolean` | - | `true` when the product's own rules apply; `false` when it follows its category. |
| `data.productPermission` | `object[]` | - | Access rules. An empty array means the product has no rules of its own. |
| `data.productPermission.isEnabled` | `boolean` | - | `true` when the rule is active. |
| `data.productPermission.type` | `string` | - | What the rule matches. (`user`, `role`, `tag` or `role_combination`) |
| `data.productPermission.permission` | `object` | - | The traders the rule covers, by readable name. |
| `data.productPermission.permission.roleNames` | `string[]` | - | Role names. |
| `data.productPermission.permission.tagNames` | `string[]` | - | Tag names. |
| `data.productPermission.permission.usernames` | `string[]` | - | Usernames. |
| `data.productPermission.permission.userIDs` | `string[]` | - | Record IDs of the traders the rule names. Send `usernames` when you write rules. |
| `data.productPermission.permission.combination` | `object[]` | - | Role or tag groups a trader must hold together. |
| `data.productPermission.permission.combination.type` | `string` | - | `role` or `tag`. |
| `data.productPermission.permission.combination.values` | `string[]` | - | Role or tag names in the group. |
| `data.productPermission.permission.isSelectAll` | `boolean` | - | `true` when the rule covers all traders. |
| `data.productPermission.purchaseAccess` | `object` | - | Special price for traders the rule covers. |
| `data.productPermission.purchaseAccess.specialPrice` | `number` | - | Special price in coins. |
| `data.productPermission.purchaseAccess.isSpecialPriceEnabled` | `boolean` | - | `true` when the special price applies. |
| `data.productPermission.purchaseLimit` | `object` | - | How often a covered trader can buy. |
| `data.productPermission.purchaseLimit.isPurchaseLimitEnabled` | `boolean` | - | `true` when the limit applies. |
| `data.productPermission.purchaseLimit.quantity` | `number` | - | Purchases allowed in each window. |
| `data.productPermission.purchaseLimit.intervalCount` | `number` | - | Length of the window, in `intervalUnit`s. |
| `data.productPermission.purchaseLimit.intervalUnit` | `string` | - | Unit of the window, such as `months`. |
| `data.productPermission.accessLevel` | `string` | - | What covered traders can do. (`full-access`, `view-only` or `access-denied`) |
| `data.productPermission.isExclusiveTagEnabled` | `boolean` | - | `true` to show the exclusive tag. |
| `data.purchaseStatusOverride` | `object` | - | The status new orders for this product start in, when set. |
| `data.purchaseStatusOverride.isEnabled` | `boolean` | - | `true` when new orders start in `status`. |
| `data.purchaseStatusOverride.status` | `string` | - | Order status name, or `null` when the category or community default applies. |
| `data.totalOrders` | `integer` | - | Orders placed for this product. |
| `data.communityID` | `string` | - | Your community ID. |
| `data.createdAt` | `string` | - | When the product was created, ISO 8601 UTC. |
| `data.updatedAt` | `string` | - | When the product last changed, ISO 8601 UTC. |

### Example response (200)

```json
{
  "meta": {
    "status": "success",
    "statusCode": 200,
    "code": "PRODUCT_RETRIEVED"
  },
  "message": "Read product 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": 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-26T09:15:00.000Z"
  }
}
```

## Errors

Every error carries its code in `meta.code`, with the reason in `detail`. A product in another community returns `404`, the same as a missing one.

### Fix the request

| Status | Code | What to do |
| --- | --- | --- |
| 400 | `VALIDATION_FAILED` | `productID` isn't a 24-character hex ID, or `fields` names a field that can't be returned. `detail` says which. |
| 401 | `AUTH_API_KEY_REQUIRED` | No key was sent. Send `Authorization: Bearer <API_KEY>`. |
| 401 | `AUTH_API_KEY_INVALID` | The key is unknown, expired or malformed. Use a current Community API key. |
| 403 | `AUTH_PERMISSION_REQUIRED` | The key lacks `store`. Add the permission in Settings > Integration > API Keys. |

### Fix the data

| Status | Code | What to do |
| --- | --- | --- |
| 404 | `STORE_RESOURCE_NOT_FOUND` | No product with this ID in your community. Find the right ID with List products; a deleted product returns this too. |

### Retry with backoff

| Status | Code | What to do |
| --- | --- | --- |
| 502 | `STORE_DEPENDENCY_UNAVAILABLE` | The Store is briefly unavailable. Retry the same request with exponential backoff. |
| 500 | `INTERNAL_ERROR` | Retry the same request with exponential backoff. If it keeps failing, contact Returning.AI with the time of the request. |
| 401 | `AUTH_API_KEY_VALIDATION_FAILED` | The key could not be checked just now. Retry with backoff; the key itself may be fine. |

**Retries:** This endpoint is read-only, so retrying the exact same request is safe after a network error, a `500` or a `502`. Use bounded exponential backoff. Over the [rate limit](https://docs.returning.ai/how-it-works.md#rate-limits), requests return `429`; wait for the window to reset, then retry.

## Next step

- [Update product and append vouchers](https://docs.returning.ai/api-reference/store-products/update-product-and-append-vouchers.md): `PUT /v1/products/{productID}`. Change the product or add stock, starting from the values you just read.
- [Need another product? List products](https://docs.returning.ai/api-reference/store-products/list-products.md): `GET /v1/products`.
