Promotions
A discount with a name and, optionally, a window. Reading takes promotions:read, writing promotions:write, and neither implies the other.
The behaviour behind these endpoints — what a promotion does to an offer, and when — is on the guide. This page is the field-by-field.
GET/promotions
scopepromotions:readYour promotions, and which one new offers get.
meta.next_cursor.curl "https://api.zinevu.com/api/public/v1/promotions?active_only=1" \ -H "Authorization: Bearer $ZINEVU_API_KEY"{ "data": [ { "id": 41, "name": "Najaarsactie", "code": "autumn2026", "discount_pct": 10, "description": "10% najaarskorting", "valid_from": "2026-08-31T22:00:00+00:00", "valid_until": "2026-09-30T21:59:59+00:00", "is_active": true, "is_live": true, "is_applied_to_new_offers": true, "created_at": "2026-08-28T12:02:11+00:00", "updated_at": "2026-08-28T12:02:11+00:00" } ]}GET/promotions/{id}
scopepromotions:readOne promotion, with the number of offers carrying it.
POST/promotions
scopepromotions:writeStart a promotion. Applies to new offers, never to offers that exist.
// -> 201{ "data": { "id": 41, "is_live": true, "is_applied_to_new_offers": true, "backfill_preview": { "eligible": 12, "applied": 0, "skipped_pinned": 1, "skipped_dealer_set": 3 }, "also_live": [] }}PATCH/promotions/{id}
scopepromotions:writeChange one — partial, so send only what moves.
Everything you leave out keeps its stored value; null is a real value and clears the field. The window is checked against the row as it will be, so moving one edge past the other is refused even when the other one is not in your payload.
DELETE/promotions/{id}
scopepromotions:writeEnd one. Refused once if offers carry it; they keep their discount either way.
// -> 409 the first time{ "meta": { "message": "12 offers carry this promotion. …" }, "errors": { "code": "promotion_in_use", "offers_count": 12, "is_live": true }}// -> 200 with ?force=1{ "data": { "id": 41, "deleted": true, "offers_keeping_their_discount": 12 } }{"is_active": false} keeps the record, and with it the knowledge of which promotion won those offers. After a delete they are indistinguishable from offers discounted by hand.
POST/promotions/{id}/apply
scopepromotions:writeSpread one over the offers still open. Supports dry_run.
{ "data": { "dry_run": true, "promotion_id": 41, "eligible": 12, "applied": 0, "skipped_pinned": 1, "skipped_dealer_set": 3 }}The computed flags#
| Field | Means |
|---|---|
is_active | The switch. Stored, and yours to set. |
is_live | Active and inside its window, by our clock. |
is_applied_to_new_offers | Several can be live at once; exactly one — the newest — is the one new offers get. |
also_live | On a write: the other promotions running right now. Empty is the reassuring answer. |
backfill_preview | On a write: what an apply would reach, without asking twice. |