Offers & promotions

Targeted incentives to drive behavior.

View as MarkdownInstall skills

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.

ConditionExampleNotes
segments['lapsed_90d']Named segments managed in the dashboard or via the segments API.
tiers['gold', 'platinum']Restrict to one or more tiers.
min_balance500Member must hold at least this many points.
enrolled_before2026-01-01Reward 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.

typeEffectKey fields
points_multiplierMultiplies points earned on a qualifying purchase.multiplier
points_bonusFlat points award on activation or first purchase.points
percentage_discountPercentage off a checkout.percent, max_discount
gift_rewardIssues 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 windowstarts_at / ends_at. Outside the window the offer status is draft/expired and redemptions are rejected.
  • Global capmax_redemptions limits the total across every member. The offer auto-expires once reached.
  • Per-member capper_member_limit (default 1) 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

Was this page helpful?
Edit on GitHub

Last updated on

On this page