Loyalty partners

For a loyalty programme that runs alongside a firm's policies, confirm each member's enrolment, change their status, and send the points and tiers they earn.

Some products come with a loyalty or rewards programme, run by a partner. The policyholder earns points, for example for healthy activity, and moves up tiers that bring rewards or discounts. This guide is for that partner. It covers the three things your systems send to Subcontext (confirming a new member, changing their status, and sending what they earn) and reading a membership back.

You need a server-only application with the write scope, made for you by the firm in its dashboard (see Access tokens). Reading a membership needs only read.

How a membership starts#

When a policy that comes with your programme is set up, Subcontext creates a membership for the policyholder, starting pending, and records a "Programme membership enrolled" event (programme.enrolled) on it.

Subcontext doesn't tell you about it by itself. The firm decides how you hear, by adding an action to that event under the product's Lifecycle events in its dashboard:

  • a Webhook action, which sends your endpoint a signed JSON message (see Lifecycle webhooks for the headers, signature and retries), or
  • a Call an endpoint action, a request in your own system's shape that the firm sets up with you on one of its custom integrations.

The webhook names the membership and the programme, and nothing about the person:

json
{
  "id": "3f9a1c2e-8b7d-4e6f-a5c4-b3a2918f7e6d",
  "type": "programme.enrolled",
  "occurred_at": "2026-10-12T10:30:15+01:00",
  "aggregate": { "type": "membership", "id": "0199a3c2-8800-7daa-8a2b-af1021324354" },
  "payload": {
    "programme_id": "0199a3c2-8900-7ebb-9b3c-b02132435465",
    "programme_slug": "rewards",
    "engine_mode": "external",
    "status": "pending"
  },
  "product_id": "1e2d3c4b-5a69-4788-96a5-b4c3d2e1f0a9",
  "organization_id": "e9f8d7c6-b5a4-4392-8170-6f5e4d3c2b1a"
}

aggregate.id is Subcontext's membership id. Read the membership for its policy number. Who the member is, and how you get their details to enrol them, is agreed with the firm. You enrol them in your own system, then confirm it to Subcontext with your own reference for the member.

A membership's status is one of:

status What it means
pending Created, waiting for you to confirm the enrolment.
active A member, earning points.
suspended Paused, for example while premiums are in arrears. Points you send are still recorded. It can be made active again.
cancelled Ended. Nothing more can change.

Confirm an enrolment#

Until you confirm it, find the membership by Subcontext's membership id. Send your own member reference and active:

sh
curl -X PATCH https://subcontext.com/api/v1/programme_memberships/0199a3c2-8800-7daa-8a2b-af1021324354 \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "status": "active", "external_member_ref": "VIT-1001" }'
json
{
  "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"
  }
}

From now on, use your reference in the address: /api/v1/programme_memberships/VIT-1001. A confirmed reference is fixed. Confirming again with the same one is harmless, and a different one is refused with 409 external_member_ref_mismatch. Sending external_member_ref on its own, with no status, sets the reference while the membership is still pending. Once it's confirmed, the same rule applies. If a reference was wrong, your firm's team can correct it in the dashboard.

Suspend, reinstate or cancel#

Send the new status:

sh
curl -X PATCH https://subcontext.com/api/v1/programme_memberships/VIT-1001 \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "status": "suspended" }'

The answer is the membership as it is now. PUT works the same as PATCH, so only the fields you send change.

A request that can't be used (an unknown status, nothing to change, or confirming without a reference) answers 422 validation_failed, with a message saying why. A change the membership's state won't allow answers 409: membership_cancelled (a cancelled membership can't change), invalid_transition (for example suspending a pending one) or external_member_ref_mismatch.

Read a membership#

sh
curl https://subcontext.com/api/v1/programme_memberships/VIT-1001 \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

The answer is the membership, in the same shape as above. Before you've confirmed it, use Subcontext's membership id in the address.

Send points and tiers#

Members earn once their enrolment is confirmed. Events for a pending membership answer 409 membership_pending. Send what members earn as a batch of events:

sh
curl -X POST https://subcontext.com/api/v1/programme_memberships/VIT-1001/events \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "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": "points_adjusted", "points": -100 },
      { "idempotency_key": "vit-evt-55122", "entry_type": "tier_asserted", "tier_key": "silver" }
    ]
  }'
Field What it is
idempotency_key Required. Unique to the event in your system.
entry_type points_awarded, points_adjusted (a correction, which can be negative) or tier_asserted (the tier your programme says they're on).
points For points events, how many.
tier_key For tier_asserted, the tier's key, as set up on the programme.
occurred_at When it happened (ISO 8601). Now if you leave it out.
payload Anything else to keep with it, such as the activity.
json
{
  "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": "points_adjusted", "points": -100, "tier_key": null, "replayed": false },
      { "id": "0199a3c2-8702-7daa-8a2b-af1021324354", "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": 400,
      "cycle_started_on": "2026-10-12",
      "cycle_ends_on": "2027-10-12"
    }
  }
}

data.events has one result for each event, in order, and data.membership the membership with its new points and tier.

  • If any event in the batch is refused, none is recorded (422 validation_failed, with fields naming it, such as events[1].entry_type). Fix it and send the whole batch again.
  • An event whose idempotency_key was already recorded is skipped, with replayed: true, so you can send a delivery that failed part way again.
  • Cancelled members earn nothing more. Their events answer 409 membership_cancelled.
  • Pending members earn nothing until you confirm the enrolment.
  • cycle_points are the points earned in the current cycle (cycle_started_on to cycle_ends_on).

Whether your tiers or Subcontext's decide a member's tier is set up on the programme with the firm. If yours do, send tier_asserted whenever a member's tier changes. If Subcontext's do, it works the tier out from the points.

Stuck, or something missing?

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

Contact us