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:
{
"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:
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" }'
{
"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:
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#
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:
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. |
{
"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, withfieldsnaming it, such asevents[1].entry_type). Fix it and send the whole batch again. - An event whose
idempotency_keywas already recorded is skipped, withreplayed: 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_pointsare the points earned in the current cycle (cycle_started_ontocycle_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.