# Safe retries and previews

Send the same change twice and it is made once. Ask for a preview and nothing changes.

Both apply to the two calls that change a member: [`POST /members/{member_key}/adjustments`](https://matrixrewards.co.uk/docs/api/2026-10/members.md#adjustMemberPoints) and [`POST /members/{member_key}/codes/void`](https://matrixrewards.co.uk/docs/api/2026-10/members.md#cancelMemberCode).

## Idempotency-Key {#idempotency-key}

Every points change and every cancel carries an `Idempotency-Key` header: a name you choose for that one change, of up to 100 letters, digits, dots, underscores, colons and hyphens. The reference describes it like this:

> 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.

In short:

- Use a new key for each new change.
- To retry a change, send exactly the same request with the same key.
- **Never use a new key to retry a change you are unsure about.** A new key is a new change: if the first one was made, you have now made it twice.

The three answers a key can have, besides the first one:

- the same request again: the first answer, with `Idempotency-Replayed: true`;
- a different request with a key already used: [idempotency_key_reused](https://matrixrewards.co.uk/docs/errors/idempotency_key_reused.md);
- the same request while the first is still running: [idempotency_key_in_use](https://matrixrewards.co.uk/docs/errors/idempotency_key_in_use.md).

Creating a webhook endpoint takes no `Idempotency-Key`. [`POST /webhooks`](https://matrixrewards.co.uk/docs/api/2026-10/webhooks.md#createWebhookEndpoint) says why, and what to do after an answer that went missing.

## Preview a change {#preview}

Add `?dry_run=true` to either call. The reference says of `dry_run`:

> `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.

A points change's preview says in `outcome` what the real call would do: `would_apply` or `would_replay`. A cancel's preview says `would_void`, `already_voided`, `nothing_to_void` or `would_replay`. And of the preview's `confirmation`:

> 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.

## Test keys

A test key walks the whole flow for real: its preview is a real preview and it can send the confirmation back, but the change itself is refused with [test_mode_key](https://matrixrewards.co.uk/docs/errors/test_mode_key.md). Nothing changes.

## Keys for AI assistants {#assistant-keys}

A key you marked as being for an AI assistant cannot make a change in one call. It has to send the `Matrix-Confirmation` header, which the reference describes like this:

> 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.

So a change takes four steps:

1. Choose a new `Idempotency-Key` and keep it.
2. Send the change with `?dry_run=true`.
3. Show the person the preview, and wait for a clear yes.
4. Send the identical request, without `dry_run`, with the preview's `confirmation.token` in the `Matrix-Confirmation` header, within 10 minutes.

Both steps are in your activity log. What the rule proves is that a change was previewed first. It cannot prove a person saw the preview: that is the assistant's job. And it does not protect you from a stolen key, because a thief can preview and confirm too. Against that, a key has its permissions, its limits, the log, and revoking.

## A worked example

Set the member's key first, in place of the words in angle brackets, then preview the change. Choose a new `Idempotency-Key` for each change you make; the one here is an example.

```bash
export MEMBER='<member_key from GET /members>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/members/$MEMBER/adjustments?dry_run=true" \
  -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 preview:

```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"
}
```

Then make the change: the same request, without `dry_run`, with the preview's `confirmation.token` in `Matrix-Confirmation`. Only a key for an assistant has to send it, but any key may, and it is then checked.

```bash
export CONFIRMATION='<confirmation.token from the preview>'
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 "Matrix-Confirmation: $CONFIRMATION" \
  -H "Content-Type: application/json" \
  -d '{"delta_points": 50, "reason": "Sorry about the late parcel"}'
```

The answer:

```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"
}
```

Send that second request again and you get the same answer back, with `Idempotency-Replayed: true`, and the points move once.

---

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