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

# Score Streaks campaigns

> Step-by-step guide to creating and integrating a score streak campaign.

Score Streaks reward customers who consistently send a qualifying event within a defined time window. This guide walks you through creating a campaign, reading streak state from the API, and handling rewards in your frontend.

## Prerequisites

* An active Masivo account with a SERVER API key
* A customer registered in Masivo with an `external_id` matching your system's customer identifier
* At least one reward configured in your account

## Create the campaign

<Steps>
  <Step title="Open the Score Streaks section">
    In the dashboard navigate to **Campaigns → Score Streaks** and click **New campaign**.
  </Step>

  <Step title="Set the trigger event">
    Choose the event type your customers will send (e.g. `PURCHASE`, `CHECK_IN`). Every time a customer sends this event it counts toward their streak.
  </Step>

  <Step title="Configure the loss condition">
    Set how long a customer has to send the next qualifying event before the streak is broken. For example, **1 DAY** means the customer must check in daily to keep their streak alive.
  </Step>

  <Step title="Add milestone rewards">
    Click **Add milestone** to define score thresholds. Each milestone triggers a reward when the customer's score reaches that number in the current cycle.

    Example milestone tiers:

    * Score **1** → 10 points (daily reward)
    * Score **7** → 100 points (weekly bonus)
    * Score **30** → 500 points (monthly champion)
  </Step>

  <Step title="Optional: enable scheduled resets">
    Toggle **Scheduled reset** if you want streaks to restart on a fixed calendar date regardless of the customer's activity — useful for monthly leaderboards or seasonal campaigns.
  </Step>

  <Step title="Publish the campaign">
    Set start date and publish. The campaign will start processing events immediately.
  </Step>
</Steps>

## Send qualifying events

Use the [Emit event](/api-reference/behavior/events/post) endpoint from your backend:

```bash theme={null}
POST /api/storefront/v1/behavior/events
Authorization: Bearer <SERVER_API_KEY>
x-account-id: <ACCOUNT_ID>

{
  "customer_id": "user_abc123",
  "brand_id": null,
  "type": "PURCHASE",
  "issued_at": "2026-05-21T12:00:00Z"
}
```

Masivo will:

1. Identify matching score streak campaigns for this event type
2. Find or create the customer's `score_streak` record
3. Check if the loss window expired (and close the cycle if so)
4. Increment the score and write a `GAIN` history event
5. Fire any milestone rewards and write `MILESTONE` history events
6. Return the API response — rewards accumulate in the customer's wallet

## Display streak progress

Fetch the customer's active streaks to show a live progress indicator:

```bash theme={null}
GET /api/storefront/v1/customers/{customer_id}/score-streaks
Authorization: Bearer <SERVER_API_KEY>
x-account-id: <ACCOUNT_ID>
```

Example response:

```json theme={null}
{
  "data": [
    {
      "id": "b3e9a1f2-...",
      "campaign_id": "c8d2...",
      "customer_id": "user_abc123",
      "status": "ACTIVE",
      "current_score": 5,
      "current_streak": 5,
      "highest_milestone_score": 5,
      "period_started_at": "2026-05-21T12:00:00Z",
      "period_ends_at": "2026-05-22T12:00:00Z",
      "period_score": 5,
      "period_event_count": 5,
      "next_reset_at": null,
      "last_activity_at": "2026-05-21T12:00:00Z",
      "last_loss_at": null,
      "last_reset_at": null
    }
  ]
}
```

Key fields for your UI:

| Field            | Use                                                   |
| ---------------- | ----------------------------------------------------- |
| `current_score`  | Current streak score in this cycle                    |
| `period_ends_at` | Deadline for next qualifying event (show a countdown) |
| `current_streak` | Total qualifying events in current cycle              |
| `last_loss_at`   | Last time the streak was broken                       |

## Show streak history

Pass `include=history` to get a flattened, time-ordered list of all history events across all streaks for this customer:

```bash theme={null}
GET /api/storefront/v1/customers/{customer_id}/score-streaks?include=history&limit=50
```

Response shape when `include=history`:

```json theme={null}
{
  "data": {
    "streaks": [ ... ],
    "history": [
      {
        "id": "evt1...",
        "score_streak_id": "b3e9...",
        "campaign_id": "c8d2...",
        "customer_id": "user_abc123",
        "cycle_id": "cycl1...",
        "type": "MILESTONE",
        "score_delta": null,
        "score_before": 4,
        "score_after": 5,
        "milestone_score": 5,
        "period_started_at": "2026-05-21T12:00:00Z",
        "period_ends_at": "2026-05-22T12:00:00Z",
        "reversed_at": null,
        "reason": null,
        "actor_id": null,
        "metadata": {},
        "created_at": "2026-05-21T12:00:00Z"
      }
    ]
  }
}
```

Use `history` to render an activity feed, a streak timeline, or to debug unexpected state.

## Common patterns

### Show a countdown timer

```ts theme={null}
const streak = streaks[0];
const deadline = new Date(streak.period_ends_at);
const msLeft = deadline.getTime() - Date.now();
const hoursLeft = Math.floor(msLeft / 3_600_000);
```

### Detect a broken streak

```ts theme={null}
const wasBroken = streak.last_loss_at !== null;
const brokenAt = wasBroken ? new Date(streak.last_loss_at) : null;
```

### Find the next milestone

Given the campaign rules sorted by `milestone` ascending:

```ts theme={null}
const nextMilestone = rules
  .filter(r => r.milestone > streak.current_score)
  .sort((a, b) => a.milestone - b.milestone)[0];
const pointsNeeded = nextMilestone.milestone - streak.current_score;
```

## Frequently asked questions

<AccordionGroup>
  <Accordion title="What happens when an event is reversed?">
    The associated `GAIN` and `MILESTONE` history events are marked as reversed and an `EVENT_REVERSAL` history event is appended. If the reversed event was in the active cycle, the score and streak count are rolled back. The operation is idempotent.
  </Accordion>

  <Accordion title="What happens at the loss window deadline if no event arrives?">
    The cron job (`/api/cron/loyalty/score_streaks`) runs periodically. When it finds a streak whose `period_ends_at` is in the past, it closes the active cycle with a `LOSS` history event. The next qualifying event from the customer starts a fresh cycle.
  </Accordion>

  <Accordion title="Can a customer earn the same milestone reward multiple times?">
    Yes — once per cycle. If the streak is lost (new cycle) and the customer reaches the same milestone again, the reward is granted again. Within a single cycle, each milestone fires exactly once.
  </Accordion>

  <Accordion title="What does scheduled reset do vs. loss condition?">
    The **loss condition** is triggered by inactivity: the customer fails to act before the period expires. A **scheduled reset** fires on a fixed calendar schedule regardless of activity — even a customer with a perfect streak is reset on the configured date.
  </Accordion>

  <Accordion title="How do I reset a streak manually for a customer?">
    Use the internal admin endpoint `POST /api/admin/score-streaks/reset` with the `account_id`, `campaign_id`, `customer_id`, and a `reason`. This requires the `CRON_CHECKSUM` header and is intended for support operations.
  </Accordion>
</AccordionGroup>
