Loyalty

For a loyalty programme partner: read a membership, confirm a policyholder's enrolment, suspend or cancel it, and send the points and tiers they earn.

For a loyalty programme partner: read a membership, confirm a policyholder's enrolment, suspend or cancel it, and send the points and tiers they earn.

These need a server-only application, with write for changes and read or write to read a membership. The guide Loyalty partners walks through it.

Send points and tiers

POST https://subcontext.com/api/v1/programme_memberships/{external_ref}/events Try it

Token scopes: write

Sends what a member has earned: points awarded or adjusted, and the tier your programme says they're on. Members earn once their enrolment is confirmed. Until then this answers 409. Either all of a batch is recorded or none of it is, so you can send a failed delivery again. Each event needs its own idempotency_key, and one already recorded is skipped (replayed is true).

Server-only applications, with the write scope.

Parameters

NameInTypeDescription
external_ref required path string

Your own reference for the member (external_member_ref), or, before you've confirmed the enrolment, Subcontext's membership id.

Request body application/json

FieldTypeDescription
events required array of object
events[].idempotency_key required string

Unique to this event in your system.

events[].entry_type required string

Points earned, a correction to points (can be negative), or the tier the member is on.

One of: "points_awarded", "points_adjusted", "tier_asserted"
events[].points integer

For points events, how many.

events[].tier_key string

For tier_asserted, the tier's key (as set up on the programme).

events[].occurred_at string (date-time)

When it happened (ISO 8601). Now if left out.

events[].payload object

Anything else to keep with the event, such as the activity that earned the points.

Example request
{
  "events": [
    {
      "idempotency_key": "vit-evt-55120",
      "entry_type": "points_awarded",
      "points": 500,
      "occurred_at": "2026-10-11T07:30:00Z",
      "payload": {
        "activity": "weekly_steps"
      }
    },
    {
      "idempotency_key": "vit-evt-55121",
      "entry_type": "tier_asserted",
      "tier_key": "silver"
    }
  ]
}

Responses

200 The batch is recorded. The membership shows its new points and tier.

FieldTypeDescription
data required object
data.events required array of object

One for each event, in order.

data.events[].id required string (uuid)
data.events[].entry_type required string One of: "points_awarded", "points_adjusted", "tier_asserted"
data.events[].points required integer or null
data.events[].tier_key required string or null
data.events[].replayed required boolean

true when this event was already recorded.

data.membership required Membership

A policyholder's membership of your loyalty programme.

data.membership.id required string (uuid)

Subcontext's id for the membership.

data.membership.programme_slug required string
data.membership.programme_name required string
data.membership.policy_number required string
data.membership.status required string One of: "pending", "active", "suspended", "cancelled"
data.membership.external_member_ref required string or null

Your reference for the member, once confirmed.

data.membership.current_tier_key required string or null
data.membership.cycle_points required integer

Points earned this cycle.

data.membership.cycle_started_on required string or null (date)
data.membership.cycle_ends_on required string or null (date)
Example
{
  "data": {
    "events": [
      {
        "id": "0199a3c2-8700-7b88-8e09-8dfe0f102132",
        "entry_type": "points_awarded",
        "points": 500,
        "tier_key": null,
        "replayed": false
      },
      {
        "id": "0199a3c2-8701-7c99-9f1a-9e0f10213243",
        "entry_type": "tier_asserted",
        "points": 0,
        "tier_key": "silver",
        "replayed": false
      }
    ],
    "membership": {
      "id": "0199a3c2-8800-7daa-8a2b-af1021324354",
      "programme_slug": "rewards",
      "programme_name": "Rewards",
      "policy_number": "POL-AB-P001-000042",
      "status": "active",
      "external_member_ref": "VIT-1001",
      "current_tier_key": "silver",
      "cycle_points": 2400,
      "cycle_started_on": "2026-01-01",
      "cycle_ends_on": "2026-12-31"
    }
  }
}

401 No token was sent, or it isn't valid because it has expired, was revoked (for example because the application's secret was rotated), or was cut short. Get a new token and try once more.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_token",
    "message": "The access token has expired. Ask for a new one."
  }
}

403 The token can't be used here. It's missing a scope, it was used from the wrong place (a server-only application's token in a browser, or a browser application's token from a server, another website or a request only server-only applications may make), or it wasn't asked for with `resource=https://subcontext.com/api/v1`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "forbidden",
    "message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
  }
}

404 There's no such record in your organisation.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "not_found",
    "message": "Resource not found"
  }
}

