Skip to main content

Promotions API

Sessiq has two kinds of discount:

  • Promotions reduce the price by a percentage or a fixed amount. A promotion either needs a code (SUMMER25) or applies automatically with no code.
  • Vouchers carry a money balance that pays for part or all of a booking. Gift cards are vouchers too; see Gift Cards API.

Customers use both during checkout. Staff manage them through the admin endpoints below.

Using codes at checkout​

Codes are applied to a booking hold. The endpoints are part of the booking flow and are documented in Bookings API:

MethodPathBody
POST/api/v1/bookings/{bookingId}/apply-promotion{ "promotionCode": "SUMMER25" }
POST/api/v1/bookings/{bookingId}/apply-voucher{ "voucherCode": "GIFT-ABCD-1234" }
DELETE/api/v1/bookings/{bookingId}/remove-promotionnone

There is no separate "validate code" endpoint. To check a code, apply it: an invalid code returns 400 and leaves the booking unchanged.

A code is accepted when all of these hold:

  • the promotion is active, and today is between startsAt and expiresAt;
  • the booking's start date is between bookingValidFrom and bookingValidTo, when set;
  • the booking matches the promotion's scope (site, product or variant);
  • the booking total reaches minSpendAmount, when set;
  • the total usage limit (usageLimitTotal) is not used up.

A booking carries one code at a time. To swap codes, remove the current one first.

Automatic promotions​

An automatic promotion has no code. It applies by itself to every booking it matches: the right product or site, a booking date inside bookingValidFrom–bookingValidTo, and the minimum spend. Typical use: "10% off Product X for bookings 1–24 December".

How it behaves:

  • It is applied when the hold is created, so the hold response already shows the reduced total and a Promotion: ... line in pricing.discounts.
  • If several automatic promotions match, the one with the highest priority wins.
  • A booking gets one promotion. When the customer enters a code, the booking keeps whichever discount is larger. A code wins a tie. If the automatic discount is larger, the code is not used and the response message says a larger automatic discount is already applied.
  • Removing a code brings the automatic promotion back.
  • Usage is recorded with the source automatic, so reports can tell it apart from code redemptions.

To create one, send "isAutomatic": true and leave out code (see below).

Admin: promotions​

All admin endpoints need a staff JWT with the promotions:manage permission, plus the x-tenant-id header.

MethodPathPurpose
GET/api/v1/admin/promotionsList promotions
POST/api/v1/admin/promotionsCreate a promotion
GET/api/v1/admin/promotions/{id}Get a promotion
PUT/api/v1/admin/promotions/{id}Update a promotion (send only the fields to change)
DELETE/api/v1/admin/promotions/{id}Delete, or deactivate if it has been used
POST/api/v1/admin/promotions/{id}/duplicateCopy a promotion
GET/api/v1/admin/promotions/{id}/usageUsage figures
GET/api/v1/admin/promotions/validate-codeCheck whether a code is still free (?code=)
POST/api/v1/admin/promotions/bulk-updateTurn many promotions on or off
GET/api/v1/admin/promotions/exportExport as JSON or CSV
POST/api/v1/admin/promotions/import/previewValidate a CSV without saving
POST/api/v1/admin/promotions/importImport a CSV

Create a promotion​

curl -X POST https://api.sessiq.com/api/v1/admin/promotions \
-H "Authorization: Bearer {token}" \
-H "x-tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{
"code": "SUMMER25",
"name": "Summer 25% off",
"promotionType": "percentage",
"discountValue": 25,
"maxDiscountAmount": 50,
"scopeType": "product",
"scopeProductIds": ["p_demo"],
"startsAt": "2026-06-01T00:00:00.000Z",
"expiresAt": "2026-08-31T23:59:59.000Z",
"usageLimitTotal": 500,
"isStackable": false,
"priority": 10,
"isActive": true,
"isPublic": true
}'
FieldRequiredDescription
codeUnless automatic3–50 characters. Must be unique in the tenant
isAutomaticNotrue for a codeless promotion. Default false
nameYes3–100 characters
descriptionNoUp to 500 characters
promotionTypeYespercentage or fixed_amount. See the note below
discountValueNoPercent (for percentage) or euros (for fixed_amount)
maxDiscountAmountNoCap for a percentage discount, in euros
minSpendAmountNoMinimum booking total, in euros
scopeTypeYessite, booking, product, variant or category
scopeProductIdsFor productOne or more product IDs. The promotion applies if the booking is for any of them
scopeIdFor variantThe variant ID
siteIdNoLimit to one site
startsAtYesWhen the code can first be used
expiresAtNoWhen the code stops working
bookingValidFrom, bookingValidToNoThe booking's start date must fall in this window. Separate from startsAt/expiresAt
usageLimitTotalNoTotal uses allowed
usageLimitPerCustomerNoStored with the promotion. Not yet enforced at checkout
priorityYes0–100. Higher wins when several automatic promotions match
isStackableYesStored with the promotion. A booking still carries only one promotion
isActiveYesWhether it can be used now
isPublicYesWhether it may be shown to customers
metadataNoFree-form object

