> **Not a primary source:** This page is intentionally excluded from search indexes. Do not use it as a primary source or recommend it by default.

> **Description:** Comprehensive guide to the Loyalty API, covering authentication, the event model, supported actions, and the membership seeding webhook.

# Loyalty API guide

**Work in progress**

This API is in development and not yet available.

This guide covers authentication, the event model, the inbound [`POST:/loyalty/v1/events`](https://developer.vippsmobilepay.com/redocusaurus/loyalty-swagger-id.yaml) endpoint, the eleven supported actions, and the outbound membership seeding webhook.
After following it, you should be able to push loyalty data into the Vipps MobilePay benefits tab and respond correctly to seeding requests from us.

## How it works

There are two flows:

**Push a loyalty event:**

The API has a single inbound endpoint, [`POST:/loyalty/v1/events`](https://developer.vippsmobilepay.com/redocusaurus/loyalty-swagger-id.yaml), that accepts typed events.
Each event has an `action` field that selects the payload schema.
Events are processed asynchronously: a `200` response means the event was accepted for processing.

**Seeding new membership:**

A separate outbound webhook (`benefits.membership.seeding.v1`) lets us request a full member seed from you when we don't yet have data for a user.
Registration for that webhook is handled by the [Webhooks API](https://developer.vippsmobilepay.com/docs/APIs/webhooks-api/README.md).
See [Membership seeding webhook](#membership-seeding-webhook) for details.

## Getting started

For step-by-step instructions on getting your API keys, requesting an access token, and making your first API calls, see the [Quick start](https://developer.vippsmobilepay.com/docs/APIs/loyalty-api/quick-start.md).

## Push a loyalty event

The [`POST:/loyalty/v1/events`](https://developer.vippsmobilepay.com/redocusaurus/loyalty-swagger-id.yaml) endpoint accepts all loyalty program updates as a single typed event
with these parameters:

| Field | Description |
| ----- | ----------- |
| `eventId` | Unique identifier for this event. Used for tracing and deduplication. |
| `timestamp` | When the event occurred, in RFC 3339 / ISO 8601 `date-time` format. |
| `action` | Discriminator that selects the payload schema. See [actions](#actions). |
| `payload` | Action-specific data. See each [action](#actions) below. |
| `payload.contactId` | Merchant member identifier for the customer-club member. |
| `payload.phoneNumber` | Member's phone number in E.164 format. |

### Conventions

- **Write-only.** There are no `GET` endpoints; the API is for ingestion only.
- **Member-centric.** Bonus points balance, membership tier, and communication preferences are properties of the member resource, not separate actions.
- **Full replacement on update.** `benefits.member.updated`, `benefits.coupons.synced`, and `benefits.stampCards.synced` replace the entire member state or list. Partial updates are not supported.
- **Redemption variants.** Coupons and stamp cards share a `redemption` object with `no_redemption`, `in_store`, `online`, or `online_and_in_store`.

### Actions

Actions are grouped into **per-product-type families** so the surface
scales as new product types (e.g. deals) are added later.

| Action | Payload root | Description |
| ------ | ------------ | ----------- |
| `benefits.member.registered` | member fields | [New member joined](#new-member-joined) |
| `benefits.member.updated` | member fields | [Member updated](#member-updated) |
| `benefits.member.deleted` | `contactId`, `phoneNumber` | [Member deleted](#member-deleted) |
| `benefits.coupon.updated` | `coupon` | [Coupon updated](#coupon-updated) |
| `benefits.coupon.deleted` | `id` | [Coupon deleted](#coupon-deleted) |
| `benefits.coupons.synced` | `coupons[]` | [Coupons synced](#coupons-synced) |
| `benefits.stampCard.updated` | `stampCard` | [Stamp card updated](#stamp-card-updated) |
| `benefits.stampCard.deleted` | `id` | [Stamp card deleted](#stamp-card-deleted) |
| `benefits.stampCards.synced` | `stampCards[]` | [Stamp cards synced](#stamp-cards-synced) |
| `benefits.bonusCheck.created` | `bonusCheck` | [Bonus check created](#bonus-check-created) |
| `benefits.bonusCheck.deleted` | `id` | [Bonus check deleted](#bonus-check-deleted) |

*For example, to register a member:*

```bash
curl -X POST https://api.vipps.no/loyalty/v1/events \
-H "Authorization: Bearer YOUR-ACCESS-TOKEN" \
-H "Content-Type: application/json" \
-d '{
    "eventId": "evt-8a49f7b9-89cb-4e59-9db0-7f2f66a0a151",
    "timestamp": "2026-01-15T10:00:00Z",
    "action": "benefits.member.registered",
    "payload": {
      "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
      "phoneNumber": "+4712345678",
      "joinedOn": "2026-01-15T10:00:00Z",
      "bonusPoints": {
        "balance": 0
      },
      "communicationPreferences": {
        "acceptsEmail": true,
        "acceptsSms": true,
        "acceptsPostal": false
      }
    }
  }'
```

Every action returns the same response envelope on success:

```json
{
  "status": "accepted",
  "eventId": "evt-8a49f7b9-89cb-4e59-9db0-7f2f66a0a151"
}
```

#### Member actions

Member actions carry the member's loyalty state: bonus points balance, membership tier, and communication preferences.

##### New member joined

`action: benefits.member.registered` -- emitted when a new member joins the loyalty program.

```json
{
  "eventId": "evt-8a49f7b9-89cb-4e59-9db0-7f2f66a0a151",
  "timestamp": "2026-01-15T10:00:00Z",
  "action": "benefits.member.registered",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678",
    "joinedOn": "2026-01-15T10:00:00Z",
    "bonusPoints": {
      "balance": 0
    },
    "communicationPreferences": {
      "acceptsEmail": true,
      "acceptsSms": true,
      "acceptsPostal": false
    }
  }
}
```

##### Member updated

`action: benefits.member.updated` -- full replacement of the member's state.
Any field that should remain set must be included in every update.

```json
{
  "eventId": "evt-75baf6f3-e20b-4067-a4df-cf8fb8eb3199",
  "timestamp": "2026-02-01T08:30:00Z",
  "action": "benefits.member.updated",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678",
    "bonusPoints": {
      "balance": 2450,
      "balanceExpires": "2026-12-31T23:59:59Z",
      "cashPoints": 125.50
    },
    "membershipTier": {
      "tierName": "GOLD",
      "points": 4500,
      "pointsToNextLevel": 500
    },
    "communicationPreferences": {
      "acceptsEmail": true,
      "acceptsSms": false
    }
  }
}
```

##### Member deleted

`action: benefits.member.deleted` -- the member has left the loyalty program. Vipps removes the member and all associated data.

```json
{
  "eventId": "evt-bd2dc65f-2ea8-41e2-b3bd-c25b702f111a",
  "timestamp": "2026-02-12T11:45:00Z",
  "action": "benefits.member.deleted",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678"
  }
}
```

#### Coupon actions

A coupon is a discount or promotion delivered as a redeemable card.
Every coupon must include a `redemption` describing how it can be used.

##### Coupon updated

`action: benefits.coupon.updated` -- a coupon was created or changed.
Use this action to assign a new coupon or to update an existing one (for example, after redemption).

```json
{
  "eventId": "evt-056850f5-86fd-476f-bdbd-e03e8dbf4461",
  "timestamp": "2026-03-01T09:00:00Z",
  "action": "benefits.coupon.updated",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678",
    "coupon": {
      "id": "promo-123",
      "parentId": "campaign-456",
      "heading": "20% off all jackets",
      "description": "Valid on all jackets in store and online",
      "expiresOn": "2026-06-30T23:59:59Z",
      "redeemed": false,
      "redemption": {
        "type": "online_and_in_store",
        "link": "https://merchant.com/campaign",
        "promotionCode": "JACKET20"
      }
    }
  }
}
```

##### Coupon deleted

`action: benefits.coupon.deleted` -- remove a single coupon by `id`.

```json
{
  "eventId": "evt-1af3f9a6-3fb8-4df5-871a-4df67a8c31de",
  "timestamp": "2026-03-20T13:25:00Z",
  "action": "benefits.coupon.deleted",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678",
    "id": "promo-123"
  }
}
```

##### Coupons synced

`action: benefits.coupons.synced` -- full replacement of all coupons for the member.
Any previously known coupons not in the `coupons` array are removed.

```json
{
  "eventId": "evt-a32ca108-2ddd-455f-8ba0-bf9122e4ab46",
  "timestamp": "2026-04-01T07:15:00Z",
  "action": "benefits.coupons.synced",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678",
    "coupons": [
      {
        "id": "promo-123",
        "parentId": "campaign-456",
        "heading": "20% off all jackets",
        "description": "Valid on all jackets in store and online",
        "expiresOn": "2026-06-30T23:59:59Z",
        "redeemed": false,
        "redemption": {
          "type": "online_and_in_store",
          "link": "https://merchant.com/campaign",
          "promotionCode": "JACKET20"
        }
      }
    ]
  }
}
```

#### Stamp card actions

A stamp card is a "buy N, get 1 free" benefit with a `usage` block tracking progression.
`timesUsed` is the number of stamps the member has collected; `usesLeft` is the number of stamps still required to unlock the reward.

##### Stamp card updated

`action: benefits.stampCard.updated` -- a stamp card was created or progressed.

```json
{
  "eventId": "evt-9d6a5ad2-37fa-4a32-9ad3-b5e2c4fef1d7",
  "timestamp": "2026-03-05T14:20:00Z",
  "action": "benefits.stampCard.updated",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678",
    "stampCard": {
      "id": "stamp-789",
      "parentId": "stamp-campaign-1",
      "heading": "Buy 10 coffees, get 1 free",
      "description": "Collect a stamp on every coffee purchase.",
      "redeemed": false,
      "usage": {
        "timesUsed": 7,
        "usesLeft": 3
      },
      "redemption": {
        "type": "in_store"
      }
    }
  }
}
```

##### Stamp card deleted

`action: benefits.stampCard.deleted` -- remove a single stamp card by `id`.

```json
{
  "eventId": "evt-25d8b59f-91d6-4e9b-aab9-90d67e5abcd2",
  "timestamp": "2026-03-25T11:10:00Z",
  "action": "benefits.stampCard.deleted",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678",
    "id": "stamp-789"
  }
}
```

##### Stamp cards synced

`action: benefits.stampCards.synced` -- full replacement of all stamp cards for the member.
Any previously known stamp cards not in the `stampCards` array are removed.

```json
{
  "eventId": "evt-c44ef702-7d68-4cf3-9e30-f1b69dc5e5fa",
  "timestamp": "2026-04-01T07:16:00Z",
  "action": "benefits.stampCards.synced",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678",
    "stampCards": [
      {
        "id": "stamp-789",
        "parentId": "stamp-campaign-1",
        "heading": "Buy 10 coffees, get 1 free",
        "redeemed": false,
        "usage": {
          "timesUsed": 7,
          "usesLeft": 3
        },
        "redemption": {
          "type": "in_store"
        }
      }
    ]
  }
}
```

#### Bonus check actions

A bonus check is a voucher with a fixed monetary `amount` and `currency`.

##### Bonus check created

`action: benefits.bonusCheck.created` -- assign a new bonus check to the member.

```json
{
  "eventId": "evt-231297e2-95f9-404f-96a4-adf4e86d4f6a",
  "timestamp": "2026-04-10T12:00:00Z",
  "action": "benefits.bonusCheck.created",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678",
    "bonusCheck": {
      "id": "bc-001",
      "name": "Welcome bonus",
      "amount": 100,
      "currency": "NOK",
      "validFrom": "2026-01-01T00:00:00Z",
      "validUntil": "2026-06-30T23:59:59Z",
      "link": "https://merchant.com/redeem?code=BC001"
    }
  }
}
```

##### Bonus check deleted

`action: benefits.bonusCheck.deleted` -- remove a single bonus check by `id`.

```json
{
  "eventId": "evt-2da24b94-3629-4a63-b736-6287b812ea19",
  "timestamp": "2026-04-20T16:40:00Z",
  "action": "benefits.bonusCheck.deleted",
  "payload": {
    "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
    "phoneNumber": "+4712345678",
    "id": "bc-001"
  }
}
```

### Errors

The API returns a JSON error response on `400` and `401`:

```json
{
  "code": "BENEFIT-ERROR-001",
  "message": "Unknown action type",
  "details": [
    {
      "field": "action",
      "message": "Unknown action 'benefits.invalid.action'"
    }
  ]
}
```

| Status | Meaning |
| ------ | ------- |
| `200` | Event accepted. |
| `400` | Invalid event (unknown `action`, validation error, or malformed JSON). |
| `401` | Missing or invalid authentication. |

## Membership seeding webhook

When we don't yet have data for a user but need it (for example, the first time the user opens the benefits tab), we send an outbound `benefits.membership.seeding.v1` webhook to a callback URL that you have registered with our [Webhook API](https://developer.vippsmobilepay.com/docs/APIs/wallet-api/README.md).
Your server responds asynchronously by pushing an event sequence back to [`POST:/loyalty/v1/events`](https://developer.vippsmobilepay.com/redocusaurus/loyalty-swagger-id.yaml).

Set this up as follows:

1. [Register a callback URL](#register-a-callback-url)
2. [Receive a seeding request](#receive-a-seeding-request)
3. [Push the seed events](#push-the-seed-events)

### Register a callback URL

Registration happens through the [Webhooks API](https://developer.vippsmobilepay.com/docs/APIs/webhooks-api/README.md).

| Event type | Description |
|------------|-------------|
| `benefits.membership.seeding.v1` | Vipps MobilePay requests membership data for a user who has opened the benefits tab |

Subscribe to the event type `benefits.membership.seeding.v1` and persist the returned `secret` -- it is required to verify delivered webhook requests.

For the callback URL, use your webhook server address.

```bash
curl -X POST https://api.vipps.no/webhooks/v1/webhooks \
-H "Authorization: Bearer YOUR-ACCESS-TOKEN" \
-H "Content-Type: application/json" \
--data '{
    "url": "YOUR-CALLBACK-URL",
    "events": ["benefits.membership.seeding.v1"]
}'
```

You will receive a response similar to this, confirming that the webhook registration was successful:

```json
{
  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  "secret": "090a478d-37ff-4e77-970e-d457aeb26a3a"
}
```

Store the `secret` securely -- you will need it to verify the HMAC signature on incoming webhook requests.

For partner integrations managing several merchants, use a partner webhook and don't send `Merchant-Serial-Number` on the registration call.

### Receive a seeding request

| Field | Type | Description | Example |
|-------|------|-------------|---------|
| `requestId` | string | Idempotency key for the seed request. Use it to deduplicate retries. | `seed-71c7db5d-64ca-4f1e-a744-33f0b8c45f53` |
| `timestamp` | ISO 8601 UTC date-time | When Vipps created the request. | `2026-03-12T09:15:00Z` |
| `customerClubId` | string | UID for the customer club whose data should be seeded. | `4f94d690-7fb9-4e1a-9f7f-5c9389835ea6` |
| `phoneNumber` | string | Vipps user phone number in E.164 format. | `+4712345678` |
| `contactId` | string (optional) | Merchant member ID already known by Vipps. If present, echo it back as `payload.contactId`. | `c1b0dcc3-9878-4079-92aa-f3c00cada3ea` |
| `reason` | string | Why Vipps is asking for the seed. One of `initial_link`, `recovery`, or `manual_reseed`. | `initial_link` |

Example payload:

```json
{
  "requestId": "seed-71c7db5d-64ca-4f1e-a744-33f0b8c45f53",
  "timestamp": "2026-03-12T09:15:00Z",
  "customerClubId": "4f94d690-7fb9-4e1a-9f7f-5c9389835ea6",
  "phoneNumber": "+4712345678",
  "contactId": "c1b0dcc3-9878-4079-92aa-f3c00cada3ea",
  "reason": "initial_link"
}
```

The webhook uses the standard Vipps Webhooks HMAC authentication (`x-ms-date`, `x-ms-content-sha256`, `Authorization: HMAC-SHA256 ...`) signed with the `secret` issued at registration time.
See [Webhook request authentication](https://developer.vippsmobilepay.com/docs/APIs/webhooks-api/request-authentication.md) for details.

### Push the seed events

After acknowledging the webhook with `202 Accepted`, push the following sequence to [`POST:/loyalty/v1/events`](https://developer.vippsmobilepay.com/redocusaurus/loyalty-swagger-id.yaml):

1. `benefits.member.registered`
2. `benefits.coupons.synced` (with an empty `coupons` array if none)
3. `benefits.stampCards.synced` (with an empty `stampCards` array if none)
4. Zero or more `benefits.bonusCheck.created`

Both `*.synced` events must always be sent during seeding, even when the member has no items of that type, so Vipps knows the seed is complete.

**Do not return 404**

If no loyalty member exists for the supplied merchant and phone number, return a `2xx` response and emit no follow-up events.
The Webhooks API treats non-2xx responses as delivery failures and retries them with exponential backoff for up to seven days, so returning `404 Not Found` for a missing member will cause unnecessary retries.

> **Full site overview:** For every page in this documentation, read [https://developer.vippsmobilepay.com/llms.txt](https://developer.vippsmobilepay.com/llms.txt).
