# Members

Shoppers holding points, and the codes they hold.

Part of [the API reference, version 2026-10](https://matrixrewards.co.uk/docs/api/2026-10.md). Every address below starts with the base address:

```text
https://app.matrixrewards.co.uk/api/2026-10
```

## Shoppers on this account {#listMembers}

`GET /members` — the full address is `https://app.matrixrewards.co.uk/api/2026-10/members`.

**Needs:** the `read_members` permission (in the app: “Members, their points, their email addresses and the last four characters of their reward codes”). A key with `write_members` can call it too.

Recorded in the audit log as a disclosure, counted in shoppers.

### Parameters

| Name | In | Required | Type | What it is |
| --- | --- | --- | --- | --- |
| `q` | query | no | string, up to 120 characters | Email, member key or store customer reference. At most 120 characters. |
| `store` | query | no | string | Only members with a customer record at this store. |
| `sort` | query | no | one of `member_key`, `balance`, `enrolled_at`, `last_activity_at` | — |
| `order` | query | no | one of `asc`, `desc` | — |
| `limit` | query | no | integer, from 1 to 100, default `25` | 1 to 100. Larger values are capped. |
| `cursor` | query | no | string | Opaque. Pass back `next_cursor` from the previous page, with the same filters, and on `GET /members` the same `sort` and `order`. |

### The answer

A page of members.

| Field | Type | Always there | What it is |
| --- | --- | --- | --- |
| `data` | list of object (`MemberSummary`) | yes | — |
| `data[].member_key` | string | yes | — |
| `data[].email` | string | yes | — |
| `data[].balance_points` | integer | yes | The maintained balance, which the engine recomputes from the ledger under a lock on every write. `GET /members/{member_key}` sums the ledger rows there and then; the two agree unless something is wrong, and that is the only difference between them. |
| `data[].enrolled_at` | string | yes | — |
| `data[].last_activity_at` | string | yes | — |
| `has_more` | boolean | yes | — |
| `next_cursor` | string, or null | yes | — |

**Headers on the answer:** `Request-Id`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`. [What each header means](https://matrixrewards.co.uk/docs/api/2026-10.md#headers).

### Example

```bash
curl "https://app.matrixrewards.co.uk/api/2026-10/members" \
  -H "Authorization: Bearer $MATRIX_API_KEY"
```

The answer:

```json
{
  "data": [
    {
      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
      "email": "alex@example.com",
      "balance_points": 640,
      "enrolled_at": "2026-08-04T11:02:00Z",
      "last_activity_at": "2026-08-04T11:02:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### Other answers

| Status | What it means | Error pages |
| --- | --- | --- |
| 400 | `invalid_request`: a parameter could not be read; `param` names it. `invalid_cursor`: the `cursor` is not one this list issued. | [invalid_request](https://matrixrewards.co.uk/docs/errors/invalid_request.md), [invalid_cursor](https://matrixrewards.co.uk/docs/errors/invalid_cursor.md) |
| 401 | `unauthenticated`: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer. | [unauthenticated](https://matrixrewards.co.uk/docs/errors/unauthenticated.md) |
| 403 | `forbidden_scope`: the key does not carry the scope this call needs; `required_scope` says which. | [forbidden_scope](https://matrixrewards.co.uk/docs/errors/forbidden_scope.md) |
| 429 | `rate_limited`: too many requests for this key; `bucket` says which allowance. `Retry-After` says how long to wait. | [rate_limited](https://matrixrewards.co.uk/docs/errors/rate_limited.md) |
| 503 | `access_not_recorded`: the read could not be written to the account's audit log, so no data was returned. | [access_not_recorded](https://matrixrewards.co.uk/docs/errors/access_not_recorded.md) |

## One shopper's record {#getMember}

`GET /members/{member_key}` — the full address is `https://app.matrixrewards.co.uk/api/2026-10/members/{member_key}`.

**Needs:** the `read_members` permission (in the app: “Members, their points, their email addresses and the last four characters of their reward codes”). A key with `write_members` can call it too.

Email is the only contact detail returned. The birthday a merchant's own staff screen can show is never returned here.

### Parameters

| Name | In | Required | Type | What it is |
| --- | --- | --- | --- | --- |
| `member_key` | path | yes | string | — |

### The answer

The member.

| Field | Type | Always there | What it is |
| --- | --- | --- | --- |
| `member_key` | string | yes | — |
| `email` | string | yes | The only contact detail this API returns. |
| `balance_points` | integer | yes | Summed from the ledger, not read from the balance cache. |
| `enrolled_at` | string | yes | — |
| `last_activity_at` | string | yes | — |
| `points_expire_at` | string, or null | yes | Null when expiry is off, which is the default. |
| `stores` | list of object | yes | — |
| `stores[].store_key` | string | yes | — |
| `stores[].customer_ref` | string | yes | The merchant's own customer identifier at that store. |
| `stores[].linked_at` | string | yes | — |
| `points_by_store` | list of object | yes | — |
| `points_by_store[].store_key` | string | yes | — |
| `points_by_store[].store_name` | string, or null | yes | — |
| `points_by_store[].earned_points` | integer | yes | Points the shopper got at this store: orders, bonuses and credits from staff. |
| `points_by_store[].spent_points` | integer | yes | Points exchanged for a reward at this store. Only redemptions: refunds, expiry and staff removals are in other_points. |
| `points_by_store[].other_points` | integer | yes | Signed. Every other movement at this store: points taken back on a refund, expired, removed by staff, returned when a reward was cancelled, or carried over when another of the shopper's store records was deleted. earned_points - spent_points + other_points is this store's net. |
| `outstanding_codes` | integer | yes | — |
| `history_url` | string | yes | — |

**Headers on the answer:** `Request-Id`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`. [What each header means](https://matrixrewards.co.uk/docs/api/2026-10.md#headers).

### Example

```bash
export MEMBER='<member_key from GET /members>'
curl "https://app.matrixrewards.co.uk/api/2026-10/members/$MEMBER" \
  -H "Authorization: Bearer $MATRIX_API_KEY"
```

The answer:

```json
{
  "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
  "email": "alex@example.com",
  "balance_points": 640,
  "enrolled_at": "2026-08-04T11:02:00Z",
  "last_activity_at": "2026-08-04T11:02:00Z",
  "points_expire_at": null,
  "stores": [
    {
      "store_key": "example-store",
      "customer_ref": "gid://shopify/Customer/7001",
      "linked_at": "2026-08-04T11:02:00Z"
    }
  ],
  "points_by_store": [
    {
      "store_key": "example-store",
      "store_name": "Example Store",
      "earned_points": 640,
      "spent_points": 0,
      "other_points": 0
    }
  ],
  "outstanding_codes": 1,
  "history_url": "/api/2026-10/ledger?member_key=mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18"
}
```

### Other answers

| Status | What it means | Error pages |
| --- | --- | --- |
| 401 | `unauthenticated`: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer. | [unauthenticated](https://matrixrewards.co.uk/docs/errors/unauthenticated.md) |
| 403 | `forbidden_scope`: the key does not carry the scope this call needs; `required_scope` says which. | [forbidden_scope](https://matrixrewards.co.uk/docs/errors/forbidden_scope.md) |
| 404 | `not_found`: no such resource on this account — indistinguishable from one that does not exist at all. | [not_found](https://matrixrewards.co.uk/docs/errors/not_found.md) |
| 429 | `rate_limited`: too many requests for this key; `bucket` says which allowance. `Retry-After` says how long to wait. | [rate_limited](https://matrixrewards.co.uk/docs/errors/rate_limited.md) |
| 503 | `access_not_recorded`: the read could not be written to the account's audit log, so no data was returned. | [access_not_recorded](https://matrixrewards.co.uk/docs/errors/access_not_recorded.md) |

## The reward codes this shopper holds {#listMemberCodes}

`GET /members/{member_key}/codes` — the full address is `https://app.matrixrewards.co.uk/api/2026-10/members/{member_key}/codes`.

**Needs:** the `read_members` permission (in the app: “Members, their points, their email addresses and the last four characters of their reward codes”). A key with `write_members` can call it too.

Paged: a shopper who has redeemed for two years has a history, so this list is keyset-paged like the others. Each code is named by its last four characters and its `ref`, never whole — and so is the cursor, which is opaque but not secret.

### Parameters

| Name | In | Required | Type | What it is |
| --- | --- | --- | --- | --- |
| `member_key` | path | yes | string | — |
| `status` | query | no | string | 'minted' is issued and unspent. |
| `limit` | query | no | integer, from 1 to 100, default `25` | 1 to 100. Larger values are capped. |
| `cursor` | query | no | string | Opaque. Pass back `next_cursor` from the previous page, with the same filters, and on `GET /members` the same `sort` and `order`. |

### The answer

The codes.

| Field | Type | Always there | What it is |
| --- | --- | --- | --- |
| `data` | list of object | yes | — |
| `data[].code_ending` | string, or null | yes | The last four characters of the code: enough to match what the shopper reads out. Null when the code's final run of letters and digits is shorter than twelve, as a code your own connector recorded in a short or prefixed format can be, because its last four would be all of its random part; name it by `ref`. The whole code is never returned by this API — an unused code is a live discount, and API answers end up in logs and assistant transcripts. |
| `data[].ref` | string, matching `^[0-9a-f]{20}$` | yes | The code's reference: what `cancelMemberCode` takes. It is derived from the redemption, never from the code, so it says nothing about the code. |
| `data[].store_key` | string | yes | — |
| `data[].reward_id` | string | yes | The reward the code was bought with, as `listRewards` gives its id; `@any-amount` for a code the shopper bought by choosing how many points to spend (`spend_any_amount`). A merchant's reward id never starts with `@`. |
| `data[].cost_points` | integer | yes | — |
| `data[].status` | string | yes | — |
| `data[].issued_at` | string | yes | — |
| `has_more` | boolean | yes | — |
| `next_cursor` | string, or null | yes | — |

**Headers on the answer:** `Request-Id`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`. [What each header means](https://matrixrewards.co.uk/docs/api/2026-10.md#headers).

### Example

```bash
export MEMBER='<member_key from GET /members>'
curl "https://app.matrixrewards.co.uk/api/2026-10/members/$MEMBER/codes" \
  -H "Authorization: Bearer $MATRIX_API_KEY"
```

The answer:

```json
{
  "data": [
    {
      "code_ending": "4TPB",
      "ref": "5f0c2d8e91a47b36c0de",
      "store_key": "example-store",
      "reward_id": "reward_5",
      "cost_points": 500,
      "status": "minted",
      "issued_at": "2026-09-10T16:40:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### Other answers

| Status | What it means | Error pages |
| --- | --- | --- |
| 400 | `invalid_cursor`: the `cursor` is not one this list issued. | [invalid_cursor](https://matrixrewards.co.uk/docs/errors/invalid_cursor.md) |
| 401 | `unauthenticated`: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer. | [unauthenticated](https://matrixrewards.co.uk/docs/errors/unauthenticated.md) |
| 403 | `forbidden_scope`: the key does not carry the scope this call needs; `required_scope` says which. | [forbidden_scope](https://matrixrewards.co.uk/docs/errors/forbidden_scope.md) |
| 404 | `not_found`: no such resource on this account — indistinguishable from one that does not exist at all. | [not_found](https://matrixrewards.co.uk/docs/errors/not_found.md) |
| 429 | `rate_limited`: too many requests for this key; `bucket` says which allowance. `Retry-After` says how long to wait. | [rate_limited](https://matrixrewards.co.uk/docs/errors/rate_limited.md) |
| 503 | `access_not_recorded`: the read could not be written to the account's audit log, so no data was returned. | [access_not_recorded](https://matrixrewards.co.uk/docs/errors/access_not_recorded.md) |

## Add points to a member's balance, or take them away {#adjustMemberPoints}

`POST /members/{member_key}/adjustments` — the full address is `https://app.matrixrewards.co.uk/api/2026-10/members/{member_key}/adjustments`.

**Needs:** the `write_members` permission (in the app: “Change members’ points and cancel reward codes. This also reads members, their email addresses and the last four characters of their reward codes. Every change is logged”).

Booked to the member's earliest-linked store on this account, with the reason on the shopper's statement. At most 100,000 points either way per change, and per account at most 100,000 points added and 100,000 taken away by points changes through the API in any rolling 24 hours, across all its keys (a cancelled code's points are not counted). A test key can preview and is refused the change (403 `test_mode_key`). Written to the audit log in the same transaction as the change: if the entry cannot be written, the change is not made.

### Parameters

| Name | In | Required | Type | What it is |
| --- | --- | --- | --- | --- |
| `member_key` | path | yes | string | — |
| `Idempotency-Key` | header | yes | string, matching `^[A-Za-z0-9._:-]{1,100}$` | Required on both writes, a points change and a cancel. Pick a new one for each new change and send the same one to retry it. For 24 hours from its first use, a key has three answers: the same request again gets the first answer back, byte for byte, with `Idempotency-Replayed: true`; a different request under the key is 422 `idempotency_key_reused`; and while the first request is still running, 409 `idempotency_key_in_use` with `Retry-After`. A 5xx does not use the key up: retry with the same one. For a points change the key is also the adjustment's id in the ledger, so the same change is never booked twice, whichever of the account's keys sends it, and even after the 24 hours. |
| `dry_run` | query | no | boolean | `true` previews the change: every check the real call makes runs, the answer has the status the real call would have, and nothing changes — except one `dry_run` entry in the audit log, because a preview shows the member's balance. When the change would happen, the preview carries a `confirmation` for exactly this request. It is the only query parameter a write takes: any other, a misspelling such as `dryRun` included, is refused with 400 `invalid_request` naming it in `param`, and nothing changes. |
| `Matrix-Confirmation` | header | no | string, up to 1,024 characters | The `confirmation.token` a dry run of this exact request returned. Required for keys tagged for an assistant, except on a replay: a request whose first answer is still kept, or a points change that is already made; checked whenever it is sent. It covers one request — the same key, member, body and Idempotency-Key — for 10 minutes. It proves the change was previewed first; it cannot prove a person saw the preview. |

### Request body

Sent as `application/json`.

| Field | Type | Required | What it is |
| --- | --- | --- | --- |
| `delta_points` | integer, from -100,000 to 100,000, not 0 | yes | Points to add (positive) or take away (negative). Not zero; at most 100,000 either way. |
| `reason` | string, up to 200 characters, at least 1 character | yes | Why. Shown to the shopper on their statement, and kept in the audit log. |

### The answer

The change, or with `dry_run=true` the preview of it.

**One shape: `AdjustmentResult`.**

| Field | Type | Always there | What it is |
| --- | --- | --- | --- |
| `object` | always `adjustment` | yes | — |
| `member_key` | string | yes | — |
| `delta_points` | integer | yes | — |
| `balance_points` | integer | yes | The member's balance afterwards. |
| `store_key` | string | yes | The store the change is booked to: the member's earliest-linked store on this account. |
| `replayed` | boolean | yes | True when the ledger already held this change (the same Idempotency-Key, member and amount) and nothing new was booked. |
| `idempotency_key` | string | yes | — |
| `audit_id` | string | yes | The audit-log entry this answer rests on. |

**One shape: `AdjustmentPreview`.**

| Field | Type | Always there | What it is |
| --- | --- | --- | --- |
| `object` | always `adjustment_preview` | yes | — |
| `dry_run` | always `true` | yes | — |
| `outcome` | one of `would_apply`, `would_replay` | yes | — |
| `member_key` | string | yes | — |
| `delta_points` | integer | yes | — |
| `balance_points_before` | integer | yes | — |
| `balance_points_after` | integer | yes | What the real call would report. Equal to before when it would replay. |
| `store_key` | string | yes | — |
| `limit` | object, or null | yes | The account's rolling 24-hour limit in this change's direction, before it. Null when the change would be a replay (a replay spends nothing). |
| `limit.direction` | one of `credit`, `debit` | yes | — |
| `limit.limit_points` | integer | yes | — |
| `limit.used_points` | integer | yes | — |
| `limit.remaining_points` | integer | yes | — |
| `confirmation` | object (`Confirmation`), or null | yes | Send `token` back in `Matrix-Confirmation` with the same request (without dry_run). Null when nothing would change, or when the change would be a replay: a replay needs no confirmation, so send the request without one. |
| `confirmation.token` | string | yes | — |
| `confirmation.expires_at` | string | yes | — |
| `audit_id` | string | yes | The preview's own audit-log entry. |

**Headers on the answer:** `Request-Id`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`; and, when it applies, `Idempotency-Replayed`. [What each header means](https://matrixrewards.co.uk/docs/api/2026-10.md#headers).

### Example

```bash
export MEMBER='<member_key from GET /members>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/members/$MEMBER/adjustments" \
  -H "Authorization: Bearer $MATRIX_API_KEY" \
  -H "Idempotency-Key: goodwill-2026-09-16-0001" \
  -H "Content-Type: application/json" \
  -d '{"delta_points": 50, "reason": "Sorry about the late parcel"}'
```

The change, made:

```json
{
  "object": "adjustment",
  "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
  "delta_points": 50,
  "balance_points": 690,
  "store_key": "example-store",
  "replayed": false,
  "idempotency_key": "goodwill-2026-09-16-0001",
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000000d"
}
```

The same request with `?dry_run=true`: nothing changes:

```json
{
  "object": "adjustment_preview",
  "dry_run": true,
  "outcome": "would_apply",
  "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
  "delta_points": 50,
  "balance_points_before": 640,
  "balance_points_after": 690,
  "store_key": "example-store",
  "limit": {
    "direction": "credit",
    "limit_points": 100000,
    "used_points": 0,
    "remaining_points": 100000
  },
  "confirmation": {
    "token": "mct_EXAMPLE.EXAMPLE",
    "expires_at": "2026-09-16T10:10:00.000Z"
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000000b"
}
```

### Other answers

| Status | What it means | Error pages |
| --- | --- | --- |
| 400 | `invalid_request`: a parameter or the body could not be read; `param` names it. | [invalid_request](https://matrixrewards.co.uk/docs/errors/invalid_request.md) |
| 401 | `unauthenticated`: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer. | [unauthenticated](https://matrixrewards.co.uk/docs/errors/unauthenticated.md) |
| 403 | `forbidden_scope`: the key does not hold `write_members` (`required_scope` says so). `test_mode_key`: a test key can preview a change and never make one. | [forbidden_scope](https://matrixrewards.co.uk/docs/errors/forbidden_scope.md), [test_mode_key](https://matrixrewards.co.uk/docs/errors/test_mode_key.md) |
| 404 | `not_found`: no such member on this account, or (cancelling a code) the member holds no code with that `ref`. | [not_found](https://matrixrewards.co.uk/docs/errors/not_found.md) |
| 409 | `idempotency_key_in_use`: a request with this key is still running (`Retry-After`). `credit_limit_reached`: the account has moved 100,000 points through the API in this direction in the last 24 hours (`direction`, `limit_points`, `used_points`). `member_has_no_store`: the member is linked to no store to book the change to. | [idempotency_key_in_use](https://matrixrewards.co.uk/docs/errors/idempotency_key_in_use.md), [credit_limit_reached](https://matrixrewards.co.uk/docs/errors/credit_limit_reached.md), [member_has_no_store](https://matrixrewards.co.uk/docs/errors/member_has_no_store.md) |
| 413 | `payload_too_large`: the body is over 4,096 bytes. | [payload_too_large](https://matrixrewards.co.uk/docs/errors/payload_too_large.md) |
| 422 | `idempotency_key_reused`: this Idempotency-Key was used for a different request in the last 24 hours. | [idempotency_key_reused](https://matrixrewards.co.uk/docs/errors/idempotency_key_reused.md) |
| 428 | `confirmation_required`: this key is tagged for an assistant; preview with `dry_run=true` and send the returned confirmation. `confirmation_invalid`: the confirmation is not for this request, has expired, or is not one we issued (`reason`). | [confirmation_required](https://matrixrewards.co.uk/docs/errors/confirmation_required.md), [confirmation_invalid](https://matrixrewards.co.uk/docs/errors/confirmation_invalid.md) |
| 429 | `rate_limited`: too many requests for this key; `bucket` says which allowance. `Retry-After` says how long to wait. | [rate_limited](https://matrixrewards.co.uk/docs/errors/rate_limited.md) |
| 503 | `change_not_recorded` (or `access_not_recorded` for a preview or a replay): the audit log could not be written, so nothing was changed or returned. Retry with the same Idempotency-Key. | [change_not_recorded](https://matrixrewards.co.uk/docs/errors/change_not_recorded.md), [access_not_recorded](https://matrixrewards.co.uk/docs/errors/access_not_recorded.md) |

## Cancel a reward code and give the member back its points {#cancelMemberCode}

`POST /members/{member_key}/codes/void` — the full address is `https://app.matrixrewards.co.uk/api/2026-10/members/{member_key}/codes/void`.

**Needs:** the `write_members` permission (in the app: “Change members’ points and cancel reward codes. This also reads members, their email addresses and the last four characters of their reward codes. Every change is logged”).

Switches the code off at the store it belongs to and credits its cost back — never one without the other. Name the code by its `ref` from `listMemberCodes`; the code itself is never sent or returned. The attempt is written to the audit log before Shopify is asked. A preview (`dry_run=true`) never contacts Shopify, so a store problem shows only on the real call. A cancel is not counted against the daily points limits on `adjustMemberPoints`: it gives back points the shopper already spent on the code.

### Parameters

| Name | In | Required | Type | What it is |
| --- | --- | --- | --- | --- |
| `member_key` | path | yes | string | — |
| `Idempotency-Key` | header | yes | string, matching `^[A-Za-z0-9._:-]{1,100}$` | Required on both writes, a points change and a cancel. Pick a new one for each new change and send the same one to retry it. For 24 hours from its first use, a key has three answers: the same request again gets the first answer back, byte for byte, with `Idempotency-Replayed: true`; a different request under the key is 422 `idempotency_key_reused`; and while the first request is still running, 409 `idempotency_key_in_use` with `Retry-After`. A 5xx does not use the key up: retry with the same one. For a points change the key is also the adjustment's id in the ledger, so the same change is never booked twice, whichever of the account's keys sends it, and even after the 24 hours. |
| `dry_run` | query | no | boolean | `true` previews the change: every check the real call makes runs, the answer has the status the real call would have, and nothing changes — except one `dry_run` entry in the audit log, because a preview shows the member's balance. When the change would happen, the preview carries a `confirmation` for exactly this request. It is the only query parameter a write takes: any other, a misspelling such as `dryRun` included, is refused with 400 `invalid_request` naming it in `param`, and nothing changes. |
| `Matrix-Confirmation` | header | no | string, up to 1,024 characters | The `confirmation.token` a dry run of this exact request returned. Required for keys tagged for an assistant, except on a replay: a request whose first answer is still kept, or a points change that is already made; checked whenever it is sent. It covers one request — the same key, member, body and Idempotency-Key — for 10 minutes. It proves the change was previewed first; it cannot prove a person saw the preview. |

### Request body

Sent as `application/json`.

| Field | Type | Required | What it is |
| --- | --- | --- | --- |
| `ref` | string, matching `^[0-9a-f]{20}$` | yes | The code's `ref`, from `listMemberCodes`. The code itself is never sent: a write request's body passes through logs and assistant transcripts, and a live code is spendable. |

### The answer

The change, or with `dry_run=true` the preview of it.

**One shape: `VoidResult`.**

| Field | Type | Always there | What it is |
| --- | --- | --- | --- |
| `object` | always `code_void` | yes | — |
| `result` | one of `voided`, `already_voided`, `nothing_to_void` | yes | `voided`: switched off at Shopify and the points given back. `already_voided`: done before, not again. `nothing_to_void`: the code never held points to give back. |
| `member_key` | string | yes | — |
| `code_ending` | string, or null | yes | The last four characters of the code, or null for a code too short to show any of (see CodePage). The code itself is never returned. |
| `store_key` | string, or null | yes | — |
| `balance_points` | integer | yes | — |
| `audit_id` | string | yes | The audit-log entry written before Shopify was asked. |

**One shape: `VoidPreview`.**

| Field | Type | Always there | What it is |
| --- | --- | --- | --- |
| `object` | always `code_void_preview` | yes | — |
| `dry_run` | always `true` | yes | — |
| `outcome` | one of `would_void`, `already_voided`, `nothing_to_void`, `would_replay` | yes | — |
| `member_key` | string | yes | — |
| `code_ending` | string, or null | yes | — |
| `store_key` | string, or null | yes | — |
| `cost_points` | integer, or null | yes | What the code cost. Null when the answer would be a replay of a stored one. |
| `balance_points_before` | integer | yes | — |
| `balance_points_after` | integer | yes | — |
| `confirmation` | object (`Confirmation`), or null | yes | Send `token` back in `Matrix-Confirmation` with the same request (without dry_run). Null when nothing would change, or when the change would be a replay: a replay needs no confirmation, so send the request without one. |
| `confirmation.token` | string | yes | — |
| `confirmation.expires_at` | string | yes | — |
| `audit_id` | string | yes | — |

**Headers on the answer:** `Request-Id`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`; and, when it applies, `Idempotency-Replayed`. [What each header means](https://matrixrewards.co.uk/docs/api/2026-10.md#headers).

### Example

```bash
export MEMBER='<member_key from GET /members>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/members/$MEMBER/codes/void" \
  -H "Authorization: Bearer $MATRIX_API_KEY" \
  -H "Idempotency-Key: cancel-2026-09-16-0001" \
  -H "Content-Type: application/json" \
  -d '{"ref": "5f0c2d8e91a47b36c0de"}'
```

The change, made:

```json
{
  "object": "code_void",
  "result": "voided",
  "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
  "code_ending": "4TPB",
  "store_key": "example-store",
  "balance_points": 1190,
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000011"
}
```

The same request with `?dry_run=true`: nothing changes:

```json
{
  "object": "code_void_preview",
  "dry_run": true,
  "outcome": "would_void",
  "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
  "code_ending": "4TPB",
  "store_key": "example-store",
  "cost_points": 500,
  "balance_points_before": 690,
  "balance_points_after": 1190,
  "confirmation": {
    "token": "mct_EXAMPLE.EXAMPLE",
    "expires_at": "2026-09-16T10:10:00.000Z"
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000000f"
}
```

### Other answers

| Status | What it means | Error pages |
| --- | --- | --- |
| 400 | `invalid_request`: a parameter or the body could not be read; `param` names it. | [invalid_request](https://matrixrewards.co.uk/docs/errors/invalid_request.md) |
| 401 | `unauthenticated`: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer. | [unauthenticated](https://matrixrewards.co.uk/docs/errors/unauthenticated.md) |
| 403 | `forbidden_scope`: the key does not hold `write_members` (`required_scope` says so). `test_mode_key`: a test key can preview a change and never make one. | [forbidden_scope](https://matrixrewards.co.uk/docs/errors/forbidden_scope.md), [test_mode_key](https://matrixrewards.co.uk/docs/errors/test_mode_key.md) |
| 404 | `not_found`: no such member on this account, or (cancelling a code) the member holds no code with that `ref`. | [not_found](https://matrixrewards.co.uk/docs/errors/not_found.md) |
| 409 | `idempotency_key_in_use`: a request with this key is still running (`Retry-After`). `code_already_used`: the code was spent at checkout. | [idempotency_key_in_use](https://matrixrewards.co.uk/docs/errors/idempotency_key_in_use.md), [code_already_used](https://matrixrewards.co.uk/docs/errors/code_already_used.md) |
| 413 | `payload_too_large`: the body is over 4,096 bytes. | [payload_too_large](https://matrixrewards.co.uk/docs/errors/payload_too_large.md) |
| 422 | `idempotency_key_reused`: this Idempotency-Key was used for a different request in the last 24 hours. | [idempotency_key_reused](https://matrixrewards.co.uk/docs/errors/idempotency_key_reused.md) |
| 428 | `confirmation_required`: this key is tagged for an assistant; preview with `dry_run=true` and send the returned confirmation. `confirmation_invalid`: the confirmation is not for this request, has expired, or is not one we issued (`reason`). | [confirmation_required](https://matrixrewards.co.uk/docs/errors/confirmation_required.md), [confirmation_invalid](https://matrixrewards.co.uk/docs/errors/confirmation_invalid.md) |
| 429 | `rate_limited`: too many requests for this key; `bucket` says which allowance. `Retry-After` says how long to wait. | [rate_limited](https://matrixrewards.co.uk/docs/errors/rate_limited.md) |
| 502 | `upstream_failed`: Shopify did not confirm the code was switched off. Nothing was credited and the code is still live; retry with the same Idempotency-Key. | [upstream_failed](https://matrixrewards.co.uk/docs/errors/upstream_failed.md) |
| 503 | `change_not_recorded` / `access_not_recorded`: as for a points change. `store_not_connected`: the code's store is not connected, so it cannot be switched off; nothing was changed. | [change_not_recorded](https://matrixrewards.co.uk/docs/errors/change_not_recorded.md), [access_not_recorded](https://matrixrewards.co.uk/docs/errors/access_not_recorded.md), [store_not_connected](https://matrixrewards.co.uk/docs/errors/store_not_connected.md) |

---

Page: https://matrixrewards.co.uk/docs/api/2026-10/members
Contract: 2026-10 sha256:35af153960028890fea5738799f5a71e535e1eea4dd3f6b8d5f50b37f02363da