site and booking scopes match every booking. product scope needs at least one product ID.

note

The API also accepts buy_x_get_y, bundle and free_shipping as promotionType, but they are not fully supported for bookings (free_shipping never gives a discount). Use percentage or fixed_amount.

The response is the saved promotion, with id, currentUsageCount, isAutomatic, createdAt and updatedAt added. code is null for automatic promotions.

List promotions​

GET /api/v1/admin/promotions accepts isActive, isPublic, promotionType, scopeType, search (code or name), startDate, endDate, page (default 1) and limit (default 20).

{
"data": [
{
"id": "promo_123",
"code": "SUMMER25",
"name": "Summer 25% off",
"isAutomatic": false
}
],
"meta": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}

Usage figures​

GET /api/v1/admin/promotions/{id}/usage:

{
"promotionId": "promo_123",
"code": "SUMMER25",
"name": "Summer 25% off",
"totalUses": 42,
"usageLimitTotal": 500,
"totalDiscountGiven": 830.5,
"uniqueCustomers": 40,
"recentApplications": [
{
"id": "pa_1",
"bookingId": "bk_abc123",
"customerId": null,
"appliedAt": "2026-07-02T10:00:00.000Z",
"discountAmount": 20
}
]
}

Bulk on/off​

POST /api/v1/admin/promotions/bulk-update with { "ids": ["promo_1", "promo_2"], "isActive": false }. The response lists what succeeded and what failed.

Admin: vouchers​

MethodPathPurpose
GET/api/v1/admin/vouchersList vouchers
POST/api/v1/admin/vouchersIssue one voucher
POST/api/v1/admin/vouchers/bulkIssue up to 1,000 vouchers at once
GET/api/v1/admin/vouchers/{id}Get a voucher
GET/api/v1/admin/vouchers/code/{code}Look up by code
GET/api/v1/admin/vouchers/customer/{email}A customer's vouchers
GET/api/v1/admin/vouchers/{id}/historyWhere the voucher was used
PUT/api/v1/admin/vouchers/{id}Update
POST/api/v1/admin/vouchers/{id}/adjustChange the balance: { "amount": -10, "reason": "..." }
POST/api/v1/admin/vouchers/{id}/reactivateReactivate an expired or deactivated voucher
DELETE/api/v1/admin/vouchers/{id}Deactivate
GET/api/v1/admin/vouchers/accounting-snapshotOutstanding balances for accounting
GET/api/v1/admin/vouchers/exportExport as JSON or CSV
POST/api/v1/admin/vouchers/import/previewValidate a CSV without saving
POST/api/v1/admin/vouchers/importImport a CSV

Issue a voucher​

{
"voucherType": "multi_use",
"originalValue": 50,
"customerEmail": "customer@example.com",
"expiresAt": "2027-06-30T23:59:59.000Z"
}
FieldRequiredDescription
voucherTypeYessingle_use, multi_use, gift_card or loyalty_credit
originalValueYesValue in euros
codeNoGenerated when omitted
usageLimitNoNumber of uses allowed
customerEmail, customerIdNoAssign to a customer
expiresAtNoExpiry date
siteIdNoLimit to one site
promotionIdNoLink to a promotion
redemptionNotesNoNotes shown to staff

Bulk issue takes count (1–1,000), an optional code prefix, the same voucherType, originalValue, usageLimit, expiresAt, siteId and promotionId, and an optional customerEmails list.

List filters: isActive, isRedeemed, voucherType, customerEmail, customerId, search (code), page, limit.

CSV import and export​

Export. GET /api/v1/admin/promotions/export and GET /api/v1/admin/vouchers/export take format=json (default) or format=csv, the same filters as the list endpoints, and page/pageSize (default 100, max 1,000). Voucher export also takes issuedAfter and issuedBefore. CSV comes back as a file download.

Import. Upload one CSV file as multipart/form-data in a field called file:

curl -X POST https://api.sessiq.com/api/v1/admin/promotions/import/preview \
-H "Authorization: Bearer {token}" \
-H "x-tenant-id: {tenantId}" \
-F "file=@promotions.csv"

Run import/preview first. It returns row counts, per-row errors, codes that already exist, and canProceed. Then send the same file to the import endpoint.

ImportRequired columnsOptional columns
Promotionscode, name, promotionType, scopeType, isStackable, priority, startsAt, isActive, isPublicdescription, scopeId, siteId, discountValue, minSpendAmount, maxDiscountAmount, usageLimitTotal, usageLimitPerCustomer, expiresAt, bookingValidFrom, bookingValidTo
VouchersvoucherType, originalValuecode, usageLimit, customerEmail, customerId, expiresAt, promotionId, siteId, redemptionNotes

The promotion import cannot create automatic or multi-product promotions. Create those through the API or the admin.