Validate Coupon
Check whether a coupon can be redeemed — and what discount it would produce — without consuming it. Use this at checkout to show a customer their saving before they commit.
Endpoint
POST /v1/coupons/validate
Headers
| Header | Type | Description | Required |
|---|---|---|---|
X-Client-Id | String | Your client ID | ✅ Yes |
X-Client-Secret | String | Your client secret | ✅ Yes |
Content-Type | String | application/json | ✅ Yes |
Request Body
| Field | Type | Description | Required |
|---|---|---|---|
code | String | The code the customer presented | ✅ Yes |
order | Object | The order to evaluate against | ❌ No |
Order Object
| Field | Type | Description |
|---|---|---|
amount | Number | Order total |
items | Array | Order line items |
items[].productId | String | Product identifier |
items[].categoryId | String | Category identifier |
items[].quantity | Integer | Units of this line |
items[].unitPrice | Number | Price per unit |
With an order, applicability is evaluated and the discount is computed. Without one, only the coupon's own state is checked — active, not expired, usage remaining, campaign live. Omit the order when a customer applies a code before a basket exists.
Example Request
curl --location 'https://stage-platform-exlr8-incentives.exlr8now.com/v1/coupons/validate' \
--header 'X-Client-Id: YOUR_CLIENT_ID' \
--header 'X-Client-Secret: YOUR_CLIENT_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"code": "SAVE20-K4M9XQ",
"order": {
"amount": 4500,
"items": [
{
"productId": "sku-9981",
"categoryId": "electronics",
"quantity": 1,
"unitPrice": 4500
}
]
}
}'
Response
Valid — 200 OK
{
"success": true,
"data": {
"couponId": "7c8d9e0f-1a2b-4c3d-9e4f-5a6b7c8d9e0f",
"valid": true,
"result": {
"discountAmount": 900,
"description": "20% off (capped at 2000)"
}
}
}
Not valid — also 200 OK
{
"success": true,
"data": {
"couponId": "7c8d9e0f-1a2b-4c3d-9e4f-5a6b7c8d9e0f",
"valid": false,
"reason": "coupon has expired",
"code": "CV_EXPIRED"
}
}
:::info A rejected coupon is not an error
Validation answers a question, and "no" is a valid answer. The HTTP status stays 200 and success stays true — branch on data.valid, not on the status code. Only malformed requests and authentication failures produce non-2xx responses here.
:::
Response Fields
| Field | Type | Description |
|---|---|---|
couponId | String | The coupon the code resolved to |
valid | Boolean | Whether redemption would currently succeed |
reason | String | Human-readable explanation when valid is false |
code | String | Machine-readable error code when valid is false |
result | Object | The discount that redemption would produce — only when valid and an order was supplied |
result.discountAmount | Number | Money off the order |
result.freeShipping | Boolean | Present for FREE_SHIPPING coupons |
result.description | String | Human-readable summary, suitable for display |
Show reason to the customer and branch on code. The values it can take are the CV_* codes in Error Codes.
The discount is not a quote
result.discountAmount reflects the order you sent, at that moment. The basket can change before payment, and the coupon can be consumed by another session in between.
Always apply the discount returned by Redeem Coupon, never the one from validation. Redemption recomputes from the order you send it, and that figure is the one that was actually committed. Treat validation as a display aid.
Applicability
When a coupon is scoped to CATEGORIES or PRODUCTS, only matching order lines form the discount base. An order with no matching line is not valid, with code CV_NOT_APPLICABLE.
This is why line items matter: sending only an amount for a scoped coupon produces a spurious rejection, because no line can match. Send the real basket.
Error Responses
Almost every outcome is a 200 with valid: false — including a code that matches no coupon. Only these are true errors:
| Error code | HTTP | Cause |
|---|---|---|
BAD_REQUEST | 400 | Missing code, malformed order, or an unknown field |
TEMPLATE_NOT_FOUND | 404 | An order was supplied but the coupon's template no longer exists |
INTERNAL_SERVER_ERROR | 500 | Platform fault |
Codes returned inside data.code
These accompany valid: false at HTTP 200:
| Code | Meaning |
|---|---|
CV_INVALID | No coupon matches this code |
CV_ALREADY_USED | The coupon is not ACTIVE — used, or voided |
CV_EXPIRED | Past its expiry date |
CV_USAGE_LIMIT_REACHED | The usage limit is exhausted |
CV_NOT_APPLICABLE | The discount could not be computed for this order — including an order below the coupon's minimum value |
PROGRAM_INACTIVE | The campaign is not ACTIVE |
PROGRAM_NOT_FOUND | The campaign no longer exists |
:::caution Validation does not check budgets
A coupon can validate cleanly and still fail to redeem. Redemption charges the discount against the template's and the campaign's value budgets, and neither is evaluated here — so TEMPLATE_LIMIT_EXCEEDED and CAMPAIGN_LIMIT_EXCEEDED can only surface at redemption. Handle them there regardless of what validation said.
:::
Related Endpoints
- Redeem Coupon — commit the redemption
- Issue Coupon
- How Redemption Works