Skip to content
Get API key (opens in a new tab)

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

  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:

ModeWhen Cevoid sends the requestmode
Unique codesEach time a member claims the reward. One code per member reward.unique
Same code for allOnce, 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.

The request

Every request is an HTTPS POST with a JSON body and these headers:

HeaderValue
content-typeapplication/json
user-agentCevoid-Webhooks/1.0
webhook-idThe request ID, the same as id in the body.
webhook-timestampUnix seconds when this attempt was signed. Every attempt gets a new one.
webhook-signatureOne or more v1,<base64> signatures, separated by spaces.

A unique code request for a 10% reward, and every field of the body:

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
}
Show all 38 linesShow fewer lines
versionnumberrequired

Payload version. Bumped only for breaking changes. One of 1.

idstringrequired

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.

typestringrequired

Always discount-code.create. One of discount-code.create.

created_atstring<date-time>required

When this request was built, as an ISO 8601 timestamp. A retry carries a new value.

dataobjectrequired
data properties
modestringrequired

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.

suggested_codestringrequired

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.

rewardobjectrequired

The reward the code belongs to.

reward properties
idstringrequired

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.

namestringrequired

The reward title as the merchant named it in Cevoid.

member_rewardobject

The member reward being fulfilled. Present on unique requests, absent on shared ones.

member_reward properties
idstringrequired

Public id of the member reward being fulfilled (rwd_...).

profileobject

The shopper who earned the reward. Present on unique requests, absent on shared ones.

profile properties
idstringrequired

Public id of the shopper profile that earned the reward (prf_...).

emailstring<email>

The profile email, when Cevoid has one in a standard format. Use it to restrict the code to that customer.

marketobject

The Cevoid market the code is for. Absent when the reward is not tied to a market.

market properties
idstringrequired

Public id of the Cevoid market the member claimed the reward in (mar_...).

currencystringrequired

ISO 4217 currency code, for example SEK.

discountobjectrequired

The discount the merchant configured for the reward.

discount properties
value_typestringrequired

What the code gives: a fixed amount off, a percentage off, or free shipping. One of FIXED_AMOUNT, PERCENTAGE, FREE_SHIPPING.

percentagenumber

Percentage off the order, from 0 to 100. Present when value_type is PERCENTAGE.

amountnumber

Amount off in currency. Present when value_type is FIXED_AMOUNT.

currencystring

ISO 4217 currency of amount, minimum_spend and free_shipping.maximum_shipping_amount. Present when any of them is.

minimum_spendnumber

Order subtotal, in currency, the shopper must reach before the code applies. Absent when there is no minimum.

minimum_quantityinteger

Number of items the order must contain before the code applies. Absent when there is no minimum.

usage_limitinteger

How many orders may use the code in total. Absent when unlimited.

once_per_customerbooleanrequired

Whether each customer may use the code only once.

starts_atstring<date-time>required

When the code becomes valid, as an ISO 8601 timestamp.

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

combines_withobjectrequired

Which other discount kinds the code may be combined with.

combines_with properties
orderbooleanrequired

Whether the code may be combined with order discounts.

productbooleanrequired

Whether the code may be combined with product discounts.

shippingbooleanrequired

Whether the code may be combined with shipping discounts.

free_shippingobject

Present when value_type is FREE_SHIPPING.

free_shipping properties
maximum_shipping_amountnumber

Only shipping rates up to this amount, in discount.currency, are free. Absent when every rate qualifies.

productobject

Present for product rewards: the discount applies to this product only.

product properties
idstring

Public id of the product in the Cevoid catalog (prd_...), when it resolves.

external_idstring

The product id in the merchant platform the catalog was synced from, when known.

urlstring

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:

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

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_idstring

Your 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 responseWhat Cevoid does
2xx with a valid bodyGives the code to the member
2xx without a usable code, not JSON, or over 2,048 bytesMarks the reward failed
408, 425, 429 or any 5xxTries again
No response within 10 seconds, or a network errorTries again
Any other 3xx or 4xxMarks 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:

  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.