Discount code provider
Create loyalty reward discount codes in your own system. Cevoid sends a signed request when a member claims a reward, and your endpoint answers with the code.
A discount code provider is an HTTPS endpoint on your side that creates discount codes for loyalty rewards. When a member claims a reward that gets its codes from your API, Cevoid sends your endpoint a signed discount-code.create request. Your endpoint creates the discount and answers with the code, and Cevoid gives that code to the member.
Use it when your discounts live in a system Cevoid cannot write to directly, such as your own checkout, a commerce platform without a Cevoid integration, or a promotion engine.
Unlike a Flow webhook, this is a request and response: Cevoid waits for your answer and uses the code in it.
How it works
- A developer creates a destination of the type Discount code provider in Settings โ Developers โ Webhooks and stores its signing secret.
- A marketer creates a discount code or free product reward and picks one of the options created by your API. With one provider destination, the reward uses it. With several, the marketer picks one.
- A member claims the reward. Cevoid sends
discount-code.createto your endpoint. - Your endpoint creates the discount and answers
200with the code. The member sees the code at once.
Rewards come in two modes:
| Mode | When Cevoid sends the request | mode |
|---|---|---|
| Unique codes | Each time a member claims the reward. One code per member reward. | unique |
| Same code for all | Once, when the marketer chooses Create discount code in your system in the reward settings. suggested_code is the code the marketer chose. Every member receives the code you return. | shared |
A reward cannot go live until it has a provider destination, and a shared reward also needs its code created.
Set up the destination
- In Settings โ Developers โ Webhooks, choose Create destination and the type Discount code provider.
- Enter your endpoint's HTTPS URL. Cevoid cannot reach
localhostor private addresses. - Copy the signing secret. Cevoid shows it once.
Requests are signed exactly like every other Cevoid webhook, so verify them the same way. See Verify webhook signatures.
The request
Every request is an HTTPS POST with a JSON body and these headers:
| Header | Value |
|---|---|
content-type | application/json |
user-agent | Cevoid-Webhooks/1.0 |
webhook-id | The request ID, the same as id in the body. |
webhook-timestamp | Unix seconds when this attempt was signed. Every attempt gets a new one. |
webhook-signature | One or more v1,<base64> signatures, separated by spaces. |
A unique code request for a 10% reward, and every field of the body:
{
"created_at": "2026-11-02T09:30:00.000Z",
"data": {
"discount": {
"combines_with": {
"order": false,
"product": true,
"shipping": true
},
"expires_at": "2026-12-02T09:30:00.000Z",
"once_per_customer": true,
"percentage": 10,
"starts_at": "2026-11-02T09:30:00.000Z",
"usage_limit": 1,
"value_type": "PERCENTAGE"
},
"market": {
"currency": "SEK",
"id": "mar_sweden"
},
"member_reward": {
"id": "rwd_abc123"
},
"mode": "unique",
"profile": {
"email": "member@example.com",
"id": "prf_abc123"
},
"reward": {
"id": "rwi_k2m7q4x9b3d5h8n6",
"name": "10% off for members"
},
"suggested_code": "VIPK7M2PQ4X"
},
"id": "evt_k5q45bdbf99hpev3",
"type": "discount-code.create",
"version": 1
}Show all 38 linesShow fewer lines
versionnumberrequiredPayload version. Bumped only for breaking changes. One of 1.
idstringrequiredStable request id (evt_ plus 16 characters, like every Cevoid webhook id), also sent as the webhook-id header. A retry of the same creation repeats it, so treat it as your deduplication key and return the code already created for it.
typestringrequiredAlways discount-code.create. One of discount-code.create.
created_atstring<date-time>requiredWhen this request was built, as an ISO 8601 timestamp. A retry carries a new value.
dataobjectrequiredObject keys are sorted. A field without a value is left out rather than sent as null.
Respond with the code
Answer with 200 and a JSON body:
{
"code": "VIPK7M2PQ4X",
"expires_at": "2026-12-02T09:30:00.000Z",
"external_id": "promo_8812"
}codestringrequiredThe discount code the member will enter at checkout. Return the code already created for this request id when the request is a retry.
external_idstringYour identifier for the created discount, shown to merchants beside the code and useful for support.
expires_atstring<date-time>When the code stops working, as an ISO 8601 timestamp in UTC or with an offset. Overrides the expiry Cevoid requested in discount.expires_at.
Cevoid reads only these fields and ignores the rest, so you can return your own record as long as it has code. The body must be JSON and at most 2,048 bytes.
Deduplicate on the request ID
A retry of the same creation sends the same id and webhook-id. Store the code you created for each id, and when an id arrives again, return the code you already made instead of creating a second discount.
The ID is derived from what the code is for:
- Unique: one ID per member reward. Retrying a failed claim, also from the dashboard, repeats it.
- Shared: one ID per reward, code and discount value. Choosing Create discount code in your system again repeats it; a new code or value gets a new ID.
How Cevoid reads your response
Cevoid waits 10 seconds for the response and does not follow redirects.
| Your response | What Cevoid does |
|---|---|
2xx with a valid body | Gives the code to the member |
2xx without a usable code, not JSON, or over 2,048 bytes | Marks the reward failed |
408, 425, 429 or any 5xx | Tries again |
| No response within 10 seconds, or a network error | Tries again |
Any other 3xx or 4xx | Marks the reward failed at once |
Cevoid makes up to 3 attempts for one claim, a few seconds apart. When they all fail, the member reward is marked failed with the reason, and a merchant can choose Retry fulfillment on it later. The retry uses the same request ID.
Return a 4xx only when the request can never succeed, for example when the terms are invalid on your side. Return a 5xx or 429 for a problem that may pass.
Build the endpoint
This receiver verifies the signature, returns the code it already created for a repeated ID, and otherwise creates a discount. createDiscount and the code store stand for your own system.
// app/cevoid/discount-codes/route.ts
import { Webhook } from "standardwebhooks"
const webhook = new Webhook(process.env.CEVOID_WEBHOOK_SECRET ?? "")
export async function POST(request: Request) {
// The raw body: parsing and re-serializing JSON breaks the signature.
const body = await request.text()
try {
webhook.verify(body, Object.fromEntries(request.headers))
} catch {
return new Response("Invalid signature", { status: 401 })
}
const { id, type, data } = JSON.parse(body)
if (type !== "discount-code.create") return new Response(null, { status: 400 })
// A retry repeats the ID: answer with the code made the first time.
const existing = await findCodeByRequestId(id)
if (existing) return Response.json(existing)
const created = await createDiscount({
code: data.suggested_code,
terms: data.discount,
customerEmail: data.profile?.email,
productId: data.product?.external_id,
})
const reply = { code: created.code, external_id: created.id }
await saveCodeForRequestId(id, reply)
return Response.json(reply)
}Show all 31 linesShow fewer lines
import express from "express"
import { Webhook } from "standardwebhooks"
const app = express()
const webhook = new Webhook(process.env.CEVOID_WEBHOOK_SECRET)
app.post("/cevoid/discount-codes", express.raw({ type: "application/json" }), async (req, res) => {
const body = req.body.toString("utf8")
try {
webhook.verify(body, req.headers)
} catch {
return res.status(401).send("Invalid signature")
}
const { id, type, data } = JSON.parse(body)
if (type !== "discount-code.create") return res.sendStatus(400)
// A retry repeats the ID: answer with the code made the first time.
const existing = await findCodeByRequestId(id)
if (existing) return res.json(existing)
const created = await createDiscount({
code: data.suggested_code,
terms: data.discount,
customerEmail: data.profile?.email,
productId: data.product?.external_id,
})
const reply = { code: created.code, external_id: created.id }
await saveCodeForRequestId(id, reply)
res.json(reply)
})Show all 31 linesShow fewer lines
Answer within 10 seconds. Creating the discount is the work Cevoid waits for, so keep the request path short.
Check your endpoint
Before a reward goes live:
- Create a reward that uses Same code for all from your API and choose Create discount code in your system. Your endpoint receives a
sharedrequest with the reward's code insuggested_code, and the reward settings confirm that the code was created. - Open the destination in Settings โ Developers โ Webhooks. Its charts show how many requests failed and how fast your endpoint answered.
A shared request leaves out member_reward, profile and market, so also check that your endpoint handles a unique request with the fields above.
Archived destinations
Archiving a destination that rewards use is allowed. Claims of those rewards then fail with a reason until a marketer points the rewards at another provider destination. Failed claims can be retried after that, and keep their request IDs.