openapi: 3.0.3
info:
  title: Vipps MobilePay Loyalty API
  version: 1.0.0
  description: >
    Loyalty API for merchants and CXP partners to push loyalty program data

    to the Vipps MobilePay benefits tab.


    This is a **write-only, event-based** API. All updates are sent as typed
    events

    to a single endpoint. Each event has an `action` field that determines the

    payload schema.


    ## Authentication

    All requests must include:

    - `Authorization: Bearer <token>` (access token from Vipps Access Token API
    standard authentication)

    - `Merchant-Serial-Number` header identifying the merchant

    - `Ocp-Apim-Subscription-Key` header with a valid API subscription key

    - `Vipps-System-Name`, `Vipps-System-Version`, `Vipps-System-Plugin-Name`,
    `Vipps-System-Plugin-Version`


    ## Event model

    Every request is an event with:

    - `eventId` — unique identifier for the event

    - `timestamp` — when the event happened (RFC 3339 / ISO 8601 `date-time`)

    - `action` — what happened (determines payload schema)

    - `payload.contactId` — merchant member identifier for the customer-club
    member

    - `payload.phoneNumber` — member phone number in E.164 format

    - `payload` — action-specific data


    ## Supported actions


    Actions are grouped into **per-product-type families** so the surface

    scales as new product types (e.g. deals) are added later. See

    [`event-taxonomy.md`](event-taxonomy.md) for the design rationale.


    | Action | Description |

    |--------|-------------|

    | `benefits.member.registered` | New member joined the loyalty program |

    | `benefits.member.updated` | Member data replaced (full member state) |

    | `benefits.member.deleted` | Member left the loyalty program |

    | `benefits.coupon.updated` | A coupon was created or changed |

    | `benefits.coupon.deleted` | A coupon was removed |

    | `benefits.coupons.synced` | Full replacement of all coupons for a member |

    | `benefits.stampCard.updated` | A stamp card was created or progressed |

    | `benefits.stampCard.deleted` | A stamp card was removed |

    | `benefits.stampCards.synced` | Full replacement of all stamp cards for a
    member |

    | `benefits.bonusCheck.created` | A bonus check/voucher was assigned |

    | `benefits.bonusCheck.deleted` | A bonus check/voucher was removed |
  contact:
    name: Vipps MobilePay - Team Benefits
    url: https://developer.vippsmobilepay.com
  license:
    name: Proprietary
servers:
  - url: https://api.vipps.no
    description: Production
  - url: https://apitest.vipps.no
    description: Test
security:
  - bearerAuth: []
    apiKey: []
    merchantSerial: []
