# Earning rules

> Decide how members earn points.

Earning rules map events — a payment, a signup, a referral — to points awarded, with multipliers, caps, and eligibility windows. They are the configurable core of a [loyalty program](/docs/engagement/how-engagement-works): when an event fires, Engagement evaluates every active rule and writes a `points_transaction` for each match.

## Rule anatomy

A rule attaches to a `program`, listens for one `trigger`, and computes points from an `award` expression. The optional fields shape *when* and *how much*.

| Field        | Type      | Description                                                           | Default    |
| ------------ | --------- | --------------------------------------------------------------------- | ---------- |
| `trigger`    | `string`  | Event that evaluates the rule, e.g. payment.completed.                | `—`        |
| `award`      | `object`  | How points are computed — per\_amount, fixed, or per\_currency.       | `—`        |
| `multiplier` | `number`  | Scales the computed award. Stacks with tier and campaign multipliers. | `1`        |
| `cap`        | `object`  | Maximum points per member per window.                                 | `none`     |
| `window`     | `string`  | Eligibility period: day, week, month, lifetime.                       | `lifetime` |
| `active`     | `boolean` | Whether the rule is evaluated.                                        | `true`     |

```typescript
import { Vinr } from '@vinr/sdk';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

// Earn 1 point per €1 spent on any completed payment.
const rule = await vinr.loyalty.earningRules.create({
  program: 'prog_default',
  trigger: 'payment.completed',
  award: { type: 'per_amount', per: 100, points: 1 }, // per 100 minor units (= €1)
});                                                    // "rule_..."
```

The `award.type` controls the computation:

| Type           | Meaning                                         | Example                                    |
| -------------- | ----------------------------------------------- | ------------------------------------------ |
| `per_amount`   | Points per N minor units of `amount`.           | 1 point per €1 → `{ per: 100, points: 1 }` |
| `fixed`        | A flat award regardless of amount.              | Welcome bonus → `{ points: 500 }`          |
| `per_currency` | Per-currency rate, for multi-currency programs. | `{ EUR: 1, USD: 1, GBP: 1.2 }`             |

## Event triggers

Rules react to events from across VINR. A single program can mix triggers — purchase points, a welcome bonus, and referral rewards are three rules, not three features.

| Trigger                   | Typical rule                            |
| ------------------------- | --------------------------------------- |
| `payment.completed`       | Award per €1 spent.                     |
| `loyalty.account.created` | Welcome bonus.                          |
| `subscription.created`    | Bonus for committing to an annual plan. |
| `invoice.paid`            | Recurring-billing loyalty.              |
| `referral.converted`      | Reward referrer and referee.            |

For a payment to earn, it must be linked to a member. See [Linking payments & loyalty](/docs/engagement/linking-payments-and-loyalty) for how `customer` and `metadata` resolve to a `loyalty_account`.

```typescript
// A flat welcome bonus when a member enrolls.
await vinr.loyalty.earningRules.create({
  program: 'prog_default',
  trigger: 'loyalty.account.created',
  award: { type: 'fixed', points: 500 },
});
```

## Multipliers & bonuses

`multiplier` scales a rule's award. Multipliers compose: the effective rate is the rule multiplier times any active [tier](/docs/engagement/tiers-and-status) multiplier times any [campaign](/docs/engagement/campaigns) multiplier in effect at the time of the event.

```typescript
// Double points on the base purchase rule.
await vinr.loyalty.earningRules.update('rule_abc123', { multiplier: 2 });
```

> A Gold member (tier multiplier 1.5) earning under a 2x rule during a 3x weekend campaign earns `1 × 2 × 1.5 × 3 = 9` points per €1. Multipliers stack multiplicatively, not additively.

## Caps & limits

Caps protect against runaway awards from large or repeated transactions. A `cap` limits points per member within a rolling `window`.

```typescript
// Cap purchase earnings at 5,000 points per member per calendar month.
await vinr.loyalty.earningRules.update('rule_abc123', {
  cap: { points: 5000 },
  window: 'month',
});
```

When a cap is hit, the resulting `points_transaction` is truncated to the remaining headroom and tagged `capped: true` in its metadata, so you can detect and communicate the limit. Set `window: 'lifetime'` for one-time bonuses you never want to award twice — for example, a welcome bonus fires once per member even if the trigger repeats.

> Caps are evaluated at award time, not retroactively. Lowering a cap does not claw back points already granted; it only constrains future awards.

## Testing rules

Use the [sandbox](https://sandbox.api.vinr.com) to fire real events without moving money. Create a payment with a sandbox card, then inspect the member's points transactions.

### Simulate a qualifying event

In sandbox, create a payment for a linked customer using test card `4242 4242 4242 4242`. On completion it emits `payment.completed`.

### Inspect the resulting transactions

List the member's `points_transaction` records and confirm the award, multiplier, and any cap behaved as configured.

```typescript
const txns = await vinr.loyalty.pointsTransactions.list({
  account: 'loy_member123',
  limit: 5,
});
// Each ptx_ shows source rule, points, and metadata.capped if truncated.
```

### Verify the earned webhook

Confirm your endpoint received `loyalty.points.earned`. Verify the signature before trusting the payload.

```typescript
const event = vinr.webhooks.verify(payload, signature); // x-vinr-signature
if (event.type === 'loyalty.points.earned') {
  // event.data.points, event.data.account, event.data.rule
}
```

## Next steps

[Linking payments & loyalty](/docs/engagement/linking-payments-and-loyalty) — Make payments resolve to a member so rules can fire.

[Tiers and status](/docs/engagement/tiers-and-status) — Tier multipliers that stack on earning rules.

[Campaigns](/docs/engagement/campaigns) — Time-boxed bonus multipliers for earning.
