> ## Documentation Index
> Fetch the complete documentation index at: https://docs.scrip.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Before planning an integration, read https://docs.scrip.dev/which-endpoints-you-need.md. It lists the calls an app makes as users earn and redeem, such as sending and reversing events, reading balances, and redeeming rewards. Every other endpoint creates or changes the program itself, and the dashboard at https://app.scrip.dev is the usual place for that work.
> Gift cards are rewards in the program's rewards catalog, next to custom rewards. Redeem one with `POST /v1/participants/{id}/redemptions/items` and a `fulfillment` object. When the order succeeds, show the participant the claim link from `fulfillment.claim_url` in the redemption response or from `POST /v1/redemptions/{id}/claim-link`. An order can finish after the request returns, so handle the `redemption.fulfillment_succeeded` and `redemption.fulfillment_failed` webhooks. See https://docs.scrip.dev/guides/gift-cards.md.
> Give every event, redemption, and transfer an `idempotency_key` built from your own data, such as an order ID, so retries are safe. Keep API keys on your server.
> Request and response schemas are in https://docs.scrip.dev/openapi.json. Add `.md` to any docs URL to get the page as Markdown.

# Campaigns

> Run time-bound promotions as windowed rules that stack on or override your base earning

A campaign is a time-bound change to what participants earn: double points for a month, a launch-week rate, a category push. In Scrip a campaign is a group of rules with an `active_from` and `active_to` window. The rules, budgets, windows, and configuration history you already use are the campaign. There is no separate resource to create.

