Loyalty API guide
This API is in development and not yet available.
This guide covers authentication, the event model, the inbound POST:/loyalty/v1/events 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, 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.
See 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.
Push a loyalty event​
The POST:/loyalty/v1/events 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. |
payload | Action-specific data. See each action 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
GETendpoints; 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, andbenefits.stampCards.syncedreplace the entire member state or list. Partial updates are not supported. - Redemption variants. Coupons and stamp cards share a
redemptionobject withno_redemption,in_store,online, oronline_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 |
benefits.member.updated | member fields | Member updated |
benefits.member.deleted | contactId, phoneNumber | Member deleted |
benefits.coupon.updated | coupon | Coupon updated |
benefits.coupon.deleted | id | Coupon deleted |
benefits.coupons.synced | coupons[] | Coupons synced |
benefits.stampCard.updated | stampCard | Stamp card updated |
benefits.stampCard.deleted | id | Stamp card deleted |
benefits.stampCards.synced | stampCards[] | Stamp cards synced |
benefits.bonusCheck.created | bonusCheck | Bonus check created |
benefits.bonusCheck.deleted | id | Bonus check deleted |
For example, to register a member:
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:
{
"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.
{
"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.
{
"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.
{
"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).
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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:
{
"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.
Your server responds asynchronously by pushing an event sequence back to POST:/loyalty/v1/events.
Set this up as follows:
Register a callback URL​
Registration happens through the Webhooks API.
| 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.
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:
{
"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:
{
"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 for details.
Push the seed events​
After acknowledging the webhook with 202 Accepted, push the following sequence to POST:/loyalty/v1/events:
benefits.member.registeredbenefits.coupons.synced(with an emptycouponsarray if none)benefits.stampCards.synced(with an emptystampCardsarray if none)- 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.
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.