# 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.

Breadcrumb: Webhooks › Discount code provider · Updated: 2026-10-27
Docs index: https://docs.cevoid.com/developers/llms.txt

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

1. A developer creates a destination of the type **Discount code provider** in Settings → Developers → Webhooks and stores its signing secret.
2. 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.
3. A member claims the reward. Cevoid sends `discount-code.create` to your endpoint.
4. Your endpoint creates the discount and answers `200` with 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

1. In Settings → Developers → Webhooks, choose Create destination and the type Discount code provider.
2. Enter your endpoint's HTTPS URL. Cevoid cannot reach `localhost` or private addresses.
3. 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](https://docs.cevoid.com/developers/webhooks/verify-signatures.md).

## 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:

```json title="Example request"
{
  "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
}
```

- `version` (`number`, required): Payload version. Bumped only for breaking changes. One of `1`.
- `id` (`string`, required): Stable 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.
- `type` (`string`, required): Always `discount-code.create`. One of `discount-code.create`.
- `created_at` (`string<date-time>`, required): When this request was built, as an ISO 8601 timestamp. A retry carries a new value.
- `data` (`object`, required)
  - `data.mode` (`string`, required): `unique`: create a code for one member reward; `member_reward` and `profile` are present. `shared`: create one code every member of the reward will receive; sent once from the reward settings. One of `unique`, `shared`.
  - `data.suggested_code` (`string`, required): A code Cevoid proposes. For `shared` requests it is the code the merchant chose in Cevoid; for `unique` requests it is the reward prefix followed by 8 characters. Use it, or return your own `code`. A retry proposes the same code.
  - `data.reward` (`object`, required): The reward the code belongs to.
    - `data.reward.id` (`string`, required): Stable opaque identifier of the reward configuration in Cevoid. The same reward always sends the same id, so you can map it to a discount rule on your side.
    - `data.reward.name` (`string`, required): The reward title as the merchant named it in Cevoid.
  - `data.member_reward` (`object`): The member reward being fulfilled. Present on `unique` requests, absent on `shared` ones.
    - `data.member_reward.id` (`string`, required): Public id of the member reward being fulfilled (`rwd_...`).
  - `data.profile` (`object`): The shopper who earned the reward. Present on `unique` requests, absent on `shared` ones.
    - `data.profile.id` (`string`, required): Public id of the shopper profile that earned the reward (`prf_...`).
    - `data.profile.email` (`string<email>`): The profile email, when Cevoid has one in a standard format. Use it to restrict the code to that customer.
  - `data.market` (`object`): The Cevoid market the code is for. Absent when the reward is not tied to a market.
    - `data.market.id` (`string`, required): Public id of the Cevoid market the member claimed the reward in (`mar_...`).
    - `data.market.currency` (`string`, required): ISO 4217 currency code, for example `SEK`.
  - `data.discount` (`object`, required): The discount the merchant configured for the reward.
    - `data.discount.value_type` (`string`, required): What the code gives: a fixed amount off, a percentage off, or free shipping. One of `FIXED_AMOUNT`, `PERCENTAGE`, `FREE_SHIPPING`.
    - `data.discount.percentage` (`number`): Percentage off the order, from 0 to 100. Present when `value_type` is `PERCENTAGE`.
    - `data.discount.amount` (`number`): Amount off in `currency`. Present when `value_type` is `FIXED_AMOUNT`.
    - `data.discount.currency` (`string`): ISO 4217 currency of `amount`, `minimum_spend` and `free_shipping.maximum_shipping_amount`. Present when any of them is.
    - `data.discount.minimum_spend` (`number`): Order subtotal, in `currency`, the shopper must reach before the code applies. Absent when there is no minimum.
    - `data.discount.minimum_quantity` (`integer`): Number of items the order must contain before the code applies. Absent when there is no minimum.
    - `data.discount.usage_limit` (`integer`): How many orders may use the code in total. Absent when unlimited.
    - `data.discount.once_per_customer` (`boolean`, required): Whether each customer may use the code only once.
    - `data.discount.starts_at` (`string<date-time>`, required): When the code becomes valid, as an ISO 8601 timestamp.
    - `data.discount.expires_at` (`string<date-time>`): When the code should stop working, as an ISO 8601 timestamp, computed from the reward settings. Absent when the reward never expires. Return `expires_at` in the response to override it.
    - `data.discount.combines_with` (`object`, required): Which other discount kinds the code may be combined with.
      - `data.discount.combines_with.order` (`boolean`, required): Whether the code may be combined with order discounts.
      - `data.discount.combines_with.product` (`boolean`, required): Whether the code may be combined with product discounts.
      - `data.discount.combines_with.shipping` (`boolean`, required): Whether the code may be combined with shipping discounts.
    - `data.discount.free_shipping` (`object`): Present when `value_type` is `FREE_SHIPPING`.
      - `data.discount.free_shipping.maximum_shipping_amount` (`number`): Only shipping rates up to this amount, in `discount.currency`, are free. Absent when every rate qualifies.
  - `data.product` (`object`): Present for product rewards: the discount applies to this product only.
    - `data.product.id` (`string`): Public id of the product in the Cevoid catalog (`prd_...`), when it resolves.
    - `data.product.external_id` (`string`): The product id in the merchant platform the catalog was synced from, when known.
    - `data.product.url` (`string`): The product page URL the member is sent to.

Object 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:

```json title="Example response"
{
  "code": "VIPK7M2PQ4X",
  "expires_at": "2026-12-02T09:30:00.000Z",
  "external_id": "promo_8812"
}
```

- `code` (`string`, required): The discount code the member will enter at checkout. Return the code already created for this request `id` when the request is a retry.
- `external_id` (`string`): Your identifier for the created discount, shown to merchants beside the code and useful for support.
- `expires_at` (`string<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.

```ts title="Next.js"
// 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)
}
```

```js title="Express"
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)
})
```

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:

1. Create a reward that uses **Same code for all from your API** and choose Create discount code in your system. Your endpoint receives a `shared` request with the reward's code in `suggested_code`, and the reward settings confirm that the code was created.
2. 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.

## Related articles

- [Verify webhook signatures](https://docs.cevoid.com/developers/webhooks/verify-signatures.md)
- [Webhooks via Flows](https://docs.cevoid.com/developers/webhooks.md)

---

Canonical: https://docs.cevoid.com/developers/webhooks/discount-code-provider
