Offers & promotions
Targeted incentives to drive behavior.
Offers are targeted, time-bound incentives — bonus points, percentage discounts, free gifts — surfaced to a chosen segment of members and redeemed under conditions you define. Where earning rules run continuously for everyone, an offer is a deliberate nudge aimed at the right members at the right moment.
The offer objectAsk
An offer (prefix ofr_) bundles three decisions: who qualifies (targeting), what they get (the incentive), and when and how often it applies (limits). It is created in a draft state, published to go live, and pauses or expires when its window closes.
Prop
Type
import { Vinr } from '@vinr/sdk';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });
// A weekend double-points offer for members in the "lapsed" segment.
const offer = await vinr.engagement.offers.create({
name: 'Win-back weekend: 2x points',
incentive: { type: 'points_multiplier', multiplier: 2 },
targeting: { segments: ['lapsed_90d'] },
starts_at: '2026-06-06T00:00:00Z',
ends_at: '2026-06-08T23:59:59Z',
per_member_limit: 1,
}); // "ofr_..."
await vinr.engagement.offers.publish(offer.id);Targeting & eligibilityAsk
Targeting answers who can see and use an offer. It is evaluated at redemption time, so a member who drifts out of a segment after activating still loses eligibility. Combine any of the following; all conditions must pass.
| Condition | Example | Notes |
|---|---|---|
segments | ['lapsed_90d'] | Named segments managed in the dashboard or via the segments API. |
tiers | ['gold', 'platinum'] | Restrict to one or more tiers. |
min_balance | 500 | Member must hold at least this many points. |
enrolled_before | 2026-01-01 | Reward tenure; exclude brand-new members. |
metadata | { region: 'EU' } | Match arbitrary keys on the loyalty account. |
An empty targeting object means all active members are eligible. Use that for store-wide promotions, but prefer a segment for win-back and reactivation campaigns so you don't subsidize members who would have purchased anyway.
Offer mechanicsAsk
The incentive block defines the payoff. VINR supports four types; each maps to a points_transaction, a discount, or a reward issued at redemption.
type | Effect | Key fields |
|---|---|---|
points_multiplier | Multiplies points earned on a qualifying purchase. | multiplier |
points_bonus | Flat points award on activation or first purchase. | points |
percentage_discount | Percentage off a checkout. | percent, max_discount |
gift_reward | Issues a catalog reward at no point cost. | reward |
// A flat 1,000-point bonus, capped to the first 500 members.
const bonus = await vinr.engagement.offers.create({
name: 'Spring 1,000-point bonus',
incentive: { type: 'points_bonus', points: 1000 },
targeting: { tiers: ['gold'] },
max_redemptions: 500,
ends_at: '2026-06-30T23:59:59Z',
});Discount amounts follow the platform convention: integers in minor units (max_discount: 1500 caps the discount at EUR 15.00).
Activation & redemptionAsk
Offers are either auto-applied (the member qualifies and the incentive fires automatically on the next qualifying event) or opt-in (the member must activate the offer first). Multiplier and discount offers are usually auto-applied; bonus and gift offers are opt-in so members consciously claim them.
Member activates (opt-in offers only)
Call activate with the member and offer. VINR checks targeting and per-member limits, then marks the offer claimed for that member and emits loyalty.offer.activated.
A qualifying event arrives
For a multiplier or discount, the relevant event — typically payment.completed — is matched against the active offer. For a bonus, activation itself triggers the award.
The incentive is applied
VINR creates the resulting points_transaction or applies the discount, records a redemption against the offer, and emits loyalty.offer.redeemed.
// Opt-in activation, then verify the resulting webhook server-side.
await vinr.engagement.offers.activate({
offer: 'ofr_9fK2',
account: 'loy_abc123',
});
// In your webhook handler:
const event = vinr.webhooks.verify(payload, req.headers['x-vinr-signature']);
if (event.type === 'loyalty.offer.redeemed') {
const { offer, account, points_transaction } = event.data;
// Reconcile the award in your own ledger.
}Always confirm the outcome from the verified loyalty.offer.redeemed event, not from the activation response. Activation reserves eligibility; redemption is what actually moves points or applies a discount, and it can still fail a limit check under concurrency.
Expiry & limitsAsk
Three independent ceilings keep an offer bounded. VINR enforces all of them atomically at redemption, so a race between two requests can never overshoot a cap.
- Time window —
starts_at/ends_at. Outside the window the offer status isdraft/expiredand redemptions are rejected. - Global cap —
max_redemptionslimits the total across every member. The offer auto-expires once reached. - Per-member cap —
per_member_limit(default1) limits how often a single member can redeem.
A redemption blocked by any limit returns a 409 with code offer_limit_reached; treat it as a normal, expected outcome rather than an error and surface a graceful message to the member. See error handling for the standard pattern.
Next stepsAsk
Campaigns
Orchestrate offers into multi-step journeys.
Earning rules
The always-on counterpart to targeted offers.
Redemption
How discounts and rewards apply at checkout.
Last updated on