409 Nothing was recorded: - `membership_pending`: the enrolment isn't confirmed yet. Confirm it with [Update a membership](/developers/api/reference/loyalty#updateMembership) first. - `membership_cancelled`: the membership is cancelled and earns nothing more.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "membership_cancelled",
    "message": "this membership is cancelled and can't be changed"
  }
}

422 `validation_failed`: one of the events isn't valid (an unknown `entry_type`, a missing `idempotency_key` or tier, a time that isn't ISO 8601), or `events` is missing. `fields` names it, as `events[1].entry_type`. Nothing in the batch was recorded.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "validation_failed",
    "message": "events[1].idempotency_key is required on every event",
    "fields": {
      "events[1].idempotency_key": "is required on every event"
    }
  }
}

Get a membership

GET https://subcontext.com/api/v1/programme_memberships/{external_ref} Try it

Token scopes: read or write

A membership as it is now: its status, your reference, its tier and the points earned this cycle. Before you've confirmed the enrolment, find it by Subcontext's membership id. After that, find it by your reference.

Server-only applications.

Parameters

NameInTypeDescription
external_ref required path string

Your own reference for the member (external_member_ref), or, before you've confirmed the enrolment, Subcontext's membership id.

Responses

200 The membership.

FieldTypeDescription
data required Membership

A policyholder's membership of your loyalty programme.

data.id required string (uuid)

Subcontext's id for the membership.

data.programme_slug required string
data.programme_name required string
data.policy_number required string
data.status required string One of: "pending", "active", "suspended", "cancelled"
data.external_member_ref required string or null

Your reference for the member, once confirmed.

data.current_tier_key required string or null
data.cycle_points required integer

Points earned this cycle.

data.cycle_started_on required string or null (date)
data.cycle_ends_on required string or null (date)
Example
{
  "data": {
    "id": "0199a3c2-8800-7daa-8a2b-af1021324354",
    "programme_slug": "rewards",
    "programme_name": "Rewards",
    "policy_number": "POL-AB-P001-000042",
    "status": "pending",
    "external_member_ref": null,
    "current_tier_key": "bronze",
    "cycle_points": 0,
    "cycle_started_on": "2026-10-12",
    "cycle_ends_on": "2027-10-12"
  }
}

401 No token was sent, or it isn't valid because it has expired, was revoked (for example because the application's secret was rotated), or was cut short. Get a new token and try once more.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_token",
    "message": "The access token has expired. Ask for a new one."
  }
}

403 The token can't be used here. It's missing a scope, it was used from the wrong place (a server-only application's token in a browser, or a browser application's token from a server, another website or a request only server-only applications may make), or it wasn't asked for with `resource=https://subcontext.com/api/v1`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "forbidden",
    "message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
  }
}

404 There's no such record in your organisation.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "not_found",
    "message": "Resource not found"
  }
}

Update a membership (PUT)

PUT https://subcontext.com/api/v1/programme_memberships/{external_ref} Try it

Token scopes: write

The same as Update a membership, for systems that send PUT. It works as PATCH does. Only the fields you send change, and it doesn't replace the whole membership.

Parameters

NameInTypeDescription
external_ref required path string

Your own reference for the member (external_member_ref), or, before you've confirmed the enrolment, Subcontext's membership id.

Request body application/json

FieldTypeDescription
status string

The new status.

One of: "active", "suspended", "cancelled"
external_member_ref string

Your reference for the member. Required to confirm a pending enrolment.

Example request
{
  "status": "suspended"
}

Responses

200 The membership, as it is now.

FieldTypeDescription
data required Membership

A policyholder's membership of your loyalty programme.

data.id required string (uuid)

Subcontext's id for the membership.

data.programme_slug required string
data.programme_name required string
data.policy_number required string
data.status required string One of: "pending", "active", "suspended", "cancelled"
data.external_member_ref required string or null

Your reference for the member, once confirmed.

data.current_tier_key required string or null
data.cycle_points required integer

Points earned this cycle.

data.cycle_started_on required string or null (date)
data.cycle_ends_on required string or null (date)
Example
{
  "data": {
    "id": "0199a3c2-8800-7daa-8a2b-af1021324354",
    "programme_slug": "rewards",
    "programme_name": "Rewards",
    "policy_number": "POL-AB-P001-000042",
    "status": "active",
    "external_member_ref": "VIT-1001",
    "current_tier_key": "bronze",
    "cycle_points": 0,
    "cycle_started_on": "2026-10-12",
    "cycle_ends_on": "2027-10-12"
  }
}

401 No token was sent, or it isn't valid because it has expired, was revoked (for example because the application's secret was rotated), or was cut short. Get a new token and try once more.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_token",
    "message": "The access token has expired. Ask for a new one."
  }
}

