Promotion
Promotional offers attached to events: types, discount shapes, validity strategies, per-day-of-week availability, and external references
Validate Promotion objects at runtime with PromotionSchema from @eventconnectors/ndtrc_model.
A Promotion is a special offer attached to a TRCItem (typically an Event). It describes what the offer is (free entry, percentage off, gift), when it's valid (always, in a date range, only as an early-bird, only as a last-minute), and how often it's available (always, or only on specific weekdays).
A single TRCItem may carry several promotions in parallel — e.g. an early-bird discount up to two weeks before, plus a last-minute discount in the final 24 hours, plus a "groups of 10+" gift offer.
Promotion shape
The full set of fields:
| Field | Type | Description |
|---|---|---|
product | string | Internal label for the offer (e.g. "early-bird", "citycard", "1one1"). Human-readable identifier; not shown directly to consumers. |
externalReference | string | Optional pointer to the same offer in your own system. Free-form. Max 255 characters. |
promotionType | enum | none, free, discount, gift, allowance — see PromotionType. |
discount | object | Shape of the price reduction. See Discount. |
discountValueRequired | boolean | Whether the promotion has to carry a discount value to count as complete. Defaults to true. See Discount value required. |
image | object | Optional File — usually the logo of the offer — shown next to the promotion. See Image. |
enabled | boolean | Master on/off switch. Defaults to true. Setting to false disables the promotion regardless of validity strategy. |
restrictedToRegisteredUsers | boolean | When true, the promotion is only available to logged-in users on partner sites. |
validityStrategy | enum | always, dateRange, earlyBird, lastMinute — see ValidityStrategy. |
startDate | date | Start of the validity window (used by dateRange). |
endDate | date | End of the validity window (used by dateRange). |
eventRelativeDuration | string | ISO 8601 duration relative to the event's start. Used by earlyBird and lastMinute. See Event-relative durations. |
opens | array | Optional per-day-of-week or per-month restriction. Same Open shape as in Calendar. |
translations | array | { lang, description } entries. The description supports simple HTML (<a>, <b>, <i>). |
detailsUrls | array | Optional list of Url objects (see Contactinfo) pointing at terms, the booking page, partner offer details, etc. |
PromotionType
| Value | Meaning |
|---|---|
none | Tracking-only entry — the promotion exists for record-keeping (e.g. an externalReference to a partner system) but represents no active offer. Useful for stitching FeedFactory promotions to upstream systems without implying a discount. |
free | Free entry or free item. Pair with discount.free = true. |
discount | Price reduction. Use discount.percentage (0–100) or discount.amount (absolute, EUR). |
gift | Free gift bundled with the regular ticket. Doesn't reduce the ticket price. |
allowance | Spending allowance or voucher (e.g. "€10 to spend at the bar"). |
Discount
The discount object has three optional fields and the relevant ones are populated based on promotionType:
| Field | Type | Used when |
|---|---|---|
free | boolean | promotionType: "free" — set to true |
percentage | integer | promotionType: "discount" — percentage off (0–100) |
amount | number | promotionType: "discount" — absolute discount in EUR |
For percentage vs amount: pick one. If both are set, partner consumers may render either; behavior is unspecified.
Discount value required
discountValueRequired says whether this promotion has to carry a concrete value — a discount.percentage or a discount.amount — to count as complete. It exists because the completeness check on an event or a location also looks at the promotions attached to it:
| Value | Completeness check |
|---|---|
true (default) | A promotion with neither discount.percentage nor discount.amount makes the event or location incomplete, so the missing value is visible instead of silently absent. |
false | The promotion is exempt — it needs no value. Use it for offers that have none: a gift, a present, free entrance, a tracking-only entry. |
It is set on the account-level promotion product (Account → Promotions in the console), so an admin answers the question once per offer. An item picks the value up when the promotion is added to it. Promotions already on an item keep their own copy — the automatic fan-out re-syncs only enabled — so changing the product afterwards does not rewrite items that already carry it, exactly as for discount and translations. Set it on the item itself to correct one of those.
discountValueRequired is deliberately independent of promotionType. The type says what kind of offer it is; this says whether a value is expected, and the two are not the same question — a discount whose price is settled in a partner system carries no percentage here, while an allowance of "€10 at the bar" does carry an amount. Read the boolean; don't infer it from the type.
It is always present on a response. A promotion created before the field existed reads back as true, so nothing is silently exempted.
{
"product": "tuesday-gift",
"promotionType": "gift",
"discountValueRequired": false,
"translations": [{ "lang": "nl", "description": "Op dinsdag een gratis drankje" }]
}Image
image holds an optional File for the promotion — normally the logo of the offer, such as a city card or a partner brand. It is what the console renders next to the promotion, and it is returned on read so publishing sites can render the same logo.
It is one File, not an array, and it uses the same shape as the rest of the model, so it carries hlink, copyright, and per-language title translations. mediatype: "logo" is the natural choice for a brand mark. The promotion's image is unrelated to the TRCItem's own files — those are the photos of the event itself.
{
"product": "citycard",
"promotionType": "discount",
"discount": { "percentage": 25 },
"image": {
"hlink": "https://cdn.example.com/citycard-logo.png",
"filename": "citycard-logo.png",
"filetype": "png",
"mediatype": "logo",
"copyright": "City Card",
"title": {
"label": "City Card",
"titleTranslations": [{ "lang": "nl", "label": "City Card" }]
}
}
}ValidityStrategy
Controls when the promotion is active:
| Value | Active when | Required fields |
|---|---|---|
always (default) | Whenever enabled = true | — |
dateRange | Between startDate and endDate | startDate, endDate |
earlyBird | More than eventRelativeDuration before the event start | eventRelativeDuration |
lastMinute | Within eventRelativeDuration of the event start | eventRelativeDuration |
opens adds a second filter on top of validityStrategy: even if the strategy says "currently active", the promotion is only honoured when today matches one of the opens entries. Useful for offers like "Tuesdays only" or "every first Monday of the month".
Event-relative durations
eventRelativeDuration uses the ISO 8601 duration format.
| Duration | Meaning |
|---|---|
PT1H | 1 hour |
PT24H / P1D | 24 hours / 1 day |
P3D | 3 days |
P2W | 2 weeks |
P1M | 1 month |
P3Y6M4DT12H30M5S | 3 years, 6 months, 4 days, 12 hours, 30 minutes, 5 seconds |
For earlyBird, the duration is read as "the offer is valid up until this much before the event starts". P2W means the offer ends 2 weeks before showtime. For lastMinute, the duration is "the offer kicks in this much before the event starts" — PT24H opens the deal in the final day.
Examples
Free entry on the day of the event
{
"product": "free-day-pass",
"promotionType": "free",
"validityStrategy": "lastMinute",
"eventRelativeDuration": "PT24H",
"discount": { "free": true },
"translations": [
{ "lang": "nl", "description": "Gratis toegang op de dag zelf" },
{ "lang": "en", "description": "Free entry on the day itself" }
]
}Early-bird 20% discount until two weeks before
{
"product": "early-bird-2025",
"promotionType": "discount",
"validityStrategy": "earlyBird",
"eventRelativeDuration": "P2W",
"discount": { "percentage": 20 },
"translations": [
{ "lang": "nl", "description": "20% korting bij vroeg boeken" },
{ "lang": "en", "description": "20% off when booking early" }
]
}Fixed €5 discount during a date range
{
"product": "summer-sale",
"promotionType": "discount",
"validityStrategy": "dateRange",
"startDate": "2026-06-01",
"endDate": "2026-08-31",
"discount": { "amount": 5.0 },
"translations": [
{ "lang": "nl", "description": "Zomerkorting: €5 voordeel" },
{ "lang": "en", "description": "Summer sale: €5 off" }
]
}Tuesday-only gift, registered users only
{
"product": "tuesday-gift",
"promotionType": "gift",
"discountValueRequired": false,
"enabled": true,
"restrictedToRegisteredUsers": true,
"validityStrategy": "always",
"opens": [
{ "day": 3, "whens": [{ "timestart": "00:00", "timeend": "23:59" }] }
],
"translations": [
{ "lang": "nl", "description": "Op dinsdag een gratis drankje (alleen voor leden)" },
{ "lang": "en", "description": "Free drink on Tuesdays (members only)" }
]
}The opens entry uses the same day-of-week numbering as the rest of the API: 1=Sunday, 2=Monday, 3=Tuesday, 4=Wednesday, 5=Thursday, 6=Friday, 7=Saturday.
Partner-system promotion with no FeedFactory-side offer
{
"product": "partner-citycard",
"externalReference": "CITYCARD-CAMPAIGN-2026-Q1",
"promotionType": "none",
"discountValueRequired": false,
"enabled": true,
"validityStrategy": "always"
}promotionType: "none" is the right choice when the entry exists only to point at a partner system (via externalReference) — there's no FeedFactory-side discount to render. An entry like this carries no value of its own, so set discountValueRequired: false on it as well; otherwise every event carrying it is reported as missing a discount value. Decide that per promotion, though — it follows from this entry having no offer, not from its promotionType.
Per-event vs. account-level templates
Promotions on a TRCItem are instance-level — they apply to that single event/location/venue/route. There are also account-level promotion templates (configured under Account → Promotions in the console) that fan out automatically across an account's events, locations, or venues based on matching rules.
The externalReference field is not propagated by template-based cloning: clones start with externalReference = null. If you need an external pointer, set it on the per-event promotion after the clone.
External reference
externalReference is a free-form string (max 255 characters) that connects this promotion to its counterpart in another system — a partner site's "actie" record, a CRM coupon code, a ticketing-platform promotion ID. Event Connectors does not validate or interpret the value; it round-trips as-is on every read and write.
Use it to avoid inventing marker conventions or out-of-band spreadsheets to track which FeedFactory promotion corresponds to which partner record.
Translations
Each promotion carries a translations array of { lang, description } objects. The description is the user-facing text consumers render (in the matching language) — it supports simple HTML elements like <a>, <b>, <i>. Plain \n is not rendered as a line break; wrap content in HTML if you need formatting.
{
"translations": [
{ "lang": "nl", "description": "20% korting <b>tot 1 mei</b>" },
{ "lang": "en", "description": "20% off <b>until 1 May</b>" }
]
}Details URLs
Optional links to more information about the promotion — terms, booking flows, partner offer pages. Each entry is a Url with an optional urlServiceType (general, booking, webshop, etc.) and per-language description translations.
{
"detailsUrls": [
{
"url": "https://partner.example.com/offers/early-bird",
"urlServiceType": "booking",
"descriptionTranslations": [
{ "lang": "nl", "label": "Boek met early-bird tarief" },
{ "lang": "en", "label": "Book at early-bird rate" }
]
}
]
}Notes
- The
productfield is not shown directly to consumers — it's an internal identifier. Rendertranslations[lang].descriptioninstead. - An
enabled: falsepromotion is suppressed everywhere, even if itsvalidityStrategywould otherwise mark it active. Use this as a quick kill-switch. - A promotion with no
validityStrategydefaults toalways. - A promotion with
validityStrategy: "dateRange"but missingstartDateorendDateis treated as inactive. discountValueRequiredaffects the completeness check only — it never suppresses the promotion the wayenabled: falsedoes.
See the API Reference for the full Promotion schema.