paths:
  /loyalty/v1/events:
    post:
      summary: Push a loyalty event
      operationId: pushEvent
      description: |
        Single endpoint for all loyalty program updates. The `action` field
        determines which payload schema is expected.

        Events are processed asynchronously. A `200` response means the event
        was accepted and will be processed.
      parameters:
        - $ref: '#/components/parameters/vippsSystemName'
        - $ref: '#/components/parameters/vippsSystemVersion'
        - $ref: '#/components/parameters/vippsSystemPluginName'
        - $ref: '#/components/parameters/vippsSystemPluginVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BenefitsEvent'
            examples:
              memberRegistered:
                summary: benefits.member.registered — New member
                value:
                  eventId: evt-8a49f7b9-89cb-4e59-9db0-7f2f66a0a151
                  timestamp: '2025-01-15T10:00:00Z'
                  action: benefits.member.registered
                  payload:
                    contactId: c1b0dcc3-9878-4079-92aa-f3c00cada3ea
                    phoneNumber: '+4712345678'
                    joinedOn: '2024-01-15T10:00:00Z'
                    bonusPoints:
                      balance: 0
                    communicationPreferences:
                      acceptsEmail: true
                      acceptsSms: true
                      acceptsPostal: false
              memberUpdated:
                summary: benefits.member.updated — Full member state update
                value:
                  eventId: evt-75baf6f3-e20b-4067-a4df-cf8fb8eb3199
                  timestamp: '2025-02-01T08:30:00Z'
                  action: benefits.member.updated
                  payload:
                    contactId: c1b0dcc3-9878-4079-92aa-f3c00cada3ea
                    phoneNumber: '+4712345678'
                    bonusPoints:
                      balance: 2450
                      balanceExpires: '2025-12-31T23:59:59Z'
                      cashPoints: 125.5
                    membershipTier:
                      tierName: GOLD
                      points: 4500
                      pointsToNextLevel: 500
                    communicationPreferences:
                      acceptsEmail: true
                      acceptsSms: false
              memberDeleted:
                summary: benefits.member.deleted — Member removed
                value:
                  eventId: evt-bd2dc65f-2ea8-41e2-b3bd-c25b702f111a
                  timestamp: '2025-02-12T11:45:00Z'
                  action: benefits.member.deleted
                  payload:
                    contactId: c1b0dcc3-9878-4079-92aa-f3c00cada3ea
                    phoneNumber: '+4712345678'
              couponUpdated:
                summary: benefits.coupon.updated — New coupon assigned
                value:
                  eventId: evt-056850f5-86fd-476f-bdbd-e03e8dbf4461
                  timestamp: '2025-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: '2025-06-30T23:59:59Z'
                      redeemed: false
                      redemption:
                        type: online_and_in_store
                        link: https://merchant.com/campaign
                        promotionCode: JACKET20
              couponDeleted:
                summary: benefits.coupon.deleted — Coupon removed
                value:
                  eventId: evt-1af3f9a6-3fb8-4df5-871a-4df67a8c31de
                  timestamp: '2025-03-20T13:25:00Z'
                  action: benefits.coupon.deleted
                  payload:
                    contactId: c1b0dcc3-9878-4079-92aa-f3c00cada3ea
                    phoneNumber: '+4712345678'
                    id: promo-123
              couponsSynced:
                summary: benefits.coupons.synced — Full replacement of all coupons
                value:
                  eventId: evt-a32ca108-2ddd-455f-8ba0-bf9122e4ab46
                  timestamp: '2025-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: '2025-06-30T23:59:59Z'
                        redeemed: false
                        redemption:
                          type: online_and_in_store
                          link: https://merchant.com/campaign
                          promotionCode: JACKET20
              stampCardUpdated:
                summary: benefits.stampCard.updated — Stamp card progression
                value:
                  eventId: evt-9d6a5ad2-37fa-4a32-9ad3-b5e2c4fef1d7
                  timestamp: '2025-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
              stampCardDeleted:
                summary: benefits.stampCard.deleted — Stamp card removed
                value:
                  eventId: evt-25d8b59f-91d6-4e9b-aab9-90d67e5abcd2
                  timestamp: '2025-03-25T11:10:00Z'
                  action: benefits.stampCard.deleted
                  payload:
                    contactId: c1b0dcc3-9878-4079-92aa-f3c00cada3ea
                    phoneNumber: '+4712345678'
                    id: stamp-789
              stampCardsSynced:
                summary: >-
                  benefits.stampCards.synced — Full replacement of all stamp
                  cards
                value:
                  eventId: evt-c44ef702-7d68-4cf3-9e30-f1b69dc5e5fa
                  timestamp: '2025-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
              bonusCheckCreated:
                summary: benefits.bonusCheck.created — New bonus check
                value:
                  eventId: evt-231297e2-95f9-404f-96a4-adf4e86d4f6a
                  timestamp: '2025-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: '2025-01-01T00:00:00Z'
                      validUntil: '2025-06-30T23:59:59Z'
                      link: https://merchant.com/redeem?code=BC001
              bonusCheckDeleted:
                summary: benefits.bonusCheck.deleted — Bonus check removed
                value:
                  eventId: evt-2da24b94-3629-4a63-b736-6287b812ea19
                  timestamp: '2025-04-20T16:40:00Z'
                  action: benefits.bonusCheck.deleted
                  payload:
                    contactId: c1b0dcc3-9878-4079-92aa-f3c00cada3ea
                    phoneNumber: '+4712345678'
                    id: bc-001
      responses:
        '200':
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventResponse'
        '400':
          description: Invalid event (unknown action, validation error)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Access token from Vipps Access Token API standard authentication
        (`/accesstoken/get`)
    apiKey:
      type: apiKey
      in: header
      name: Ocp-Apim-Subscription-Key
      description: Vipps API subscription key
    merchantSerial:
      type: apiKey
      in: header
      name: Merchant-Serial-Number
      description: Merchant serial number (MSN) identifying the merchant
  parameters:
    vippsSystemName:
      name: Vipps-System-Name
      in: header
      required: true
      schema:
        type: string
        maxLength: 30
      description: Name of the integrating system
      example: acme-benefits-platform
    vippsSystemVersion:
      name: Vipps-System-Version
      in: header
      required: true
      schema:
        type: string
        maxLength: 30
      description: Version of the integrating system
      example: 3.1.2
    vippsSystemPluginName:
      name: Vipps-System-Plugin-Name
      in: header
      required: true
      schema:
        type: string
        maxLength: 30
      description: Name of plugin/module for the integration
      example: acme-benefits-connector
    vippsSystemPluginVersion:
      name: Vipps-System-Plugin-Version
      in: header
      required: true
      schema:
        type: string
        maxLength: 30
      description: Version of plugin/module for the integration
      example: 1.0.0
  schemas:
    BenefitsEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
      discriminator:
        propertyName: action
        mapping:
          benefits.member.registered: '#/components/schemas/MemberRegisteredEvent'
          benefits.member.updated: '#/components/schemas/MemberUpdatedEvent'
          benefits.member.deleted: '#/components/schemas/MemberDeletedEvent'
          benefits.coupon.updated: '#/components/schemas/CouponUpdatedEvent'
          benefits.coupon.deleted: '#/components/schemas/CouponDeletedEvent'
          benefits.coupons.synced: '#/components/schemas/CouponsSyncedEvent'
          benefits.stampCard.updated: '#/components/schemas/StampCardUpdatedEvent'
          benefits.stampCard.deleted: '#/components/schemas/StampCardDeletedEvent'
          benefits.stampCards.synced: '#/components/schemas/StampCardsSyncedEvent'
          benefits.bonusCheck.created: '#/components/schemas/BonusCheckCreatedEvent'
          benefits.bonusCheck.deleted: '#/components/schemas/BonusCheckDeletedEvent'
      oneOf:
        - $ref: '#/components/schemas/MemberRegisteredEvent'
        - $ref: '#/components/schemas/MemberUpdatedEvent'
        - $ref: '#/components/schemas/MemberDeletedEvent'
        - $ref: '#/components/schemas/CouponUpdatedEvent'
        - $ref: '#/components/schemas/CouponDeletedEvent'
        - $ref: '#/components/schemas/CouponsSyncedEvent'
        - $ref: '#/components/schemas/StampCardUpdatedEvent'
        - $ref: '#/components/schemas/StampCardDeletedEvent'
        - $ref: '#/components/schemas/StampCardsSyncedEvent'
        - $ref: '#/components/schemas/BonusCheckCreatedEvent'
        - $ref: '#/components/schemas/BonusCheckDeletedEvent'
    MemberRegisteredEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
          example: evt-8a49f7b9-89cb-4e59-9db0-7f2f66a0a151
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
          example: '2025-01-15T10:00:00Z'
        action:
          type: string
          enum:
            - benefits.member.registered
        payload:
          $ref: '#/components/schemas/MemberRegisteredPayload'
    MemberRegisteredPayload:
      type: object
      required:
        - contactId
        - phoneNumber
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
          example: c1b0dcc3-9878-4079-92aa-f3c00cada3ea
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
          example: '+4712345678'
        joinedOn:
          type: string
          format: date-time
        bonusPoints:
          $ref: '#/components/schemas/BonusPoints'
        membershipTier:
          $ref: '#/components/schemas/MembershipTier'
        communicationPreferences:
          $ref: '#/components/schemas/CommunicationPreferences'
    MemberUpdatedEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
          enum:
            - benefits.member.updated
        payload:
          $ref: '#/components/schemas/MemberUpdatedPayload'
    MemberUpdatedPayload:
      type: object
      description: |
        Full replacement of the member state for this merchant member.
        Partial/patch-style updates are not supported for this action.
      required:
        - contactId
        - phoneNumber
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
        bonusPoints:
          $ref: '#/components/schemas/BonusPoints'
        membershipTier:
          $ref: '#/components/schemas/MembershipTier'
        communicationPreferences:
          $ref: '#/components/schemas/CommunicationPreferences'
    MemberDeletedEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
          enum:
            - benefits.member.deleted
        payload:
          $ref: '#/components/schemas/MemberDeletedPayload'
      description: Removes the member and all associated data.
    MemberDeletedPayload:
      type: object
      required:
        - contactId
        - phoneNumber
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
    CouponUpdatedEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
          enum:
            - benefits.coupon.updated
        payload:
          $ref: '#/components/schemas/CouponUpdatedPayload'
    CouponUpdatedPayload:
      type: object
      required:
        - contactId
        - phoneNumber
        - coupon
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
        coupon:
          $ref: '#/components/schemas/Coupon'
    CouponDeletedEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
          enum:
            - benefits.coupon.deleted
        payload:
          $ref: '#/components/schemas/CouponDeletedPayload'
    CouponDeletedPayload:
      type: object
      required:
        - contactId
        - phoneNumber
        - id
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
        id:
          type: string
          description: The coupon ID to remove
    CouponsSyncedEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
          enum:
            - benefits.coupons.synced
        payload:
          $ref: '#/components/schemas/CouponsSyncedPayload'
    CouponsSyncedPayload:
      type: object
      required:
        - contactId
        - phoneNumber
        - coupons
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
        coupons:
          type: array
          description: |
            Complete list of the member's active coupons. Any previously
            known coupons not in this list will be removed.
          items:
            $ref: '#/components/schemas/Coupon'
    StampCardUpdatedEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
          enum:
            - benefits.stampCard.updated
        payload:
          $ref: '#/components/schemas/StampCardUpdatedPayload'
    StampCardUpdatedPayload:
      type: object
      required:
        - contactId
        - phoneNumber
        - stampCard
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
        stampCard:
          $ref: '#/components/schemas/StampCard'
    StampCardDeletedEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
          enum:
            - benefits.stampCard.deleted
        payload:
          $ref: '#/components/schemas/StampCardDeletedPayload'
    StampCardDeletedPayload:
      type: object
      required:
        - contactId
        - phoneNumber
        - id
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
        id:
          type: string
          description: The stamp card ID to remove
    StampCardsSyncedEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
          enum:
            - benefits.stampCards.synced
        payload:
          $ref: '#/components/schemas/StampCardsSyncedPayload'
    StampCardsSyncedPayload:
      type: object
      required:
        - contactId
        - phoneNumber
        - stampCards
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
        stampCards:
          type: array
          description: |
            Complete list of the member's active stamp cards. Any previously
            known stamp cards not in this list will be removed.
          items:
            $ref: '#/components/schemas/StampCard'
    BonusCheckCreatedEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
          enum:
            - benefits.bonusCheck.created
        payload:
          $ref: '#/components/schemas/BonusCheckCreatedPayload'
    BonusCheckCreatedPayload:
      type: object
      required:
        - contactId
        - phoneNumber
        - bonusCheck
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
        bonusCheck:
          $ref: '#/components/schemas/BonusCheck'
    BonusCheckDeletedEvent:
      type: object
      required:
        - eventId
        - timestamp
        - action
        - payload
      properties:
        eventId:
          type: string
          description: Unique identifier for this event
        timestamp:
          type: string
          format: date-time
          description: Timestamp for when the event occurred
        action:
          type: string
          enum:
            - benefits.bonusCheck.deleted
        payload:
          $ref: '#/components/schemas/BonusCheckDeletedPayload'
    BonusCheckDeletedPayload:
      type: object
      required:
        - contactId
        - phoneNumber
        - id
      properties:
        contactId:
          type: string
          description: Merchant member identifier for the customer-club member
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Member's phone number in E.164 format
        id:
          type: string
          description: The bonus check ID to remove
    Coupon:
      type: object
      description: |
        Discount / promotion delivered as a redeemable coupon. `redemption`
        is required and describes how the coupon can be used (in-store
        QR/barcode, online link + optional promotion code, or both).
      required:
        - id
        - parentId
        - heading
        - redemption
      properties:
        id:
          type: string
          description: Unique coupon identifier (CXP-assigned, per member)
          example: promo-123
        parentId:
          type: string
          description: |
            Identifier for the parent campaign/promotion. Multiple members may
            share the same parentId if they receive the same promotion.
          example: campaign-456
        heading:
          type: string
          description: Short title shown on the coupon card
          example: 20% off all jackets
        detailHeading:
          type: string
          description: Optional heading shown in the detail view (falls back to heading)
        description:
          type: string
          description: Longer description shown in the detail view
        expiresOn:
          type: string
          format: date-time
          description: Expiry date. Expired coupons are automatically hidden.
        redeemed:
          type: boolean
          default: false
        redemption:
          $ref: '#/components/schemas/Redemption'
    StampCard:
      type: object
      description: |
        Loyalty stamp card / "buy N get 1 free"-style benefit. `usage` is
        required and carries the progression: `timesUsed` = stamps
        collected, `usesLeft` = stamps remaining until the reward.
        `redemption` is optional — most stamp cards are redeemed in store
        without an online link or promotion code.
      required:
        - id
        - parentId
        - heading
        - usage
      properties:
        id:
          type: string
          description: Unique stamp card identifier (CXP-assigned, per member)
          example: stamp-789
        parentId:
          type: string
          description: |
            Identifier for the parent stamp-card campaign. Multiple members
            may share the same parentId if they participate in the same
            campaign.
          example: stamp-campaign-1
        heading:
          type: string
          description: Short title shown on the stamp card
          example: Buy 10 coffees, get 1 free
        detailHeading:
          type: string
          description: Optional heading shown in the detail view (falls back to heading)
        description:
          type: string
          description: Longer description shown in the detail view
        expiresOn:
          type: string
          format: date-time
          description: Expiry date. Expired stamp cards are automatically hidden.
        redeemed:
          type: boolean
          default: false
        usage:
          $ref: '#/components/schemas/StampCardUsage'
        redemption:
          $ref: '#/components/schemas/Redemption'
    Redemption:
      type: object
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - no_redemption
            - in_store
            - online
            - online_and_in_store
          description: |
            - `no_redemption`: Informational only
            - `in_store`: Redeemable in physical stores (via QR/barcode)
            - `online`: Redeemable online (link + optional promotion code)
            - `online_and_in_store`: Both online and in store
        link:
          type: string
          format: uri
          description: URL for online redemption
          example: https://merchant.com/campaign
        promotionCode:
          type: string
          description: Promotion code for online redemption
          example: JACKET20
    StampCardUsage:
      type: object
      description: |
        Progression for a stamp card. `timesUsed` is the number of stamps
        the member has collected; `usesLeft` is the number of stamps still
        required to unlock the reward.
      properties:
        timesUsed:
          type: integer
          minimum: 0
          example: 7
        usesLeft:
          type: integer
          minimum: 0
          example: 3
    BonusCheck:
      type: object
      required:
        - id
        - amount
        - currency
      properties:
        id:
          type: string
          example: bc-001
        name:
          type: string
          description: Display name for the bonus check
          example: Welcome bonus
        amount:
          type: integer
          minimum: 1
          example: 100
        currency:
          type: string
          minLength: 3
          maxLength: 3
          description: ISO 4217 currency code
          example: NOK
        validFrom:
          type: string
          format: date-time
        validUntil:
          type: string
          format: date-time
        link:
          type: string
          format: uri
          description: URL where the bonus check can be redeemed
    BonusPoints:
      type: object
      required:
        - balance
      properties:
        balance:
          type: integer
          minimum: 0
          description: Current bonus points balance (absolute value)
          example: 2450
        balanceExpires:
          type: string
          format: date-time
        cashPoints:
          type: number
          format: double
          description: Cash equivalent points (if applicable)
          example: 125.5
    MembershipTier:
      type: object
      required:
        - tierName
      properties:
        tierName:
          type: string
          example: GOLD
        points:
          type: integer
          example: 4500
        pointsToNextLevel:
          type: integer
          example: 500
    CommunicationPreferences:
      type: object
      properties:
        acceptsEmail:
          type: boolean
        acceptsSms:
          type: boolean
        acceptsPostal:
          type: boolean
    EventResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - accepted
        eventId:
          type: string
          format: uuid
          description: Server-assigned event ID for tracing
    ErrorResponse:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: BENEFIT-ERROR-001
        message:
          type: string
          example: Unknown action type
        details:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string