403 The token can't be used here. It's missing a scope, it was used from the wrong place (a server-only application's token in a browser, or a browser application's token from a server, another website or a request only server-only applications may make), or it wasn't asked for with `resource=https://subcontext.com/api/v1`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "forbidden",
    "message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
  }
}

404 There's no such record in your organisation.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "not_found",
    "message": "Resource not found"
  }
}

409 The membership's state won't allow it: - `membership_cancelled`: it's cancelled, and can't be changed. - `invalid_transition`: not from the status it's in (for example suspending a `pending` one). - `external_member_ref_mismatch`: it has a confirmed reference, and this one is different.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_transition",
    "message": "a pending membership can't become suspended"
  }
}

422 `validation_failed`: the request can't be used (the message says why): an unknown `status`, nothing to change, or confirming without `external_member_ref`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "validation_failed",
    "message": "external_member_ref is required to confirm the enrolment",
    "fields": {
      "status": "must be active, suspended or cancelled, with external_member_ref to confirm an enrolment"
    }
  }
}

Update a membership

PATCH https://subcontext.com/api/v1/programme_memberships/{external_ref} Try it

Token scopes: write

Confirms an enrolment or changes a membership's status.

When a policyholder joins, the membership starts pending, and you find it by Subcontext's membership id. Confirm it by sending your own member reference (external_member_ref) and status: active. From then on, use your reference in the address.

Later, send status to suspend (suspended), reinstate (active) or end (cancelled) it. A cancelled membership can't be changed.

Sending external_member_ref on its own, with no status, records it as the member's reference while the membership is pending. Once a reference is confirmed it's fixed. Sending the same one again is harmless, and a different one is refused (409 external_member_ref_mismatch), as when confirming. Only your firm's own team can correct a confirmed reference, in the dashboard.

Server-only applications, with the write scope.

Parameters

NameInTypeDescription
external_ref required path string

Your own reference for the member (external_member_ref), or, before you've confirmed the enrolment, Subcontext's membership id.

Request body application/json

FieldTypeDescription
status string

The new status.

One of: "active", "suspended", "cancelled"
external_member_ref string

Your reference for the member. Required to confirm a pending enrolment.

Example request
{
  "status": "active",
  "external_member_ref": "VIT-1001"
}

Responses

200 The membership, as it is now.

FieldTypeDescription
data required Membership

A policyholder's membership of your loyalty programme.

data.id required string (uuid)

Subcontext's id for the membership.

data.programme_slug required string
data.programme_name required string
data.policy_number required string
data.status required string One of: "pending", "active", "suspended", "cancelled"
data.external_member_ref required string or null

Your reference for the member, once confirmed.

data.current_tier_key required string or null
data.cycle_points required integer

Points earned this cycle.

data.cycle_started_on required string or null (date)
data.cycle_ends_on required string or null (date)
Example
{
  "data": {
    "id": "0199a3c2-8800-7daa-8a2b-af1021324354",
    "programme_slug": "rewards",
    "programme_name": "Rewards",
    "policy_number": "POL-AB-P001-000042",
    "status": "active",
    "external_member_ref": "VIT-1001",
    "current_tier_key": "bronze",
    "cycle_points": 0,
    "cycle_started_on": "2026-10-12",
    "cycle_ends_on": "2027-10-12"
  }
}

401 No token was sent, or it isn't valid because it has expired, was revoked (for example because the application's secret was rotated), or was cut short. Get a new token and try once more.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_token",
    "message": "The access token has expired. Ask for a new one."
  }
}

403 The token can't be used here. It's missing a scope, it was used from the wrong place (a server-only application's token in a browser, or a browser application's token from a server, another website or a request only server-only applications may make), or it wasn't asked for with `resource=https://subcontext.com/api/v1`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "forbidden",
    "message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
  }
}

404 There's no such record in your organisation.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "not_found",
    "message": "Resource not found"
  }
}

409 The membership's state won't allow it: - `membership_cancelled`: it's cancelled, and can't be changed. - `invalid_transition`: not from the status it's in (for example suspending a `pending` one). - `external_member_ref_mismatch`: it has a confirmed reference, and this one is different.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_transition",
    "message": "a pending membership can't become suspended"
  }
}

422 `validation_failed`: the request can't be used (the message says why): an unknown `status`, nothing to change, or confirming without `external_member_ref`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "validation_failed",
    "message": "external_member_ref is required to confirm the enrolment",
    "fields": {
      "status": "must be active, suspended or cancelled, with external_member_ref to confirm an enrolment"
    }
  }
}

Stuck, or something missing?

Get in touch and a person on the team will help.

Contact us