[Writing rules](/guides/writing-rules#time-windowed-rules) covers the window fields on a single rule. Use this page when a promotion has to coexist with the rates you already pay, has a spend cap, or has to be launched and retired as a unit.

## Choose a shape

The first decision is how the campaign relates to your base rules.

| You want                                        | Where the campaign rules go                                                    | Why                                                                                      |
| ----------------------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| Bonus on top of the base rate (stack)           | Their own set, positioned after your earning set                               | Every set with a matching rule runs, so the base rate and the bonus both pay             |
| Replace the base rate for the window (override) | The same set as the base rate, positioned first, with `stop_after_match: true` | A match stops the rest of that set, so the base rule is skipped while the window is open |
| Reward a new behavior only during the window    | Their own set                                                                  | Nothing to interact with                                                                 |

Stacking is the safer default. Override only when the campaign rate is meant to be the whole rate.

## Stack a bonus

Add a `campaigns` set after your earning set. Each campaign is one or more rules in that set with a window. This one adds 2% on dining for October:

```bash theme={null}
POST /v1/programs/{programId}/rule-sets
```

```json theme={null}
{"key": "campaigns", "name": "Campaigns"}
```

```bash theme={null}
POST /v1/rules
```

```json theme={null}
{
  "program_id": "{programId}",
  "rule_set_id": "{campaignsRuleSetId}",
  "name": "october_dining_bonus",
  "active_from": "2026-10-01T00:00:00Z",
  "active_to": "2026-11-01T00:00:00Z",
  "condition": "event.type == \"purchase\" && event.mcc in [\"5812\", \"5813\", \"5814\"]",
  "actions": [
    {
      "type": "CREDIT",
      "asset_id": "{assetId}",
      "amount": "${{ round(event.amount * 0.02, 2) }}",
      "description": "October dining bonus"
    }
  ]
}
```

A dining purchase stamped in October earns the base rate from the earning set plus 2% from this rule. Stamped in November, the rule is skipped and the base rate pays alone.

## Override the base rate

To pay a flat 5% on everything during launch week instead of the usual rates, put the rule in the earning set, first, with `stop_after_match`:

```bash theme={null}
POST /v1/rules
```

```json theme={null}
{
  "program_id": "{programId}",
  "rule_set_id": "{earningRuleSetId}",
  "position": {"placement": "first"},
  "stop_after_match": true,
  "name": "launch_week_5pct",
  "active_from": "2026-10-01T00:00:00Z",
  "active_to": "2026-10-08T00:00:00Z",
  "condition": "event.type == \"purchase\"",
  "actions": [
    {
      "type": "CREDIT",
      "asset_id": "{assetId}",
      "amount": "${{ round(event.amount * 0.05, 2) }}",
      "description": "Launch week 5%"
    }
  ]
}
```

Inside the window, a purchase matches this rule and the rest of the earning set is skipped. Outside the window, the rule is skipped and the earning set runs as before. Rules in other sets, such as tracking or the `campaigns` set, run either way. See [Set-local stopping](/guides/rule-sets#set-local-stopping).

## Windows

| Field         | Effect                                                  |
| ------------- | ------------------------------------------------------- |
| `active_from` | Inclusive. Events stamped before it skip the rule.      |
| `active_to`   | Exclusive. Events stamped at or after it skip the rule. |

Both are compared to the event's `event_timestamp`, not the clock when Scrip processes it. That has three consequences:

* You can create campaign rules weeks ahead. They start and stop on their own.
* Retries and replays make the same decision as the original run.
* A backdated event can land inside a window that has already closed. Historical imports fire past campaigns unless you suspend or archive the rules first.

A skipped evaluation is recorded on the event with reason `OUTSIDE_TIME_WINDOW`, so you can confirm a campaign is dormant by reading any event's trace.

## Cap the spend

A budget on the campaign rule caps what it issues across all participants. A lifetime budget is the right shape for a one-off campaign: once the pool is spent, the rule keeps matching but its actions are skipped with reason `BUDGET_EXCEEDED`, and the base rate still pays.

```json theme={null}
{
  "budgets": [
    {"asset_id": "{assetId}", "limit": "250000"}
  ]
}
```

To limit each participant instead, gate the rule on a tag or counter it sets. One bonus per participant for the whole campaign:

```json theme={null}
{
  "condition": "event.type == \"purchase\" && !(\"october_bonus_paid\" in participant.tags)",
  "actions": [
    {"type": "TAG", "tag": "october_bonus_paid"},
    {"type": "CREDIT", "asset_id": "{assetId}", "amount": "1000", "description": "October welcome bonus"}
  ]
}
```

A budget and a per-participant gate are independent. Use both to cap the pool and the individual at the same time. See [Budgets](/guides/writing-rules#budgets).

For a campaign paid in its own currency with a hard ceiling, link a `PREFUNDED` asset and fund the program wallet with the pool. Credits fail when the wallet is empty. See [Asset configuration](/guides/asset-configuration#issuance-policy).

## Test before launch

Send the campaign rules as a draft `rule_configuration` to `POST /v1/test-runs` with scenario events stamped inside and outside the window. Set `options.compare_live` to see what the current configuration would have paid on the same events. Test runs roll back, so nothing is credited.

Check three things: an in-window event pays the campaign amount, an out-of-window event pays only the base rate, and for an override, the base rule is not reached inside the window. See [Testing](/guides/testing#test-a-full-configuration).

## Launch as one change

When a campaign is more than one rule, publish them together with `/rule-configuration/preview` and `/rule-configuration/apply` so they go live in one version. The change is recorded as a revision you can diff or roll back later. See [Managing rule configuration](/guides/managing-rule-configuration).

Because windows do the scheduling, apply the change whenever it is convenient. The rules stay dormant until `active_from`.

## During the campaign

| You want                                      | Do                                                                                                                                                                                                          |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Stop it early                                 | Set the rule's `status` to `SUSPENDED`. Events not yet processed skip it immediately.                                                                                                                       |
| Change the rate                               | `PATCH` the rule. Events already processed keep the old result; events not yet processed use the new one. See [Updating rules under live traffic](/guides/writing-rules#updating-rules-under-live-traffic). |
| See how much has been paid                    | Read the rule. Its `budgets` array reports `consumed`.                                                                                                                                                      |
| See why an event did or did not get the bonus | Read the event. Its `rule_evaluations` show the campaign rule as matched, `OUTSIDE_TIME_WINDOW`, or `BUDGET_EXCEEDED`.                                                                                      |

Name the campaign in each CREDIT's `description`. It appears on participant statements and journal entries, which is how finance and support tell campaign credits from base earning.

## After the campaign

The window closes on its own. The rules stay in the configuration as a record of what ran. Archive them once you no longer need them in the active list, or leave them; a rule outside its window costs one skipped evaluation per matching event.

To run the same campaign again, `PATCH` the window on the existing rule or apply a new revision with fresh dates. To undo a campaign that was configured wrong, roll back to the revision before it was applied.

<CardGroup cols={3}>
  <Card icon="pen-to-square" href="/guides/writing-rules#time-windowed-rules" title="Time-windowed rules">
    The window fields and how they read the event timestamp.
  </Card>

  <Card icon="layer-group" href="/guides/rule-sets" title="Rule sets">
    Set order, positions, and set-local stopping.
  </Card>

  <Card icon="flask" href="/guides/testing#test-a-full-configuration" title="Test a full configuration">
    Run the draft against in-window and out-of-window events.
  </Card>
</CardGroup>
