Skip to content
MatrixRewards
API docs menu

Stores

The stores on the account and their programmes.

Part of the API reference, version 2026-10. Every address below starts with the base address:

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

Every store on this account

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

Needs: the read_programme permission (in the app: “Your programme settings — rates, rewards, rules”). A key with write_programme can call it too.

The answer

The account's stores.

FieldTypeAlways thereWhat it is
datalist of object (Store)yes—
data[].keystringyes—
data[].namestringyes—
data[].platformstringyes'shopify' today; the engine ingests events, so another platform is another producer.
data[].external_refstring, or nullyesThat platform's own identifier — a myshopify domain, for instance.
data[].created_atstringyes—
data[].display_namestringyesThe store's name as every page shows it: the design's storeName, else the name Shopify has for the shop now, else the name it was installed with. name stays the name it was installed with.
has_morebooleanyes—
next_cursorstring, or nullyes—

Headers on the answer: Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. What each header means.

Example

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

The answer:

{
  "data": [
    {
      "key": "example-outlet",
      "name": "Example Outlet",
      "platform": "shopify",
      "external_ref": "example-outlet.myshopify.com",
      "created_at": "2026-08-20T14:30:00Z",
      "display_name": "Example Outlet"
    },
    {
      "key": "example-store",
      "name": "Example Store",
      "platform": "shopify",
      "external_ref": "example-store.myshopify.com",
      "created_at": "2026-08-03T09:12:00Z",
      "display_name": "Example Store"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Other answers

StatusWhat it meansError pages
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not carry the scope this call needs; required_scope says which.forbidden_scope
429rate_limited: too many requests for this key; bucket says which allowance. Retry-After says how long to wait.rate_limited

One store

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

Needs: the read_programme permission (in the app: “Your programme settings — rates, rewards, rules”). A key with write_programme can call it too.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—

The answer

The store.

FieldTypeAlways thereWhat it is
keystringyes—
namestringyes—
platformstringyes'shopify' today; the engine ingests events, so another platform is another producer.
external_refstring, or nullyesThat platform's own identifier — a myshopify domain, for instance.
created_atstringyes—
display_namestringyesThe store's name as every page shows it: the design's storeName, else the name Shopify has for the shop now, else the name it was installed with. name stays the name it was installed with.

Headers on the answer: Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. What each header means.

Example

export STORE='<key of a store from GET /stores>'
curl "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE" \
  -H "Authorization: Bearer $MATRIX_API_KEY"

The answer:

{
  "key": "example-store",
  "name": "Example Store",
  "platform": "shopify",
  "external_ref": "example-store.myshopify.com",
  "created_at": "2026-08-03T09:12:00Z",
  "display_name": "Example Store"
}

Other answers

StatusWhat it meansError pages
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not carry the scope this call needs; required_scope says which.forbidden_scope
404not_found: no such resource on this account — indistinguishable from one that does not exist at all.not_found
429rate_limited: too many requests for this key; bucket says which allowance. Retry-After says how long to wait.rate_limited

A store's programme: what is stored, the settings in force, what shoppers are actually paid, and the expiry rule

GET /stores/{store_key}/programme — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme.

Needs: the read_programme permission (in the app: “Your programme settings — rates, rewards, rules”). A key with write_programme can call it too.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—

The answer

The programme.

FieldTypeAlways thereWhat it is
store_keystringyes—
configobject, openyesWhat the merchant has stored. Empty until they configure it.
effectiveobject, or nullyesThe settings in force: what is stored, over built-in defaults, in the engine's own field names. These are SETTINGS, not what shoppers are paid, and three of them can disagree with what they are paid. basePointsPerMajorUnit is a stored setting nothing earns by: orders are paid by the always-on perMajorUnit rules in rules. welcomeBonusPoints is a stored setting nothing pays: the joining bonus is the welcome_bonus rule, and a store with no such rule pays 0. waysToEarn is as stored, and a sentence in it that states the rate, an app-order bonus or the joining bonus may have been written for rules the store no longer has. Do not quote any of the three to shoppers: quote as_paid. bonusesInAppOnly is always present, and false unless set: true means the store's welcome and birthday bonuses are paid only on its own app sign-in, never on a web page. Only Matrix Rewards sets it. spendAnyAmount is always present: whether a shopper may choose how many points to spend, at redeemPointsPerMajorUnit, and the limits on one code. Change it with spend_any_amount (POST /stores/{store_key}/programme). While it is off, the limits shown are the ones kept for when it is switched on again; a minPoints never set is the points worth one unit of the store's currency at redeemPointsPerMajorUnit.
effective.spendAnyAmountobjectyesSpend any amount, in the engine's own names. Off (enabled false) at every store until it is switched on.
effective.spendAnyAmount.enabledbooleanyesWhether a shopper may choose how many points to spend.
effective.spendAnyAmount.minPointsinteger, at least 1yesThe fewest points one code may take, before it is rounded up to a whole step.
effective.spendAnyAmount.maxPointsinteger, at least 1, or nullyesThe most points one code may take, before it is rounded down to a whole step; null for no limit of the store's own (one spend never takes more than 10,000,000 points).
effective.spendAnyAmount.minSubtotalstringyesThe smallest order a code can be used on, in the store's currency; "0.00" for any order.
as_paidobject, or nullyesWhat a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.
as_paid.earn_rate_per_major_unitnumber, at least 0yesPoints an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than "earn 0".
as_paid.earn_rates_per_major_unitlist of number, at least 0yesThe same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays.
as_paid.welcome_bonus_pointsinteger, at least 0yesThe joining bonus a new shopper is paid, once. 0 when the store pays none, whatever effective.welcomeBonusPoints says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's effective.bonusesInAppOnly), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in ways_to_earn says so.
as_paid.ways_to_earnlist of objectyeseffective.waysToEarn as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from earn_rate_per_major_unit, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.
as_paid.ways_to_earn[].idstringyes—
as_paid.ways_to_earn[].labelstringyes—
as_paid.ways_to_earn[].detailstringyes—
appearanceobject, openyesResolved colours, wording and toggles for the shopper-facing surfaces.
expiryobject, or nullyes—
expiry.period_monthsintegeryesAt least 12 — the platform's floor.
expiry.effective_fromstringyes—
versionstring, matching ^[0-9a-f]{32}$yesWhich stored programme this is: it changes whenever the store's programme settings change (not its look or expiry), so two reads with the same version saw the same programme. Send it as if_version with a programme change (POST /stores/{store_key}/programme) and the change is refused, 409 settings_changed, if the programme has changed since you read it. The embedded admin's save checks the same version.

An open object has no fixed set of fields: it is sent as the platform holds it, in its own field names.

Headers on the answer: Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. What each header means.

Example

export STORE='<key of a store from GET /stores>'
curl "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme" \
  -H "Authorization: Bearer $MATRIX_API_KEY"

The answer:

{
  "store_key": "example-store",
  "config": {
    "rules": [
      {
        "id": "base_earn",
        "award": {
          "type": "perMajorUnit",
          "pointsPerUnit": 1
        },
        "event": "order_paid",
        "reason": "Points earned on your order",
        "condition": {
          "type": "always"
        }
      }
    ],
    "rewards": [
      {
        "id": "reward_5",
        "costPoints": 500,
        "valueLabel": "5.00 GBP off",
        "minSubtotal": "5.00",
        "valueAmount": "5.00"
      }
    ],
    "currency": "GBP",
    "timezone": "Europe/London",
    "waysToEarn": [
      {
        "id": "orders",
        "label": "Shop online",
        "detail": "Earn 1 point for every £1 you spend"
      }
    ],
    "appChannelValue": "",
    "loyaltyStartDate": "2026-08-03",
    "appOrderMultiplier": 1,
    "welcomeBonusPoints": 0,
    "appChannelAttribute": "_channel",
    "basePointsPerMajorUnit": 1,
    "redeemPointsPerMajorUnit": 100
  },
  "effective": {
    "currency": "GBP",
    "timezone": "Europe/London",
    "basePointsPerMajorUnit": 1,
    "appOrderMultiplier": 1,
    "welcomeBonusPoints": 0,
    "redeemPointsPerMajorUnit": 100,
    "appChannelAttribute": "_channel",
    "appChannelValue": "",
    "loyaltyStartDate": "2026-08-03",
    "rewards": [
      {
        "id": "reward_5",
        "costPoints": 500,
        "valueLabel": "5.00 GBP off",
        "minSubtotal": "5.00",
        "valueAmount": "5.00"
      }
    ],
    "rules": [
      {
        "id": "base_earn",
        "award": {
          "type": "perMajorUnit",
          "pointsPerUnit": 1
        },
        "event": "order_paid",
        "reason": "Points earned on your order",
        "condition": {
          "type": "always"
        }
      }
    ],
    "waysToEarn": [
      {
        "id": "orders",
        "label": "Shop online",
        "detail": "Earn 1 point for every £1 you spend"
      }
    ],
    "referral": {
      "enabled": true,
      "friendPoints": 300,
      "advocatePoints": 200,
      "qualifyingMinAmount": "15.00",
      "claimExpiryDays": 30,
      "advocateCapFriends": 10
    },
    "bonusesInAppOnly": false,
    "spendAnyAmount": {
      "enabled": false,
      "minPoints": 100,
      "maxPoints": null,
      "minSubtotal": "0.00"
    }
  },
  "as_paid": {
    "earn_rate_per_major_unit": 1,
    "earn_rates_per_major_unit": [
      1
    ],
    "welcome_bonus_points": 0,
    "ways_to_earn": [
      {
        "id": "orders",
        "label": "Shop online",
        "detail": "Earn 1 point for every £1 you spend"
      }
    ]
  },
  "appearance": {
    "programmeName": "Rewards",
    "pointsName": "point",
    "pointsNamePlural": "points",
    "heading": "",
    "storeName": "",
    "signedOutPrompt": "Sign in to see your points.",
    "rewardsHeading": "What you can get",
    "earnHeading": "Ways to earn",
    "activityHeading": "Recent activity",
    "accent": null,
    "cornerRadius": null,
    "density": "comfortable",
    "textScale": "normal",
    "fontFamily": "theme",
    "showRewards": true,
    "showWaysToEarn": true,
    "showActivity": true,
    "showExpiry": true,
    "showSharedWallet": true,
    "showProductPoints": true,
    "showRewardsSite": false,
    "termsUrl": null,
    "otherShops": {}
  },
  "expiry": null,
  "version": "3f1c9a2b7d4e5f60718293a4b5c6d7e8"
}

Other answers

StatusWhat it meansError pages
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not carry the scope this call needs; required_scope says which.forbidden_scope
404not_found: no such resource on this account — indistinguishable from one that does not exist at all.not_found
429rate_limited: too many requests for this key; bucket says which allowance. Retry-After says how long to wait.rate_limited

Change the earn rate, the bonuses, what a point is worth, referrals, spending any amount, the time zone, the ways to earn, or when points expire

POST /stores/{store_key}/programme — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme.

Needs: the write_programme permission (in the app: “Change your programme: the earn rate, the joining and birthday bonuses, what a point is worth, when points expire, the rewards and the offers. Every change is previewable and logged”).

Plain fields, not rules: the rules that pay them are built by the same code the Programme screen in your Shopify admin runs, and every other rule (offers, rewards, anything set up by hand) is carried exactly as it is. The change is built over one reading of the programme, so a change made elsewhere in between is refused (409 settings_changed), never undone. A preview (dry_run=true) makes the change in a transaction, reads the programme back and rolls it back: it shows what three example orders earn before and after, every figure the change moves before and after in this call's own field names (before_after) and, for a point-value change, what the points shoppers hold are worth. At most 100 changes per account through the API in any 24 hours. Spending any amount (spend_any_amount) lets a shopper choose how many points to spend, for a code worth exactly that at the store's point value; it is off at every store until it is switched on, and the preview shows its terms before and after. The currency cannot be changed here. Written to the audit log in the same transaction as the change: if the entry cannot be written, the change is not made.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—
Idempotency-Keyheaderyesstring, 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_runquerynobooleantrue 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-Confirmationheadernostring, up to 1,024 charactersThe 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.

FieldTypeRequiredWhat it is
points_per_currency_unitnumber, from 0 to 1,000noThe everyday earn rate: points for every 1 of the store's currency an order pays, with at most 2 decimal places. 0 stops it.
welcome_bonus_pointsinteger, from 0 to 100,000noPoints a shopper gets for joining. 0 stops the bonus.
birthday_bonus_pointsinteger, from 0 to 100,000noPoints a shopper gets on their birthday. 0 stops the bonus.
point_valueobjectnoWhat a point is worth: points points are worth amount of the store's currency. points divided by amount must be a whole number.
point_value.pointsinteger, from 1 to 100,000yes—
point_value.amountstring, matching ^\d{1,7}(?:\.\d{1,2})?$yes—
referralobjectnoWhat a referral pays at this store, and whether it runs. Only the terms sent change: { "enabled": false } stops referrals and keeps the numbers. A friend who has already accepted an invitation is paid on the terms they were shown.
referral.enabledbooleannotrue runs referrals at this store; false stops them.
referral.friend_pointsinteger, from 1 to 1,000,000noWhat the invited friend is paid, in points.
referral.advocate_pointsinteger, from 1 to 1,000,000noWhat the shopper who invited them is paid, in points.
referral.qualifying_min_amountstring, matching ^\d{1,9}(?:\.\d{1,3})?$noThe smallest first order that pays the referral, in the store's currency; more than zero.
referral.claim_expiry_daysinteger, from 14 to 365noDays the friend has to place that order.
referral.advocate_cap_friendsinteger, from 1 to 100noThe most friends one shopper is paid for in any rolling window (referral_advocate_cap_window_days).
spend_any_amountobjectnoWhether a shopper may choose how many points to spend, and get a discount code worth exactly that at the store's point value (point_value: there is no second rate), and the limits on one code. Off at every store until it is switched on, here or in the app. Only the terms sent change: { "enabled": false } switches it off and keeps the limits for when it is switched on again. A shopper can spend only whole steps, a number of points worth a whole number of the currency's smallest unit, so the fewest points are rounded up to a whole step and the most down, and there must be something between them: otherwise the change is refused, invalid_request with problem below_minimum on spend_any_amount.max_points, and so is a point_value change that would leave nothing between them. A code comes off the items of an order, never delivery, and anything over their total is not kept. Codes already issued keep what they were issued for, and the store's rewards are unchanged beside it. An assistant's key may switch it off or narrow it, never switch it on.
spend_any_amount.enabledbooleannotrue lets shoppers choose how many points to spend; false stops it. Off until it is switched on.
spend_any_amount.min_pointsinteger, from 1 to 10,000,000noThe fewest points one code may take, rounded up to a whole step. Until it is set, the points worth one whole unit of the store's currency.
spend_any_amount.max_pointsinteger, from 1 to 10,000,000, or nullnoThe most points one code may take, rounded down to a whole step; null (the default) for no limit of the store's own (one spend never takes more than 10,000,000 points).
spend_any_amount.min_subtotalstring, matching ^\d{1,9}(?:\.\d{1,3})?$noThe smallest order a code can be used on, in the store's currency and no finer than its smallest unit (two decimal places for pounds); "0.00" (the default) for any order.
timezonestringnoThe store's time zone, by its IANA name, like "Europe/London" or "America/New_York": the one its days are reckoned in (a shopper's birthday, a campaign's first and last day). Set from the shop when the app is installed. An unknown name is refused (invalid_request on timezone); the zone already set changes nothing. Campaigns already set keep their exact start and end times, and their days are then read in the new zone.
ways_to_earnlist of objectnoThe lines a shopper reads under "Ways to earn", in order, at most 50 (ways_to_earn_max), replacing the store's list whole; [] for none of its own. Each id once. The platform still adds a "Join" line for a joining bonus and a "Birthday" line for a birthday bonus when the list has no entry with the id join or birthday; a list with an id starting earn_ is read as one of the platform's own templates, and gets neither. A line with the id orders saying what every order earns in the platform's own words is rewritten whenever the rate moves. The preview shows the list before and after as a shopper will read it (before_after.ways_to_earn). The same list as now changes nothing.
ways_to_earn[].idstring, up to 64 characters, at least 1 characteryes—
ways_to_earn[].labelstring, up to 120 characters, at least 1 characteryesThe line's heading, like "Shop online".
ways_to_earn[].detailstring, up to 500 characters, at least 1 characteryesWhat it pays, in words a shopper reads. Say only what the programme pays.
expiryobjectnoPoints expire this many months after a shopper's last activity, from this instant. It can never reach back before what shoppers were told (retroactive_change).
expiry.period_monthsinteger, from 12 to 1,200yes—
expiry.effective_fromstring, matching ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$yesA UTC instant on a date that exists, in the year 9898 or earlier (expiry_max_effective_year): a later start would put shoppers' expiry dates past four-digit years.
if_versionstring, matching ^[0-9a-f]{32}$noThe version from a programme read. If the programme has changed since, nothing is changed (409 settings_changed).

The answer

The change, or with dry_run=true the preview of it. A request already answered is answered again, byte for byte (Idempotency-Replayed).

One shape: ProgrammeChange.

FieldTypeAlways thereWhat it is
objectalways programme_changeyes—
store_keystringyes—
versionstring, matching ^[0-9a-f]{32}$yesThe programme's version after the change.
changedlist of stringyesThe settings that changed, in the engine's names. Empty when the change asked for what was already set.
as_paidobject, or nullyesWhat a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.
as_paid.earn_rate_per_major_unitnumber, at least 0yesPoints an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than "earn 0".
as_paid.earn_rates_per_major_unitlist of number, at least 0yesThe same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays.
as_paid.welcome_bonus_pointsinteger, at least 0yesThe joining bonus a new shopper is paid, once. 0 when the store pays none, whatever effective.welcomeBonusPoints says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's effective.bonusesInAppOnly), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in ways_to_earn says so.
as_paid.ways_to_earnlist of objectyeseffective.waysToEarn as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from earn_rate_per_major_unit, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.
as_paid.ways_to_earn[].idstringyes—
as_paid.ways_to_earn[].labelstringyes—
as_paid.ways_to_earn[].detailstringyes—
audit_idstring, or nullyes—

One shape: ProgrammeChangePreview.

FieldTypeAlways thereWhat it is
objectalways programme_change_previewyes—
dry_runalways trueyes—
outcomeone of would_apply, no_changeyes—
store_keystringyes—
changedlist of stringyesThe settings that would change, in the engine's names. What each moves from and to is before_after.
before_afterobject (PreviewBeforeAfter)yesEvery figure the change moves, before and after, in this API's own write field names, worked out from the programme as it would price orders before and after (the same way the storefront works them out). Only the ones that differ are here, so {} means none moves. points_per_currency_unit is the everyday rate an order pays (a running campaign or other offer left out); welcome_bonus_points and birthday_bonus_points are 0 when none is paid; point_value is how many points are worth one unit of the currency; ways_to_earn is the list as a shopper reads it. On a reward's call, reward is the reward itself: null before an add, null after a removal.
before_after.points_per_currency_unitobjectno—
before_after.points_per_currency_unit.beforenumber, at least 0yes—
before_after.points_per_currency_unit.afternumber, at least 0yes—
before_after.welcome_bonus_pointsobjectno—
before_after.welcome_bonus_points.beforeinteger, at least 0yes—
before_after.welcome_bonus_points.afterinteger, at least 0yes—
before_after.birthday_bonus_pointsobjectno—
before_after.birthday_bonus_points.beforeinteger, at least 0yes—
before_after.birthday_bonus_points.afterinteger, at least 0yes—
before_after.point_valueobjectno—
before_after.point_value.beforeobjectyes—
before_after.point_value.before.pointsinteger, at least 1yes—
before_after.point_value.before.amountalways 1.00yes—
before_after.point_value.afterobjectyes—
before_after.point_value.after.pointsinteger, at least 1yes—
before_after.point_value.after.amountalways 1.00yes—
before_after.timezoneobjectno—
before_after.timezone.beforestringyes—
before_after.timezone.afterstringyes—
before_after.ways_to_earnobjectno—
before_after.ways_to_earn.beforelist of objectyes—
before_after.ways_to_earn.before[].idstringyes—
before_after.ways_to_earn.before[].labelstringyes—
before_after.ways_to_earn.before[].detailstringyes—
before_after.ways_to_earn.afterlist of objectyes—
before_after.ways_to_earn.after[].idstringyes—
before_after.ways_to_earn.after[].labelstringyes—
before_after.ways_to_earn.after[].detailstringyes—
before_after.referralobjectno—
before_after.referral.beforeany, or null or objectyes—
before_after.referral.afterany, or null or objectyes—
before_after.spend_any_amountobjectno—
before_after.spend_any_amount.beforeany, or null or objectyes—
before_after.spend_any_amount.afterany, or null or objectyes—
before_after.rewardobjectno—
before_after.reward.beforeany, or null or object (ApiReward)yes—
before_after.reward.afterany, or null or object (ApiReward)yes—
currencystring, or nullyes—
exampleslist of objectyesWhat a plain order of each size earns now, before and after the change, under every offer running now.
examples[].order_amountstringyes—
examples[].points_beforeintegeryes—
examples[].points_afterintegeryes—
point_valueobjectyes—
point_value.points_per_currency_unit_beforeinteger, or nullyes—
point_value.points_per_currency_unit_afterinteger, or nullyes—
liabilityany, or null or object or objectyesWhen the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; unavailable when the account has more than one store.
version_afterstring, matching ^[0-9a-f]{32}$, or nullyesThe version the programme would have.
confirmationany, or null or objectyesFor a key tagged for an assistant: send token back as the Matrix-Confirmation header with the same request. Null when nothing would change.
audit_idstring, or nullyes—
referralany, or null or objectyesWhen the change sends referral: the store's referral terms before and after. Null otherwise.
spend_any_amountany, or null or objectyesWhen the change sends spend_any_amount: its terms before and after, as stored, so the limits kept while it is off are shown. Null otherwise.

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.

Example

export STORE='<key of a store from GET /stores>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme" \
  -H "Authorization: Bearer $MATRIX_API_KEY" \
  -H "Idempotency-Key: programme-2026-09-16-0001" \
  -H "Content-Type: application/json" \
  -d '{"points_per_currency_unit": 2}'

The change, made:

{
  "object": "programme_change",
  "store_key": "example-store",
  "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f0",
  "changed": [
    "basePointsPerMajorUnit",
    "rules"
  ],
  "as_paid": {
    "earn_rate_per_major_unit": 2,
    "earn_rates_per_major_unit": [
      2
    ],
    "welcome_bonus_points": 0,
    "ways_to_earn": []
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000000e"
}

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

{
  "object": "programme_change_preview",
  "dry_run": true,
  "outcome": "would_apply",
  "store_key": "example-store",
  "changed": [
    "basePointsPerMajorUnit",
    "rules"
  ],
  "before_after": {
    "points_per_currency_unit": {
      "before": 1,
      "after": 2
    }
  },
  "currency": "GBP",
  "examples": [
    {
      "order_amount": "10.00",
      "points_before": 10,
      "points_after": 20
    },
    {
      "order_amount": "50.00",
      "points_before": 50,
      "points_after": 100
    },
    {
      "order_amount": "100.00",
      "points_before": 100,
      "points_after": 200
    }
  ],
  "point_value": {
    "points_per_currency_unit_before": 100,
    "points_per_currency_unit_after": 100
  },
  "liability": null,
  "referral": null,
  "spend_any_amount": null,
  "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f0",
  "confirmation": {
    "token": "mct_EXAMPLE.EXAMPLE",
    "expires_at": "2026-09-16T10:10:00.000Z"
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000000f"
}

Other answers

StatusWhat it meansError pages
400invalid_request: a parameter or the body could not be read; param names it.invalid_request
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not hold write_programme (required_scope says so). test_mode_key: a test key can preview a change and never make one. assistant_limit: the key is an assistant's (every Claude connection, and a key made for an assistant), the merchant has not lifted its fixed limits (/me key.limits_lifted; only a person can lift them, on the app's API keys screen), and the change moves the store's money further than one step. It is measured from the programme as it stood before the assistant's changes began, so steps add up; that starting point holds until assistant_window_hours pass with no assistant change, and a change by the owner to what the programme pays starts afresh (a new label or reason does not; nor does a change by an assistant key whose limits are lifted). What a point is worth, the currency and when points expire never change. What an order earns at most doubles: a new offer counts as paying every order, against what every order earned; and the everyday rate, which still prices some refunds (an API refund that names no order, and an order paid before amounts were recorded), is not lowered to fund an offer (then the figures are the everyday rate's own, least_allowed is the least it may be for every order to earn what it did, up to the field's two decimal places — null when no rate within its own step would do — and on an offer's route no param is named). Every other figure that pays shoppers at most doubles, one at zero stays there, and referrals that are off stay off. Spending any amount that is off stays off; while it is on, the fewest points one code may take and the smallest order it can be used on at most halve, and the most points at most double (a limit cannot be removed); switching it off is never limited. An offer may only narrow who and when it pays. A reward at most doubles what it gives for each point, keeps its kind, and its smallest order at most halves; a new reward stays within one step of a reward of its kind. The preview is refused the same way. problem says which limit; before, asked and most_allowed or least_allowed give the figure in the unit of param, and sending that limit back is accepted.forbidden_scope, test_mode_key, assistant_limit
404not_found: no such member on this account, or (cancelling a code) the member holds no code with that ref.not_found
409idempotency_key_in_use: a request with this key is still running (Retry-After). settings_changed: the programme changed after it was read — by if_version, or by the reading this change was built over — so nothing was changed; read it again and resend.idempotency_key_in_use, settings_changed
413payload_too_large: the body is over 4,096 bytes.payload_too_large
422idempotency_key_reused: this Idempotency-Key was used for a different request in the last 24 hours. retroactive_change: the expiry change would expire points sooner than the rule shoppers have already been told about, or switch expiry on from a date already past (problem, and the current rule when there is one).idempotency_key_reused, retroactive_change
428confirmation_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, confirmation_invalid
429rate_limited: too many requests for this key (Retry-After). programme_write_limit: this account has made 100 programme changes through the API in the last 24 hours (limit, used); nothing was changed.rate_limited, programme_write_limit
503change_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, access_not_recorded

The rewards a shopper can spend points on at this store

GET /stores/{store_key}/programme/rewards — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme/rewards.

Needs: the read_programme permission (in the app: “Your programme settings — rates, rewards, rules”). A key with write_programme can call it too.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—

The answer

The effective rewards list.

FieldTypeAlways thereWhat it is
datalist of objectyes—
data[].idstringyesThe reward's id: what changeReward and removeReward take as reward_id.
has_morebooleanyes—
next_cursorstring, or nullyes—
versionstring, matching ^[0-9a-f]{32}$yesWhich stored programme this is: it changes whenever the store's programme settings change (not its look or expiry), so two reads with the same version saw the same programme. Send it as if_version with a programme change (POST /stores/{store_key}/programme) and the change is refused, 409 settings_changed, if the programme has changed since you read it. The embedded admin's save checks the same version.

Headers on the answer: Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. What each header means.

Example

export STORE='<key of a store from GET /stores>'
curl "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme/rewards" \
  -H "Authorization: Bearer $MATRIX_API_KEY"

The answer:

{
  "data": [
    {
      "id": "reward_5",
      "costPoints": 500,
      "valueLabel": "5.00 GBP off",
      "minSubtotal": "5.00",
      "valueAmount": "5.00"
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "version": "3f1c9a2b7d4e5f60718293a4b5c6d7e8"
}

Other answers

StatusWhat it meansError pages
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not carry the scope this call needs; required_scope says which.forbidden_scope
404not_found: no such resource on this account — indistinguishable from one that does not exist at all.not_found
429rate_limited: too many requests for this key; bucket says which allowance. Retry-After says how long to wait.rate_limited

Add a reward shoppers can spend points on

POST /stores/{store_key}/programme/rewards — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme/rewards.

Needs: the write_programme permission (in the app: “Change your programme: the earn rate, the joining and birthday bonuses, what a point is worth, when points expire, the rewards and the offers. Every change is previewable and logged”).

The platform names the reward reward_ and the next whole number, whatever its label says, never a number a reward or a code has already had. Every other reward, and every earn rule, is carried exactly as it is. The preview warns, and never refuses, when the reward is worth more than 10 times what its points are worth at the store's point value, or more than the smallest order it can be used on. Counts toward the 100 programme changes a day through the API.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—
Idempotency-Keyheaderyesstring, 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_runquerynobooleantrue 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-Confirmationheadernostring, up to 1,024 charactersThe 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.

FieldTypeRequiredWhat it is
kindone of amount, percentage, free_shippingnoWhat the reward gives: an amount off the order (the default), a percentage off its goods, or free shipping.
cost_pointsinteger, from 1 to 10,000,000yesWhat the reward costs, in points.
value_amountstring, matching ^\d{1,7}(?:\.\d{1,2})?$noFor an amount reward: how much it takes off an order, in the store's currency, more than zero. Not sent for the other kinds.
percent_offinteger, from 1 to 100noFor a percentage reward: the percentage off, a whole number.
min_subtotalstring, matching ^\d{1,7}(?:\.\d{1,2})?$noThe smallest order it can be used on; "0.00" (the default) for any order.
labelstring, up to 64 characters, at least 1 characternoWhat the shopper sees. Left out on an add, it is the value in the store's currency, like "£5.00 off".
if_versionstring, matching ^[0-9a-f]{32}$noThe version from a programme read. If the programme has changed since, nothing is changed (409 settings_changed).

The answer

The change, or with dry_run=true the preview of it. A request already answered is answered again, byte for byte (Idempotency-Replayed).

One shape: RewardResult.

FieldTypeAlways thereWhat it is
objectalways rewardyes—
rewardobject (ApiReward)yes—
reward.idstringyes—
reward.kindone of amount, percentage, free_shippingyesWhat the reward gives: an amount off the order (the default), a percentage off its goods, or free shipping.
reward.cost_pointsintegeryes—
reward.value_amountstringyesFor an amount reward, the amount off; "0.00" for the other kinds.
reward.percent_offinteger, or nullyesFor a percentage reward, the percentage off; otherwise null.
reward.min_subtotalstringyes—
reward.labelstringyes—
warningslist of one of reward_worth_more_than_points, reward_worth_more_than_minimum_orderyesWhat may be a mistake, never a refusal: reward_worth_more_than_points (worth more than 10 times what its points are worth at the store's point value) and reward_worth_more_than_minimum_order (worth more than the smallest order it can be used on; a reward with no minimum can be used on any order, so it always is). Only an amount reward is weighed: a percentage or free shipping is worth what the order makes it.
store_keystringyes—
versionstring, matching ^[0-9a-f]{32}$yesThe programme's version after the change.
changedlist of stringyesThe settings that changed, in the engine's names. Empty when the change asked for what was already set.
as_paidobject, or nullyesWhat a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.
as_paid.earn_rate_per_major_unitnumber, at least 0yesPoints an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than "earn 0".
as_paid.earn_rates_per_major_unitlist of number, at least 0yesThe same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays.
as_paid.welcome_bonus_pointsinteger, at least 0yesThe joining bonus a new shopper is paid, once. 0 when the store pays none, whatever effective.welcomeBonusPoints says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's effective.bonusesInAppOnly), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in ways_to_earn says so.
as_paid.ways_to_earnlist of objectyeseffective.waysToEarn as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from earn_rate_per_major_unit, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.
as_paid.ways_to_earn[].idstringyes—
as_paid.ways_to_earn[].labelstringyes—
as_paid.ways_to_earn[].detailstringyes—
audit_idstring, or nullyes—

One shape: RewardPreview.

FieldTypeAlways thereWhat it is
objectalways reward_previewyes—
rewardobject (ApiReward)yes—
reward.idstringyes—
reward.kindone of amount, percentage, free_shippingyesWhat the reward gives: an amount off the order (the default), a percentage off its goods, or free shipping.
reward.cost_pointsintegeryes—
reward.value_amountstringyesFor an amount reward, the amount off; "0.00" for the other kinds.
reward.percent_offinteger, or nullyesFor a percentage reward, the percentage off; otherwise null.
reward.min_subtotalstringyes—
reward.labelstringyes—
warningslist of one of reward_worth_more_than_points, reward_worth_more_than_minimum_orderyesWhat may be a mistake, never a refusal: reward_worth_more_than_points (worth more than 10 times what its points are worth at the store's point value) and reward_worth_more_than_minimum_order (worth more than the smallest order it can be used on; a reward with no minimum can be used on any order, so it always is). Only an amount reward is weighed: a percentage or free shipping is worth what the order makes it.
dry_runalways trueyes—
outcomeone of would_apply, no_changeyes—
store_keystringyes—
changedlist of stringyesThe settings that would change, in the engine's names. What each moves from and to is before_after.
before_afterobject (PreviewBeforeAfter)yesEvery figure the change moves, before and after, in this API's own write field names, worked out from the programme as it would price orders before and after (the same way the storefront works them out). Only the ones that differ are here, so {} means none moves. points_per_currency_unit is the everyday rate an order pays (a running campaign or other offer left out); welcome_bonus_points and birthday_bonus_points are 0 when none is paid; point_value is how many points are worth one unit of the currency; ways_to_earn is the list as a shopper reads it. On a reward's call, reward is the reward itself: null before an add, null after a removal.
before_after.points_per_currency_unitobjectno—
before_after.points_per_currency_unit.beforenumber, at least 0yes—
before_after.points_per_currency_unit.afternumber, at least 0yes—
before_after.welcome_bonus_pointsobjectno—
before_after.welcome_bonus_points.beforeinteger, at least 0yes—
before_after.welcome_bonus_points.afterinteger, at least 0yes—
before_after.birthday_bonus_pointsobjectno—
before_after.birthday_bonus_points.beforeinteger, at least 0yes—
before_after.birthday_bonus_points.afterinteger, at least 0yes—
before_after.point_valueobjectno—
before_after.point_value.beforeobjectyes—
before_after.point_value.before.pointsinteger, at least 1yes—
before_after.point_value.before.amountalways 1.00yes—
before_after.point_value.afterobjectyes—
before_after.point_value.after.pointsinteger, at least 1yes—
before_after.point_value.after.amountalways 1.00yes—
before_after.timezoneobjectno—
before_after.timezone.beforestringyes—
before_after.timezone.afterstringyes—
before_after.ways_to_earnobjectno—
before_after.ways_to_earn.beforelist of objectyes—
before_after.ways_to_earn.before[].idstringyes—
before_after.ways_to_earn.before[].labelstringyes—
before_after.ways_to_earn.before[].detailstringyes—
before_after.ways_to_earn.afterlist of objectyes—
before_after.ways_to_earn.after[].idstringyes—
before_after.ways_to_earn.after[].labelstringyes—
before_after.ways_to_earn.after[].detailstringyes—
before_after.referralobjectno—
before_after.referral.beforeany, or null or objectyes—
before_after.referral.afterany, or null or objectyes—
before_after.spend_any_amountobjectno—
before_after.spend_any_amount.beforeany, or null or objectyes—
before_after.spend_any_amount.afterany, or null or objectyes—
before_after.rewardobjectno—
before_after.reward.beforeany, or null or object (ApiReward)yes—
before_after.reward.afterany, or null or object (ApiReward)yes—
currencystring, or nullyes—
exampleslist of objectyesWhat a plain order of each size earns now, before and after the change, under every offer running now.
examples[].order_amountstringyes—
examples[].points_beforeintegeryes—
examples[].points_afterintegeryes—
point_valueobjectyes—
point_value.points_per_currency_unit_beforeinteger, or nullyes—
point_value.points_per_currency_unit_afterinteger, or nullyes—
liabilityany, or null or object or objectyesWhen the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; unavailable when the account has more than one store.
version_afterstring, matching ^[0-9a-f]{32}$, or nullyesThe version the programme would have.
confirmationany, or null or objectyesFor a key tagged for an assistant: send token back as the Matrix-Confirmation header with the same request. Null when nothing would change.
audit_idstring, or nullyes—

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.

Example

export STORE='<key of a store from GET /stores>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme/rewards" \
  -H "Authorization: Bearer $MATRIX_API_KEY" \
  -H "Idempotency-Key: reward-add-2026-09-16-0001" \
  -H "Content-Type: application/json" \
  -d '{"cost_points": 1000, "value_amount": "10.00"}'

The reward, added:

{
  "object": "reward",
  "reward": {
    "id": "reward_6",
    "kind": "amount",
    "cost_points": 1000,
    "value_amount": "10.00",
    "percent_off": null,
    "min_subtotal": "0.00",
    "label": "£10.00 off"
  },
  "warnings": [],
  "store_key": "example-store",
  "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f1",
  "changed": [
    "rewards"
  ],
  "as_paid": {
    "earn_rate_per_major_unit": 1,
    "earn_rates_per_major_unit": [
      1
    ],
    "welcome_bonus_points": 0,
    "ways_to_earn": []
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000010"
}

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

{
  "object": "reward_preview",
  "reward": {
    "id": "reward_6",
    "kind": "amount",
    "cost_points": 1000,
    "value_amount": "10.00",
    "percent_off": null,
    "min_subtotal": "0.00",
    "label": "£10.00 off"
  },
  "warnings": [],
  "dry_run": true,
  "outcome": "would_apply",
  "store_key": "example-store",
  "changed": [
    "rewards"
  ],
  "before_after": {
    "reward": {
      "before": null,
      "after": {
        "id": "reward_6",
        "kind": "amount",
        "cost_points": 1000,
        "value_amount": "10.00",
        "percent_off": null,
        "min_subtotal": "0.00",
        "label": "£10.00 off"
      }
    }
  },
  "currency": "GBP",
  "examples": [
    {
      "order_amount": "10.00",
      "points_before": 10,
      "points_after": 10
    },
    {
      "order_amount": "50.00",
      "points_before": 50,
      "points_after": 50
    },
    {
      "order_amount": "100.00",
      "points_before": 100,
      "points_after": 100
    }
  ],
  "point_value": {
    "points_per_currency_unit_before": 100,
    "points_per_currency_unit_after": 100
  },
  "liability": null,
  "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f1",
  "confirmation": {
    "token": "mct_EXAMPLE.EXAMPLE",
    "expires_at": "2026-09-16T10:10:00.000Z"
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000011"
}

Other answers

StatusWhat it meansError pages
400invalid_request: a parameter or the body could not be read; param names it.invalid_request
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not hold write_programme (required_scope says so). test_mode_key: a test key can preview a change and never make one. assistant_limit: the key is an assistant's (every Claude connection, and a key made for an assistant), the merchant has not lifted its fixed limits (/me key.limits_lifted; only a person can lift them, on the app's API keys screen), and the change moves the store's money further than one step. It is measured from the programme as it stood before the assistant's changes began, so steps add up; that starting point holds until assistant_window_hours pass with no assistant change, and a change by the owner to what the programme pays starts afresh (a new label or reason does not; nor does a change by an assistant key whose limits are lifted). What a point is worth, the currency and when points expire never change. What an order earns at most doubles: a new offer counts as paying every order, against what every order earned; and the everyday rate, which still prices some refunds (an API refund that names no order, and an order paid before amounts were recorded), is not lowered to fund an offer (then the figures are the everyday rate's own, least_allowed is the least it may be for every order to earn what it did, up to the field's two decimal places — null when no rate within its own step would do — and on an offer's route no param is named). Every other figure that pays shoppers at most doubles, one at zero stays there, and referrals that are off stay off. Spending any amount that is off stays off; while it is on, the fewest points one code may take and the smallest order it can be used on at most halve, and the most points at most double (a limit cannot be removed); switching it off is never limited. An offer may only narrow who and when it pays. A reward at most doubles what it gives for each point, keeps its kind, and its smallest order at most halves; a new reward stays within one step of a reward of its kind. The preview is refused the same way. problem says which limit; before, asked and most_allowed or least_allowed give the figure in the unit of param, and sending that limit back is accepted.forbidden_scope, test_mode_key, assistant_limit
404not_found: no such member on this account, or (cancelling a code) the member holds no code with that ref.not_found
409idempotency_key_in_use: a request with this key is still running (Retry-After). settings_changed: the programme changed after it was read — by if_version, or by the reading this change was built over — so nothing was changed; read it again and resend.idempotency_key_in_use, settings_changed
413payload_too_large: the body is over 4,096 bytes.payload_too_large
422idempotency_key_reused: this Idempotency-Key was used for a different request in the last 24 hours.idempotency_key_reused
428confirmation_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, confirmation_invalid
429rate_limited: too many requests for this key (Retry-After). programme_write_limit: this account has made 100 programme changes through the API in the last 24 hours (limit, used); nothing was changed.rate_limited, programme_write_limit
503change_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, access_not_recorded

Change a reward's cost, value, minimum order or label

POST /stores/{store_key}/programme/rewards/{reward_id} — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme/rewards/{reward_id}.

Needs: the write_programme permission (in the app: “Change your programme: the earn rate, the joining and birthday bonuses, what a point is worth, when points expire, the rewards and the offers. Every change is previewable and logged”).

Only the fields sent change. Codes already issued for the reward keep the value they were issued with. The same warnings as an add.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—
reward_idpathyesstring, matching ^[A-Za-z0-9._-]{1,64}$A reward's id, as listRewards gives it.
Idempotency-Keyheaderyesstring, 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_runquerynobooleantrue 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-Confirmationheadernostring, up to 1,024 charactersThe 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.

FieldTypeRequiredWhat it is
kindone of amount, percentage, free_shippingnoWhat the reward gives: an amount off the order (the default), a percentage off its goods, or free shipping.
cost_pointsinteger, from 1 to 10,000,000noWhat the reward costs, in points.
value_amountstring, matching ^\d{1,7}(?:\.\d{1,2})?$noFor an amount reward: how much it takes off an order, in the store's currency, more than zero. Not sent for the other kinds.
percent_offinteger, from 1 to 100noFor a percentage reward: the percentage off, a whole number.
min_subtotalstring, matching ^\d{1,7}(?:\.\d{1,2})?$noThe smallest order it can be used on; "0.00" (the default) for any order.
labelstring, up to 64 characters, at least 1 characternoWhat the shopper sees. Left out on an add, it is the value in the store's currency, like "£5.00 off".
if_versionstring, matching ^[0-9a-f]{32}$noThe version from a programme read. If the programme has changed since, nothing is changed (409 settings_changed).

The answer

The change, or with dry_run=true the preview of it. A request already answered is answered again, byte for byte (Idempotency-Replayed).

One shape: RewardResult.

FieldTypeAlways thereWhat it is
objectalways rewardyes—
rewardobject (ApiReward)yes—
reward.idstringyes—
reward.kindone of amount, percentage, free_shippingyesWhat the reward gives: an amount off the order (the default), a percentage off its goods, or free shipping.
reward.cost_pointsintegeryes—
reward.value_amountstringyesFor an amount reward, the amount off; "0.00" for the other kinds.
reward.percent_offinteger, or nullyesFor a percentage reward, the percentage off; otherwise null.
reward.min_subtotalstringyes—
reward.labelstringyes—
warningslist of one of reward_worth_more_than_points, reward_worth_more_than_minimum_orderyesWhat may be a mistake, never a refusal: reward_worth_more_than_points (worth more than 10 times what its points are worth at the store's point value) and reward_worth_more_than_minimum_order (worth more than the smallest order it can be used on; a reward with no minimum can be used on any order, so it always is). Only an amount reward is weighed: a percentage or free shipping is worth what the order makes it.
store_keystringyes—
versionstring, matching ^[0-9a-f]{32}$yesThe programme's version after the change.
changedlist of stringyesThe settings that changed, in the engine's names. Empty when the change asked for what was already set.
as_paidobject, or nullyesWhat a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.
as_paid.earn_rate_per_major_unitnumber, at least 0yesPoints an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than "earn 0".
as_paid.earn_rates_per_major_unitlist of number, at least 0yesThe same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays.
as_paid.welcome_bonus_pointsinteger, at least 0yesThe joining bonus a new shopper is paid, once. 0 when the store pays none, whatever effective.welcomeBonusPoints says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's effective.bonusesInAppOnly), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in ways_to_earn says so.
as_paid.ways_to_earnlist of objectyeseffective.waysToEarn as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from earn_rate_per_major_unit, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.
as_paid.ways_to_earn[].idstringyes—
as_paid.ways_to_earn[].labelstringyes—
as_paid.ways_to_earn[].detailstringyes—
audit_idstring, or nullyes—

One shape: RewardPreview.

FieldTypeAlways thereWhat it is
objectalways reward_previewyes—
rewardobject (ApiReward)yes—
reward.idstringyes—
reward.kindone of amount, percentage, free_shippingyesWhat the reward gives: an amount off the order (the default), a percentage off its goods, or free shipping.
reward.cost_pointsintegeryes—
reward.value_amountstringyesFor an amount reward, the amount off; "0.00" for the other kinds.
reward.percent_offinteger, or nullyesFor a percentage reward, the percentage off; otherwise null.
reward.min_subtotalstringyes—
reward.labelstringyes—
warningslist of one of reward_worth_more_than_points, reward_worth_more_than_minimum_orderyesWhat may be a mistake, never a refusal: reward_worth_more_than_points (worth more than 10 times what its points are worth at the store's point value) and reward_worth_more_than_minimum_order (worth more than the smallest order it can be used on; a reward with no minimum can be used on any order, so it always is). Only an amount reward is weighed: a percentage or free shipping is worth what the order makes it.
dry_runalways trueyes—
outcomeone of would_apply, no_changeyes—
store_keystringyes—
changedlist of stringyesThe settings that would change, in the engine's names. What each moves from and to is before_after.
before_afterobject (PreviewBeforeAfter)yesEvery figure the change moves, before and after, in this API's own write field names, worked out from the programme as it would price orders before and after (the same way the storefront works them out). Only the ones that differ are here, so {} means none moves. points_per_currency_unit is the everyday rate an order pays (a running campaign or other offer left out); welcome_bonus_points and birthday_bonus_points are 0 when none is paid; point_value is how many points are worth one unit of the currency; ways_to_earn is the list as a shopper reads it. On a reward's call, reward is the reward itself: null before an add, null after a removal.
before_after.points_per_currency_unitobjectno—
before_after.points_per_currency_unit.beforenumber, at least 0yes—
before_after.points_per_currency_unit.afternumber, at least 0yes—
before_after.welcome_bonus_pointsobjectno—
before_after.welcome_bonus_points.beforeinteger, at least 0yes—
before_after.welcome_bonus_points.afterinteger, at least 0yes—
before_after.birthday_bonus_pointsobjectno—
before_after.birthday_bonus_points.beforeinteger, at least 0yes—
before_after.birthday_bonus_points.afterinteger, at least 0yes—
before_after.point_valueobjectno—
before_after.point_value.beforeobjectyes—
before_after.point_value.before.pointsinteger, at least 1yes—
before_after.point_value.before.amountalways 1.00yes—
before_after.point_value.afterobjectyes—
before_after.point_value.after.pointsinteger, at least 1yes—
before_after.point_value.after.amountalways 1.00yes—
before_after.timezoneobjectno—
before_after.timezone.beforestringyes—
before_after.timezone.afterstringyes—
before_after.ways_to_earnobjectno—
before_after.ways_to_earn.beforelist of objectyes—
before_after.ways_to_earn.before[].idstringyes—
before_after.ways_to_earn.before[].labelstringyes—
before_after.ways_to_earn.before[].detailstringyes—
before_after.ways_to_earn.afterlist of objectyes—
before_after.ways_to_earn.after[].idstringyes—
before_after.ways_to_earn.after[].labelstringyes—
before_after.ways_to_earn.after[].detailstringyes—
before_after.referralobjectno—
before_after.referral.beforeany, or null or objectyes—
before_after.referral.afterany, or null or objectyes—
before_after.spend_any_amountobjectno—
before_after.spend_any_amount.beforeany, or null or objectyes—
before_after.spend_any_amount.afterany, or null or objectyes—
before_after.rewardobjectno—
before_after.reward.beforeany, or null or object (ApiReward)yes—
before_after.reward.afterany, or null or object (ApiReward)yes—
currencystring, or nullyes—
exampleslist of objectyesWhat a plain order of each size earns now, before and after the change, under every offer running now.
examples[].order_amountstringyes—
examples[].points_beforeintegeryes—
examples[].points_afterintegeryes—
point_valueobjectyes—
point_value.points_per_currency_unit_beforeinteger, or nullyes—
point_value.points_per_currency_unit_afterinteger, or nullyes—
liabilityany, or null or object or objectyesWhen the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; unavailable when the account has more than one store.
version_afterstring, matching ^[0-9a-f]{32}$, or nullyesThe version the programme would have.
confirmationany, or null or objectyesFor a key tagged for an assistant: send token back as the Matrix-Confirmation header with the same request. Null when nothing would change.
audit_idstring, or nullyes—

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.

Example

export STORE='<key of a store from GET /stores>'
export REWARD='<id of a reward from GET /stores/{store_key}/programme/rewards>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme/rewards/$REWARD" \
  -H "Authorization: Bearer $MATRIX_API_KEY" \
  -H "Idempotency-Key: reward-change-2026-09-16-0001" \
  -H "Content-Type: application/json" \
  -d '{"cost_points": 800}'

The reward, changed:

{
  "object": "reward",
  "reward": {
    "id": "reward_6",
    "kind": "amount",
    "cost_points": 800,
    "value_amount": "10.00",
    "percent_off": null,
    "min_subtotal": "0.00",
    "label": "£10.00 off"
  },
  "warnings": [],
  "store_key": "example-store",
  "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f2",
  "changed": [
    "rewards"
  ],
  "as_paid": {
    "earn_rate_per_major_unit": 1,
    "earn_rates_per_major_unit": [
      1
    ],
    "welcome_bonus_points": 0,
    "ways_to_earn": []
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000012"
}

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

{
  "object": "reward_preview",
  "reward": {
    "id": "reward_6",
    "kind": "amount",
    "cost_points": 800,
    "value_amount": "10.00",
    "percent_off": null,
    "min_subtotal": "0.00",
    "label": "£10.00 off"
  },
  "warnings": [],
  "dry_run": true,
  "outcome": "would_apply",
  "store_key": "example-store",
  "changed": [
    "rewards"
  ],
  "before_after": {
    "reward": {
      "before": {
        "id": "reward_6",
        "kind": "amount",
        "cost_points": 1000,
        "value_amount": "10.00",
        "percent_off": null,
        "min_subtotal": "0.00",
        "label": "£10.00 off"
      },
      "after": {
        "id": "reward_6",
        "kind": "amount",
        "cost_points": 800,
        "value_amount": "10.00",
        "percent_off": null,
        "min_subtotal": "0.00",
        "label": "£10.00 off"
      }
    }
  },
  "currency": "GBP",
  "examples": [
    {
      "order_amount": "10.00",
      "points_before": 10,
      "points_after": 10
    },
    {
      "order_amount": "50.00",
      "points_before": 50,
      "points_after": 50
    },
    {
      "order_amount": "100.00",
      "points_before": 100,
      "points_after": 100
    }
  ],
  "point_value": {
    "points_per_currency_unit_before": 100,
    "points_per_currency_unit_after": 100
  },
  "liability": null,
  "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f2",
  "confirmation": {
    "token": "mct_EXAMPLE.EXAMPLE",
    "expires_at": "2026-09-16T10:10:00.000Z"
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000013"
}

Other answers

StatusWhat it meansError pages
400invalid_request: a parameter or the body could not be read; param names it.invalid_request
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not hold write_programme (required_scope says so). test_mode_key: a test key can preview a change and never make one. assistant_limit: the key is an assistant's (every Claude connection, and a key made for an assistant), the merchant has not lifted its fixed limits (/me key.limits_lifted; only a person can lift them, on the app's API keys screen), and the change moves the store's money further than one step. It is measured from the programme as it stood before the assistant's changes began, so steps add up; that starting point holds until assistant_window_hours pass with no assistant change, and a change by the owner to what the programme pays starts afresh (a new label or reason does not; nor does a change by an assistant key whose limits are lifted). What a point is worth, the currency and when points expire never change. What an order earns at most doubles: a new offer counts as paying every order, against what every order earned; and the everyday rate, which still prices some refunds (an API refund that names no order, and an order paid before amounts were recorded), is not lowered to fund an offer (then the figures are the everyday rate's own, least_allowed is the least it may be for every order to earn what it did, up to the field's two decimal places — null when no rate within its own step would do — and on an offer's route no param is named). Every other figure that pays shoppers at most doubles, one at zero stays there, and referrals that are off stay off. Spending any amount that is off stays off; while it is on, the fewest points one code may take and the smallest order it can be used on at most halve, and the most points at most double (a limit cannot be removed); switching it off is never limited. An offer may only narrow who and when it pays. A reward at most doubles what it gives for each point, keeps its kind, and its smallest order at most halves; a new reward stays within one step of a reward of its kind. The preview is refused the same way. problem says which limit; before, asked and most_allowed or least_allowed give the figure in the unit of param, and sending that limit back is accepted.forbidden_scope, test_mode_key, assistant_limit
404not_found: no such member on this account, or (cancelling a code) the member holds no code with that ref.not_found
409idempotency_key_in_use: a request with this key is still running (Retry-After). settings_changed: the programme changed after it was read — by if_version, or by the reading this change was built over — so nothing was changed; read it again and resend.idempotency_key_in_use, settings_changed
413payload_too_large: the body is over 4,096 bytes.payload_too_large
422idempotency_key_reused: this Idempotency-Key was used for a different request in the last 24 hours.idempotency_key_reused
428confirmation_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, confirmation_invalid
429rate_limited: too many requests for this key (Retry-After). programme_write_limit: this account has made 100 programme changes through the API in the last 24 hours (limit, used); nothing was changed.rate_limited, programme_write_limit
503change_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, access_not_recorded

Remove a reward

POST /stores/{store_key}/programme/rewards/{reward_id}/remove — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme/rewards/{reward_id}/remove.

Needs: the write_programme permission (in the app: “Change your programme: the earn rate, the joining and birthday bonuses, what a point is worth, when points expire, the rewards and the offers. Every change is previewable and logged”).

Codes already issued for it stay valid. The store's only reward cannot be removed (409 last_reward): a programme with nothing to spend points on is a trap.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—
reward_idpathyesstring, matching ^[A-Za-z0-9._-]{1,64}$A reward's id, as listRewards gives it.
Idempotency-Keyheaderyesstring, 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_runquerynobooleantrue 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-Confirmationheadernostring, up to 1,024 charactersThe 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.

FieldTypeRequiredWhat it is
if_versionstring, matching ^[0-9a-f]{32}$noThe version from a programme read. If the programme has changed since, nothing is changed (409 settings_changed).

The answer

The change, or with dry_run=true the preview of it. A request already answered is answered again, byte for byte (Idempotency-Replayed).

One shape: RewardRemoved.

FieldTypeAlways thereWhat it is
objectalways reward_removedyes—
reward_idstringyes—
store_keystringyes—
versionstring, matching ^[0-9a-f]{32}$yesThe programme's version after the change.
changedlist of stringyesThe settings that changed, in the engine's names. Empty when the change asked for what was already set.
as_paidobject, or nullyesWhat a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.
as_paid.earn_rate_per_major_unitnumber, at least 0yesPoints an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than "earn 0".
as_paid.earn_rates_per_major_unitlist of number, at least 0yesThe same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays.
as_paid.welcome_bonus_pointsinteger, at least 0yesThe joining bonus a new shopper is paid, once. 0 when the store pays none, whatever effective.welcomeBonusPoints says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's effective.bonusesInAppOnly), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in ways_to_earn says so.
as_paid.ways_to_earnlist of objectyeseffective.waysToEarn as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from earn_rate_per_major_unit, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.
as_paid.ways_to_earn[].idstringyes—
as_paid.ways_to_earn[].labelstringyes—
as_paid.ways_to_earn[].detailstringyes—
audit_idstring, or nullyes—

One shape: RewardRemovedPreview.

FieldTypeAlways thereWhat it is
objectalways reward_removed_previewyes—
reward_idstringyes—
dry_runalways trueyes—
outcomeone of would_apply, no_changeyes—
store_keystringyes—
changedlist of stringyesThe settings that would change, in the engine's names. What each moves from and to is before_after.
before_afterobject (PreviewBeforeAfter)yesEvery figure the change moves, before and after, in this API's own write field names, worked out from the programme as it would price orders before and after (the same way the storefront works them out). Only the ones that differ are here, so {} means none moves. points_per_currency_unit is the everyday rate an order pays (a running campaign or other offer left out); welcome_bonus_points and birthday_bonus_points are 0 when none is paid; point_value is how many points are worth one unit of the currency; ways_to_earn is the list as a shopper reads it. On a reward's call, reward is the reward itself: null before an add, null after a removal.
before_after.points_per_currency_unitobjectno—
before_after.points_per_currency_unit.beforenumber, at least 0yes—
before_after.points_per_currency_unit.afternumber, at least 0yes—
before_after.welcome_bonus_pointsobjectno—
before_after.welcome_bonus_points.beforeinteger, at least 0yes—
before_after.welcome_bonus_points.afterinteger, at least 0yes—
before_after.birthday_bonus_pointsobjectno—
before_after.birthday_bonus_points.beforeinteger, at least 0yes—
before_after.birthday_bonus_points.afterinteger, at least 0yes—
before_after.point_valueobjectno—
before_after.point_value.beforeobjectyes—
before_after.point_value.before.pointsinteger, at least 1yes—
before_after.point_value.before.amountalways 1.00yes—
before_after.point_value.afterobjectyes—
before_after.point_value.after.pointsinteger, at least 1yes—
before_after.point_value.after.amountalways 1.00yes—
before_after.timezoneobjectno—
before_after.timezone.beforestringyes—
before_after.timezone.afterstringyes—
before_after.ways_to_earnobjectno—
before_after.ways_to_earn.beforelist of objectyes—
before_after.ways_to_earn.before[].idstringyes—
before_after.ways_to_earn.before[].labelstringyes—
before_after.ways_to_earn.before[].detailstringyes—
before_after.ways_to_earn.afterlist of objectyes—
before_after.ways_to_earn.after[].idstringyes—
before_after.ways_to_earn.after[].labelstringyes—
before_after.ways_to_earn.after[].detailstringyes—
before_after.referralobjectno—
before_after.referral.beforeany, or null or objectyes—
before_after.referral.afterany, or null or objectyes—
before_after.spend_any_amountobjectno—
before_after.spend_any_amount.beforeany, or null or objectyes—
before_after.spend_any_amount.afterany, or null or objectyes—
before_after.rewardobjectno—
before_after.reward.beforeany, or null or object (ApiReward)yes—
before_after.reward.afterany, or null or object (ApiReward)yes—
currencystring, or nullyes—
exampleslist of objectyesWhat a plain order of each size earns now, before and after the change, under every offer running now.
examples[].order_amountstringyes—
examples[].points_beforeintegeryes—
examples[].points_afterintegeryes—
point_valueobjectyes—
point_value.points_per_currency_unit_beforeinteger, or nullyes—
point_value.points_per_currency_unit_afterinteger, or nullyes—
liabilityany, or null or object or objectyesWhen the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; unavailable when the account has more than one store.
version_afterstring, matching ^[0-9a-f]{32}$, or nullyesThe version the programme would have.
confirmationany, or null or objectyesFor a key tagged for an assistant: send token back as the Matrix-Confirmation header with the same request. Null when nothing would change.
audit_idstring, or nullyes—

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.

Example

export STORE='<key of a store from GET /stores>'
export REWARD='<id of a reward from GET /stores/{store_key}/programme/rewards>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme/rewards/$REWARD/remove" \
  -H "Authorization: Bearer $MATRIX_API_KEY" \
  -H "Idempotency-Key: reward-remove-2026-09-16-0001" \
  -H "Content-Type: application/json" \
  -d '{}'

The reward, removed:

{
  "object": "reward_removed",
  "reward_id": "reward_6",
  "store_key": "example-store",
  "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f3",
  "changed": [
    "rewards"
  ],
  "as_paid": {
    "earn_rate_per_major_unit": 1,
    "earn_rates_per_major_unit": [
      1
    ],
    "welcome_bonus_points": 0,
    "ways_to_earn": []
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000014"
}

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

{
  "object": "reward_removed_preview",
  "reward_id": "reward_6",
  "dry_run": true,
  "outcome": "would_apply",
  "store_key": "example-store",
  "changed": [
    "rewards"
  ],
  "before_after": {
    "reward": {
      "before": {
        "id": "reward_6",
        "kind": "amount",
        "cost_points": 800,
        "value_amount": "10.00",
        "percent_off": null,
        "min_subtotal": "0.00",
        "label": "£10.00 off"
      },
      "after": null
    }
  },
  "currency": "GBP",
  "examples": [
    {
      "order_amount": "10.00",
      "points_before": 10,
      "points_after": 10
    },
    {
      "order_amount": "50.00",
      "points_before": 50,
      "points_after": 50
    },
    {
      "order_amount": "100.00",
      "points_before": 100,
      "points_after": 100
    }
  ],
  "point_value": {
    "points_per_currency_unit_before": 100,
    "points_per_currency_unit_after": 100
  },
  "liability": null,
  "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f3",
  "confirmation": {
    "token": "mct_EXAMPLE.EXAMPLE",
    "expires_at": "2026-09-16T10:10:00.000Z"
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000015"
}

Other answers

StatusWhat it meansError pages
400invalid_request: a parameter or the body could not be read; param names it.invalid_request
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not hold write_programme (required_scope says so). test_mode_key: a test key can preview a change and never make one. assistant_limit: the key is an assistant's (every Claude connection, and a key made for an assistant), the merchant has not lifted its fixed limits (/me key.limits_lifted; only a person can lift them, on the app's API keys screen), and the change moves the store's money further than one step. It is measured from the programme as it stood before the assistant's changes began, so steps add up; that starting point holds until assistant_window_hours pass with no assistant change, and a change by the owner to what the programme pays starts afresh (a new label or reason does not; nor does a change by an assistant key whose limits are lifted). What a point is worth, the currency and when points expire never change. What an order earns at most doubles: a new offer counts as paying every order, against what every order earned; and the everyday rate, which still prices some refunds (an API refund that names no order, and an order paid before amounts were recorded), is not lowered to fund an offer (then the figures are the everyday rate's own, least_allowed is the least it may be for every order to earn what it did, up to the field's two decimal places — null when no rate within its own step would do — and on an offer's route no param is named). Every other figure that pays shoppers at most doubles, one at zero stays there, and referrals that are off stay off. Spending any amount that is off stays off; while it is on, the fewest points one code may take and the smallest order it can be used on at most halve, and the most points at most double (a limit cannot be removed); switching it off is never limited. An offer may only narrow who and when it pays. A reward at most doubles what it gives for each point, keeps its kind, and its smallest order at most halves; a new reward stays within one step of a reward of its kind. The preview is refused the same way. problem says which limit; before, asked and most_allowed or least_allowed give the figure in the unit of param, and sending that limit back is accepted.forbidden_scope, test_mode_key, assistant_limit
404not_found: no such member on this account, or (cancelling a code) the member holds no code with that ref.not_found
409idempotency_key_in_use: a request with this key is still running (Retry-After). settings_changed: the programme changed after it was read, so nothing was changed. last_reward: this is the store's only reward, and a programme with nothing to spend points on is not allowed; add another first.idempotency_key_in_use, settings_changed, last_reward
413payload_too_large: the body is over 4,096 bytes.payload_too_large
422idempotency_key_reused: this Idempotency-Key was used for a different request in the last 24 hours.idempotency_key_reused
428confirmation_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, confirmation_invalid
429rate_limited: too many requests for this key (Retry-After). programme_write_limit: this account has made 100 programme changes through the API in the last 24 hours (limit, used); nothing was changed.rate_limited, programme_write_limit
503change_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, access_not_recorded

A store's offers

GET /stores/{store_key}/programme/offers — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme/offers.

Needs: the read_programme permission (in the app: “Your programme settings — rates, rewards, rules”). A key with write_programme can call it too.

Campaigns, product offers and channel offers in the words the offer calls take, and every other rule as other, read-only. The everyday rate and the two bonuses are not offers: they are in getProgramme.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—

The answer

The store's offers.

FieldTypeAlways thereWhat it is
datalist of object (ApiOffer)yes—
data[].idstringyes—
data[].kindone of campaign, product, channel, otheryes—
data[].reasonstringyesWhat the shopper is told the points are for.
data[].read_onlybooleanyesTrue for other, and for a campaign that has ended: remove it, but not change it.
data[].endedbooleanno—
data[].points_per_currency_unitnumberno—
data[].starts_atstringno—
data[].ends_atstringno—
data[].starts_onstringnoThe first day, in the store's time zone.
data[].ends_onstringnoThe last day, in the store's time zone.
data[].points_per_itemintegerno—
data[].product_idslist of stringno—
data[].channelobjectnoOrders from any of these count: a sales channel's source name, an app's id, or an order tag; at most 50 of each, and at least one list not empty.
data[].channel.source_nameslist of string, up to 100 characters, at least 1 characterno—
data[].channel.app_idslist of integer, at least 0no—
data[].channel.order_tagslist of string, up to 100 characters, at least 1 characterno—
has_morebooleanyes—
next_cursorstring, or nullyes—
versionstring, matching ^[0-9a-f]{32}$yesWhich stored programme this is: it changes whenever the store's programme settings change (not its look or expiry), so two reads with the same version saw the same programme. Send it as if_version with a programme change (POST /stores/{store_key}/programme) and the change is refused, 409 settings_changed, if the programme has changed since you read it. The embedded admin's save checks the same version.

Headers on the answer: Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. What each header means.

Example

export STORE='<key of a store from GET /stores>'
curl "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme/offers" \
  -H "Authorization: Bearer $MATRIX_API_KEY"

The answer:

{
  "data": [
    {
      "id": "campaign_mfk2q",
      "kind": "campaign",
      "reason": "Double points week",
      "read_only": false,
      "ended": false,
      "points_per_currency_unit": 1,
      "starts_at": "2026-10-05T23:00:00.000Z",
      "ends_at": "2026-10-12T23:00:00.000Z",
      "starts_on": "2026-10-06",
      "ends_on": "2026-10-12"
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "version": "3f1c9a2b7d4e5f60718293a4b5c6d7e8"
}

Other answers

StatusWhat it meansError pages
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not carry the scope this call needs; required_scope says which.forbidden_scope
404not_found: no such resource on this account — indistinguishable from one that does not exist at all.not_found
429rate_limited: too many requests for this key; bucket says which allowance. Retry-After says how long to wait.rate_limited

Add an offer: a campaign, a product offer or a channel offer

POST /stores/{store_key}/programme/offers — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme/offers.

Needs: the write_programme permission (in the app: “Change your programme: the earn rate, the joining and birthday bonuses, what a point is worth, when points expire, the rewards and the offers. Every change is previewable and logged”).

The platform makes the offer's id. Every other rule is carried exactly as it is. A campaign cannot start before today or end on a day already past, in the store's time zone; one starting today starts when it is made. The preview prices three orders under the offers running now, so a campaign starting later changes nothing in it yet.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—
Idempotency-Keyheaderyesstring, 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_runquerynobooleantrue 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-Confirmationheadernostring, up to 1,024 charactersThe 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.

FieldTypeRequiredWhat it is
kindone of campaign, product, channelno—
reasonstring, up to 200 characters, at least 1 characterno—
points_per_currency_unitnumber, at most 1,000noExtra points for every 1 of the store's currency: more than 0, at most 2 decimal places.
starts_onstring, matching ^\d{4}-\d{2}-\d{2}$noThe first day, in the store's time zone: today or later.
ends_onstring, matching ^\d{4}-\d{2}-\d{2}$noThe last day, in the store's time zone: not before starts_on, and not already past.
points_per_iteminteger, from 1 to 1,000,000no—
product_idslist of string or integer, at least 1noShopify product ids, at most 500: numbers, or gid://shopify/Product/…
channelobjectnoOrders from any of these count: a sales channel's source name, an app's id, or an order tag; at most 50 of each, and at least one list not empty.
channel.source_nameslist of string, up to 100 characters, at least 1 characterno—
channel.app_idslist of integer, at least 0no—
channel.order_tagslist of string, up to 100 characters, at least 1 characterno—
if_versionstring, matching ^[0-9a-f]{32}$noThe version from a programme read. If the programme has changed since, nothing is changed (409 settings_changed).

The answer

The change, or with dry_run=true the preview of it. A request already answered is answered again, byte for byte (Idempotency-Replayed).

One shape: OfferResult.

FieldTypeAlways thereWhat it is
objectalways offeryes—
offerobject (ApiOffer)yesAn offer in the store's own days. campaign: extra points per unit between two days. product: points per item of chosen products. channel: extra points per unit on orders from chosen sources. other: set up another way; read-only here.
offer.idstringyes—
offer.kindone of campaign, product, channel, otheryes—
offer.reasonstringyesWhat the shopper is told the points are for.
offer.read_onlybooleanyesTrue for other, and for a campaign that has ended: remove it, but not change it.
offer.endedbooleanno—
offer.points_per_currency_unitnumberno—
offer.starts_atstringno—
offer.ends_atstringno—
offer.starts_onstringnoThe first day, in the store's time zone.
offer.ends_onstringnoThe last day, in the store's time zone.
offer.points_per_itemintegerno—
offer.product_idslist of stringno—
offer.channelobjectnoOrders from any of these count: a sales channel's source name, an app's id, or an order tag; at most 50 of each, and at least one list not empty.
offer.channel.source_nameslist of string, up to 100 characters, at least 1 characterno—
offer.channel.app_idslist of integer, at least 0no—
offer.channel.order_tagslist of string, up to 100 characters, at least 1 characterno—
store_keystringyes—
versionstring, matching ^[0-9a-f]{32}$yesThe programme's version after the change.
changedlist of stringyesThe settings that changed, in the engine's names. Empty when the change asked for what was already set.
as_paidobject, or nullyesWhat a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.
as_paid.earn_rate_per_major_unitnumber, at least 0yesPoints an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than "earn 0".
as_paid.earn_rates_per_major_unitlist of number, at least 0yesThe same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays.
as_paid.welcome_bonus_pointsinteger, at least 0yesThe joining bonus a new shopper is paid, once. 0 when the store pays none, whatever effective.welcomeBonusPoints says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's effective.bonusesInAppOnly), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in ways_to_earn says so.
as_paid.ways_to_earnlist of objectyeseffective.waysToEarn as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from earn_rate_per_major_unit, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.
as_paid.ways_to_earn[].idstringyes—
as_paid.ways_to_earn[].labelstringyes—
as_paid.ways_to_earn[].detailstringyes—
audit_idstring, or nullyes—

One shape: OfferPreview.

FieldTypeAlways thereWhat it is
objectalways offer_previewyes—
offerobject (ApiOffer)yesAn offer in the store's own days. campaign: extra points per unit between two days. product: points per item of chosen products. channel: extra points per unit on orders from chosen sources. other: set up another way; read-only here.
offer.idstringyes—
offer.kindone of campaign, product, channel, otheryes—
offer.reasonstringyesWhat the shopper is told the points are for.
offer.read_onlybooleanyesTrue for other, and for a campaign that has ended: remove it, but not change it.
offer.endedbooleanno—
offer.points_per_currency_unitnumberno—
offer.starts_atstringno—
offer.ends_atstringno—
offer.starts_onstringnoThe first day, in the store's time zone.
offer.ends_onstringnoThe last day, in the store's time zone.
offer.points_per_itemintegerno—
offer.product_idslist of stringno—
offer.channelobjectnoOrders from any of these count: a sales channel's source name, an app's id, or an order tag; at most 50 of each, and at least one list not empty.
offer.channel.source_nameslist of string, up to 100 characters, at least 1 characterno—
offer.channel.app_idslist of integer, at least 0no—
offer.channel.order_tagslist of string, up to 100 characters, at least 1 characterno—
dry_runalways trueyes—
outcomeone of would_apply, no_changeyes—
store_keystringyes—
changedlist of stringyesThe settings that would change, in the engine's names. What each moves from and to is before_after.
before_afterobject (PreviewBeforeAfter)yesEvery figure the change moves, before and after, in this API's own write field names, worked out from the programme as it would price orders before and after (the same way the storefront works them out). Only the ones that differ are here, so {} means none moves. points_per_currency_unit is the everyday rate an order pays (a running campaign or other offer left out); welcome_bonus_points and birthday_bonus_points are 0 when none is paid; point_value is how many points are worth one unit of the currency; ways_to_earn is the list as a shopper reads it. On a reward's call, reward is the reward itself: null before an add, null after a removal.
before_after.points_per_currency_unitobjectno—
before_after.points_per_currency_unit.beforenumber, at least 0yes—
before_after.points_per_currency_unit.afternumber, at least 0yes—
before_after.welcome_bonus_pointsobjectno—
before_after.welcome_bonus_points.beforeinteger, at least 0yes—
before_after.welcome_bonus_points.afterinteger, at least 0yes—
before_after.birthday_bonus_pointsobjectno—
before_after.birthday_bonus_points.beforeinteger, at least 0yes—
before_after.birthday_bonus_points.afterinteger, at least 0yes—
before_after.point_valueobjectno—
before_after.point_value.beforeobjectyes—
before_after.point_value.before.pointsinteger, at least 1yes—
before_after.point_value.before.amountalways 1.00yes—
before_after.point_value.afterobjectyes—
before_after.point_value.after.pointsinteger, at least 1yes—
before_after.point_value.after.amountalways 1.00yes—
before_after.timezoneobjectno—
before_after.timezone.beforestringyes—
before_after.timezone.afterstringyes—
before_after.ways_to_earnobjectno—
before_after.ways_to_earn.beforelist of objectyes—
before_after.ways_to_earn.before[].idstringyes—
before_after.ways_to_earn.before[].labelstringyes—
before_after.ways_to_earn.before[].detailstringyes—
before_after.ways_to_earn.afterlist of objectyes—
before_after.ways_to_earn.after[].idstringyes—
before_after.ways_to_earn.after[].labelstringyes—
before_after.ways_to_earn.after[].detailstringyes—
before_after.referralobjectno—
before_after.referral.beforeany, or null or objectyes—
before_after.referral.afterany, or null or objectyes—
before_after.spend_any_amountobjectno—
before_after.spend_any_amount.beforeany, or null or objectyes—
before_after.spend_any_amount.afterany, or null or objectyes—
before_after.rewardobjectno—
before_after.reward.beforeany, or null or object (ApiReward)yes—
before_after.reward.afterany, or null or object (ApiReward)yes—
currencystring, or nullyes—
exampleslist of objectyesWhat a plain order of each size earns now, before and after the change, under every offer running now.
examples[].order_amountstringyes—
examples[].points_beforeintegeryes—
examples[].points_afterintegeryes—
point_valueobjectyes—
point_value.points_per_currency_unit_beforeinteger, or nullyes—
point_value.points_per_currency_unit_afterinteger, or nullyes—
liabilityany, or null or object or objectyesWhen the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; unavailable when the account has more than one store.
version_afterstring, matching ^[0-9a-f]{32}$, or nullyesThe version the programme would have.
confirmationany, or null or objectyesFor a key tagged for an assistant: send token back as the Matrix-Confirmation header with the same request. Null when nothing would change.
audit_idstring, or nullyes—

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.

Example

export STORE='<key of a store from GET /stores>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme/offers" \
  -H "Authorization: Bearer $MATRIX_API_KEY" \
  -H "Idempotency-Key: offer-add-2026-09-16-0001" \
  -H "Content-Type: application/json" \
  -d '{"kind": "campaign", "reason": "Double points week", "points_per_currency_unit": 1, "starts_on": "2026-10-06", "ends_on": "2026-10-12"}'

The offer, added:

{
  "object": "offer",
  "offer": {
    "id": "campaign_mfk2q",
    "kind": "campaign",
    "reason": "Double points week",
    "read_only": false,
    "ended": false,
    "points_per_currency_unit": 1,
    "starts_at": "2026-10-05T23:00:00.000Z",
    "ends_at": "2026-10-12T23:00:00.000Z",
    "starts_on": "2026-10-06",
    "ends_on": "2026-10-12"
  },
  "store_key": "example-store",
  "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f4",
  "changed": [
    "rules"
  ],
  "as_paid": {
    "earn_rate_per_major_unit": 1,
    "earn_rates_per_major_unit": [
      1
    ],
    "welcome_bonus_points": 0,
    "ways_to_earn": []
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000016"
}

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

{
  "object": "offer_preview",
  "offer": {
    "id": "campaign_mfk2q",
    "kind": "campaign",
    "reason": "Double points week",
    "read_only": false,
    "ended": false,
    "points_per_currency_unit": 1,
    "starts_at": "2026-10-05T23:00:00.000Z",
    "ends_at": "2026-10-12T23:00:00.000Z",
    "starts_on": "2026-10-06",
    "ends_on": "2026-10-12"
  },
  "dry_run": true,
  "outcome": "would_apply",
  "store_key": "example-store",
  "changed": [
    "rules"
  ],
  "before_after": {},
  "currency": "GBP",
  "examples": [
    {
      "order_amount": "10.00",
      "points_before": 10,
      "points_after": 10
    },
    {
      "order_amount": "50.00",
      "points_before": 50,
      "points_after": 50
    },
    {
      "order_amount": "100.00",
      "points_before": 100,
      "points_after": 100
    }
  ],
  "point_value": {
    "points_per_currency_unit_before": 100,
    "points_per_currency_unit_after": 100
  },
  "liability": null,
  "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f4",
  "confirmation": {
    "token": "mct_EXAMPLE.EXAMPLE",
    "expires_at": "2026-09-16T10:10:00.000Z"
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000017"
}

Other answers

StatusWhat it meansError pages
400invalid_request: a parameter or the body could not be read; param names it.invalid_request
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not hold write_programme (required_scope says so). test_mode_key: a test key can preview a change and never make one. assistant_limit: the key is an assistant's (every Claude connection, and a key made for an assistant), the merchant has not lifted its fixed limits (/me key.limits_lifted; only a person can lift them, on the app's API keys screen), and the change moves the store's money further than one step. It is measured from the programme as it stood before the assistant's changes began, so steps add up; that starting point holds until assistant_window_hours pass with no assistant change, and a change by the owner to what the programme pays starts afresh (a new label or reason does not; nor does a change by an assistant key whose limits are lifted). What a point is worth, the currency and when points expire never change. What an order earns at most doubles: a new offer counts as paying every order, against what every order earned; and the everyday rate, which still prices some refunds (an API refund that names no order, and an order paid before amounts were recorded), is not lowered to fund an offer (then the figures are the everyday rate's own, least_allowed is the least it may be for every order to earn what it did, up to the field's two decimal places — null when no rate within its own step would do — and on an offer's route no param is named). Every other figure that pays shoppers at most doubles, one at zero stays there, and referrals that are off stay off. Spending any amount that is off stays off; while it is on, the fewest points one code may take and the smallest order it can be used on at most halve, and the most points at most double (a limit cannot be removed); switching it off is never limited. An offer may only narrow who and when it pays. A reward at most doubles what it gives for each point, keeps its kind, and its smallest order at most halves; a new reward stays within one step of a reward of its kind. The preview is refused the same way. problem says which limit; before, asked and most_allowed or least_allowed give the figure in the unit of param, and sending that limit back is accepted.forbidden_scope, test_mode_key, assistant_limit
404not_found: no such member on this account, or (cancelling a code) the member holds no code with that ref.not_found
409idempotency_key_in_use: a request with this key is still running (Retry-After). settings_changed: the programme changed after it was read, so nothing was changed. offer_read_only: the rule was set up in a way the API does not change; change it in the app. offer_ended: the campaign has ended, so it can only be removed.idempotency_key_in_use, settings_changed, offer_read_only, offer_ended
413payload_too_large: the body is over 4,096 bytes.payload_too_large
422idempotency_key_reused: this Idempotency-Key was used for a different request in the last 24 hours.idempotency_key_reused
428confirmation_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, confirmation_invalid
429rate_limited: too many requests for this key (Retry-After). programme_write_limit: this account has made 100 programme changes through the API in the last 24 hours (limit, used); nothing was changed.rate_limited, programme_write_limit
503change_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, access_not_recorded

Change an offer

POST /stores/{store_key}/programme/offers/{offer_id} — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme/offers/{offer_id}.

Needs: the write_programme permission (in the app: “Change your programme: the earn rate, the joining and birthday bonuses, what a point is worth, when points expire, the rewards and the offers. Every change is previewable and logged”).

Send only what changes; kind cannot change. A campaign's start or end is rebuilt only from a day sent, so a window set up to start at a time of day keeps that time until its day is changed. An ended campaign cannot be changed (409 offer_ended), and a rule set up another way is read-only (409 offer_read_only).

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—
offer_idpathyesstring, matching ^[a-z0-9_]{1,64}$An offer's id, as listOffers gives it.
Idempotency-Keyheaderyesstring, 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_runquerynobooleantrue 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-Confirmationheadernostring, up to 1,024 charactersThe 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.

FieldTypeRequiredWhat it is
kindone of campaign, product, channelno—
reasonstring, up to 200 characters, at least 1 characterno—
points_per_currency_unitnumber, at most 1,000noExtra points for every 1 of the store's currency: more than 0, at most 2 decimal places.
starts_onstring, matching ^\d{4}-\d{2}-\d{2}$noThe first day, in the store's time zone: today or later.
ends_onstring, matching ^\d{4}-\d{2}-\d{2}$noThe last day, in the store's time zone: not before starts_on, and not already past.
points_per_iteminteger, from 1 to 1,000,000no—
product_idslist of string or integer, at least 1noShopify product ids, at most 500: numbers, or gid://shopify/Product/…
channelobjectnoOrders from any of these count: a sales channel's source name, an app's id, or an order tag; at most 50 of each, and at least one list not empty.
channel.source_nameslist of string, up to 100 characters, at least 1 characterno—
channel.app_idslist of integer, at least 0no—
channel.order_tagslist of string, up to 100 characters, at least 1 characterno—
if_versionstring, matching ^[0-9a-f]{32}$noThe version from a programme read. If the programme has changed since, nothing is changed (409 settings_changed).

The answer

The change, or with dry_run=true the preview of it. A request already answered is answered again, byte for byte (Idempotency-Replayed).

One shape: OfferResult.

FieldTypeAlways thereWhat it is
objectalways offeryes—
offerobject (ApiOffer)yesAn offer in the store's own days. campaign: extra points per unit between two days. product: points per item of chosen products. channel: extra points per unit on orders from chosen sources. other: set up another way; read-only here.
offer.idstringyes—
offer.kindone of campaign, product, channel, otheryes—
offer.reasonstringyesWhat the shopper is told the points are for.
offer.read_onlybooleanyesTrue for other, and for a campaign that has ended: remove it, but not change it.
offer.endedbooleanno—
offer.points_per_currency_unitnumberno—
offer.starts_atstringno—
offer.ends_atstringno—
offer.starts_onstringnoThe first day, in the store's time zone.
offer.ends_onstringnoThe last day, in the store's time zone.
offer.points_per_itemintegerno—
offer.product_idslist of stringno—
offer.channelobjectnoOrders from any of these count: a sales channel's source name, an app's id, or an order tag; at most 50 of each, and at least one list not empty.
offer.channel.source_nameslist of string, up to 100 characters, at least 1 characterno—
offer.channel.app_idslist of integer, at least 0no—
offer.channel.order_tagslist of string, up to 100 characters, at least 1 characterno—
store_keystringyes—
versionstring, matching ^[0-9a-f]{32}$yesThe programme's version after the change.
changedlist of stringyesThe settings that changed, in the engine's names. Empty when the change asked for what was already set.
as_paidobject, or nullyesWhat a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.
as_paid.earn_rate_per_major_unitnumber, at least 0yesPoints an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than "earn 0".
as_paid.earn_rates_per_major_unitlist of number, at least 0yesThe same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays.
as_paid.welcome_bonus_pointsinteger, at least 0yesThe joining bonus a new shopper is paid, once. 0 when the store pays none, whatever effective.welcomeBonusPoints says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's effective.bonusesInAppOnly), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in ways_to_earn says so.
as_paid.ways_to_earnlist of objectyeseffective.waysToEarn as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from earn_rate_per_major_unit, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.
as_paid.ways_to_earn[].idstringyes—
as_paid.ways_to_earn[].labelstringyes—
as_paid.ways_to_earn[].detailstringyes—
audit_idstring, or nullyes—

One shape: OfferPreview.

FieldTypeAlways thereWhat it is
objectalways offer_previewyes—
offerobject (ApiOffer)yesAn offer in the store's own days. campaign: extra points per unit between two days. product: points per item of chosen products. channel: extra points per unit on orders from chosen sources. other: set up another way; read-only here.
offer.idstringyes—
offer.kindone of campaign, product, channel, otheryes—
offer.reasonstringyesWhat the shopper is told the points are for.
offer.read_onlybooleanyesTrue for other, and for a campaign that has ended: remove it, but not change it.
offer.endedbooleanno—
offer.points_per_currency_unitnumberno—
offer.starts_atstringno—
offer.ends_atstringno—
offer.starts_onstringnoThe first day, in the store's time zone.
offer.ends_onstringnoThe last day, in the store's time zone.
offer.points_per_itemintegerno—
offer.product_idslist of stringno—
offer.channelobjectnoOrders from any of these count: a sales channel's source name, an app's id, or an order tag; at most 50 of each, and at least one list not empty.
offer.channel.source_nameslist of string, up to 100 characters, at least 1 characterno—
offer.channel.app_idslist of integer, at least 0no—
offer.channel.order_tagslist of string, up to 100 characters, at least 1 characterno—
dry_runalways trueyes—
outcomeone of would_apply, no_changeyes—
store_keystringyes—
changedlist of stringyesThe settings that would change, in the engine's names. What each moves from and to is before_after.
before_afterobject (PreviewBeforeAfter)yesEvery figure the change moves, before and after, in this API's own write field names, worked out from the programme as it would price orders before and after (the same way the storefront works them out). Only the ones that differ are here, so {} means none moves. points_per_currency_unit is the everyday rate an order pays (a running campaign or other offer left out); welcome_bonus_points and birthday_bonus_points are 0 when none is paid; point_value is how many points are worth one unit of the currency; ways_to_earn is the list as a shopper reads it. On a reward's call, reward is the reward itself: null before an add, null after a removal.
before_after.points_per_currency_unitobjectno—
before_after.points_per_currency_unit.beforenumber, at least 0yes—
before_after.points_per_currency_unit.afternumber, at least 0yes—
before_after.welcome_bonus_pointsobjectno—
before_after.welcome_bonus_points.beforeinteger, at least 0yes—
before_after.welcome_bonus_points.afterinteger, at least 0yes—
before_after.birthday_bonus_pointsobjectno—
before_after.birthday_bonus_points.beforeinteger, at least 0yes—
before_after.birthday_bonus_points.afterinteger, at least 0yes—
before_after.point_valueobjectno—
before_after.point_value.beforeobjectyes—
before_after.point_value.before.pointsinteger, at least 1yes—
before_after.point_value.before.amountalways 1.00yes—
before_after.point_value.afterobjectyes—
before_after.point_value.after.pointsinteger, at least 1yes—
before_after.point_value.after.amountalways 1.00yes—
before_after.timezoneobjectno—
before_after.timezone.beforestringyes—
before_after.timezone.afterstringyes—
before_after.ways_to_earnobjectno—
before_after.ways_to_earn.beforelist of objectyes—
before_after.ways_to_earn.before[].idstringyes—
before_after.ways_to_earn.before[].labelstringyes—
before_after.ways_to_earn.before[].detailstringyes—
before_after.ways_to_earn.afterlist of objectyes—
before_after.ways_to_earn.after[].idstringyes—
before_after.ways_to_earn.after[].labelstringyes—
before_after.ways_to_earn.after[].detailstringyes—
before_after.referralobjectno—
before_after.referral.beforeany, or null or objectyes—
before_after.referral.afterany, or null or objectyes—
before_after.spend_any_amountobjectno—
before_after.spend_any_amount.beforeany, or null or objectyes—
before_after.spend_any_amount.afterany, or null or objectyes—
before_after.rewardobjectno—
before_after.reward.beforeany, or null or object (ApiReward)yes—
before_after.reward.afterany, or null or object (ApiReward)yes—
currencystring, or nullyes—
exampleslist of objectyesWhat a plain order of each size earns now, before and after the change, under every offer running now.
examples[].order_amountstringyes—
examples[].points_beforeintegeryes—
examples[].points_afterintegeryes—
point_valueobjectyes—
point_value.points_per_currency_unit_beforeinteger, or nullyes—
point_value.points_per_currency_unit_afterinteger, or nullyes—
liabilityany, or null or object or objectyesWhen the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; unavailable when the account has more than one store.
version_afterstring, matching ^[0-9a-f]{32}$, or nullyesThe version the programme would have.
confirmationany, or null or objectyesFor a key tagged for an assistant: send token back as the Matrix-Confirmation header with the same request. Null when nothing would change.
audit_idstring, or nullyes—

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.

Example

export STORE='<key of a store from GET /stores>'
export OFFER='<id of an offer from GET /stores/{store_key}/programme/offers>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme/offers/$OFFER" \
  -H "Authorization: Bearer $MATRIX_API_KEY" \
  -H "Idempotency-Key: offer-change-2026-09-16-0001" \
  -H "Content-Type: application/json" \
  -d '{"ends_on": "2026-10-19"}'

The offer, changed:

{
  "object": "offer",
  "offer": {
    "id": "campaign_mfk2q",
    "kind": "campaign",
    "reason": "Double points week",
    "read_only": false,
    "ended": false,
    "points_per_currency_unit": 1,
    "starts_at": "2026-10-05T23:00:00.000Z",
    "ends_at": "2026-10-19T23:00:00.000Z",
    "starts_on": "2026-10-06",
    "ends_on": "2026-10-19"
  },
  "store_key": "example-store",
  "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f5",
  "changed": [
    "rules"
  ],
  "as_paid": {
    "earn_rate_per_major_unit": 1,
    "earn_rates_per_major_unit": [
      1
    ],
    "welcome_bonus_points": 0,
    "ways_to_earn": []
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000018"
}

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

{
  "object": "offer_preview",
  "offer": {
    "id": "campaign_mfk2q",
    "kind": "campaign",
    "reason": "Double points week",
    "read_only": false,
    "ended": false,
    "points_per_currency_unit": 1,
    "starts_at": "2026-10-05T23:00:00.000Z",
    "ends_at": "2026-10-19T23:00:00.000Z",
    "starts_on": "2026-10-06",
    "ends_on": "2026-10-19"
  },
  "dry_run": true,
  "outcome": "would_apply",
  "store_key": "example-store",
  "changed": [
    "rules"
  ],
  "before_after": {},
  "currency": "GBP",
  "examples": [
    {
      "order_amount": "10.00",
      "points_before": 10,
      "points_after": 10
    },
    {
      "order_amount": "50.00",
      "points_before": 50,
      "points_after": 50
    },
    {
      "order_amount": "100.00",
      "points_before": 100,
      "points_after": 100
    }
  ],
  "point_value": {
    "points_per_currency_unit_before": 100,
    "points_per_currency_unit_after": 100
  },
  "liability": null,
  "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f5",
  "confirmation": {
    "token": "mct_EXAMPLE.EXAMPLE",
    "expires_at": "2026-09-16T10:10:00.000Z"
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000019"
}

Other answers

StatusWhat it meansError pages
400invalid_request: a parameter or the body could not be read; param names it.invalid_request
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not hold write_programme (required_scope says so). test_mode_key: a test key can preview a change and never make one. assistant_limit: the key is an assistant's (every Claude connection, and a key made for an assistant), the merchant has not lifted its fixed limits (/me key.limits_lifted; only a person can lift them, on the app's API keys screen), and the change moves the store's money further than one step. It is measured from the programme as it stood before the assistant's changes began, so steps add up; that starting point holds until assistant_window_hours pass with no assistant change, and a change by the owner to what the programme pays starts afresh (a new label or reason does not; nor does a change by an assistant key whose limits are lifted). What a point is worth, the currency and when points expire never change. What an order earns at most doubles: a new offer counts as paying every order, against what every order earned; and the everyday rate, which still prices some refunds (an API refund that names no order, and an order paid before amounts were recorded), is not lowered to fund an offer (then the figures are the everyday rate's own, least_allowed is the least it may be for every order to earn what it did, up to the field's two decimal places — null when no rate within its own step would do — and on an offer's route no param is named). Every other figure that pays shoppers at most doubles, one at zero stays there, and referrals that are off stay off. Spending any amount that is off stays off; while it is on, the fewest points one code may take and the smallest order it can be used on at most halve, and the most points at most double (a limit cannot be removed); switching it off is never limited. An offer may only narrow who and when it pays. A reward at most doubles what it gives for each point, keeps its kind, and its smallest order at most halves; a new reward stays within one step of a reward of its kind. The preview is refused the same way. problem says which limit; before, asked and most_allowed or least_allowed give the figure in the unit of param, and sending that limit back is accepted.forbidden_scope, test_mode_key, assistant_limit
404not_found: no such member on this account, or (cancelling a code) the member holds no code with that ref.not_found
409idempotency_key_in_use: a request with this key is still running (Retry-After). settings_changed: the programme changed after it was read, so nothing was changed. offer_read_only: the rule was set up in a way the API does not change; change it in the app. offer_ended: the campaign has ended, so it can only be removed.idempotency_key_in_use, settings_changed, offer_read_only, offer_ended
413payload_too_large: the body is over 4,096 bytes.payload_too_large
422idempotency_key_reused: this Idempotency-Key was used for a different request in the last 24 hours.idempotency_key_reused
428confirmation_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, confirmation_invalid
429rate_limited: too many requests for this key (Retry-After). programme_write_limit: this account has made 100 programme changes through the API in the last 24 hours (limit, used); nothing was changed.rate_limited, programme_write_limit
503change_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, access_not_recorded

Remove an offer

POST /stores/{store_key}/programme/offers/{offer_id}/remove — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme/offers/{offer_id}/remove.

Needs: the write_programme permission (in the app: “Change your programme: the earn rate, the joining and birthday bonuses, what a point is worth, when points expire, the rewards and the offers. Every change is previewable and logged”).

Any campaign, product or channel offer, ended or not. Orders it already paid keep their points. A rule set up another way is read-only (409 offer_read_only).

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—
offer_idpathyesstring, matching ^[a-z0-9_]{1,64}$An offer's id, as listOffers gives it.
Idempotency-Keyheaderyesstring, 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_runquerynobooleantrue 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-Confirmationheadernostring, up to 1,024 charactersThe 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.

FieldTypeRequiredWhat it is
if_versionstring, matching ^[0-9a-f]{32}$noThe version from a programme read. If the programme has changed since, nothing is changed (409 settings_changed).

The answer

The change, or with dry_run=true the preview of it. A request already answered is answered again, byte for byte (Idempotency-Replayed).

One shape: OfferRemoved.

FieldTypeAlways thereWhat it is
objectalways offer_removedyes—
offer_idstringyes—
store_keystringyes—
versionstring, matching ^[0-9a-f]{32}$yesThe programme's version after the change.
changedlist of stringyesThe settings that changed, in the engine's names. Empty when the change asked for what was already set.
as_paidobject, or nullyesWhat a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.
as_paid.earn_rate_per_major_unitnumber, at least 0yesPoints an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than "earn 0".
as_paid.earn_rates_per_major_unitlist of number, at least 0yesThe same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays.
as_paid.welcome_bonus_pointsinteger, at least 0yesThe joining bonus a new shopper is paid, once. 0 when the store pays none, whatever effective.welcomeBonusPoints says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's effective.bonusesInAppOnly), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in ways_to_earn says so.
as_paid.ways_to_earnlist of objectyeseffective.waysToEarn as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from earn_rate_per_major_unit, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.
as_paid.ways_to_earn[].idstringyes—
as_paid.ways_to_earn[].labelstringyes—
as_paid.ways_to_earn[].detailstringyes—
audit_idstring, or nullyes—

One shape: OfferRemovedPreview.

FieldTypeAlways thereWhat it is
objectalways offer_removed_previewyes—
offer_idstringyes—
dry_runalways trueyes—
outcomeone of would_apply, no_changeyes—
store_keystringyes—
changedlist of stringyesThe settings that would change, in the engine's names. What each moves from and to is before_after.
before_afterobject (PreviewBeforeAfter)yesEvery figure the change moves, before and after, in this API's own write field names, worked out from the programme as it would price orders before and after (the same way the storefront works them out). Only the ones that differ are here, so {} means none moves. points_per_currency_unit is the everyday rate an order pays (a running campaign or other offer left out); welcome_bonus_points and birthday_bonus_points are 0 when none is paid; point_value is how many points are worth one unit of the currency; ways_to_earn is the list as a shopper reads it. On a reward's call, reward is the reward itself: null before an add, null after a removal.
before_after.points_per_currency_unitobjectno—
before_after.points_per_currency_unit.beforenumber, at least 0yes—
before_after.points_per_currency_unit.afternumber, at least 0yes—
before_after.welcome_bonus_pointsobjectno—
before_after.welcome_bonus_points.beforeinteger, at least 0yes—
before_after.welcome_bonus_points.afterinteger, at least 0yes—
before_after.birthday_bonus_pointsobjectno—
before_after.birthday_bonus_points.beforeinteger, at least 0yes—
before_after.birthday_bonus_points.afterinteger, at least 0yes—
before_after.point_valueobjectno—
before_after.point_value.beforeobjectyes—
before_after.point_value.before.pointsinteger, at least 1yes—
before_after.point_value.before.amountalways 1.00yes—
before_after.point_value.afterobjectyes—
before_after.point_value.after.pointsinteger, at least 1yes—
before_after.point_value.after.amountalways 1.00yes—
before_after.timezoneobjectno—
before_after.timezone.beforestringyes—
before_after.timezone.afterstringyes—
before_after.ways_to_earnobjectno—
before_after.ways_to_earn.beforelist of objectyes—
before_after.ways_to_earn.before[].idstringyes—
before_after.ways_to_earn.before[].labelstringyes—
before_after.ways_to_earn.before[].detailstringyes—
before_after.ways_to_earn.afterlist of objectyes—
before_after.ways_to_earn.after[].idstringyes—
before_after.ways_to_earn.after[].labelstringyes—
before_after.ways_to_earn.after[].detailstringyes—
before_after.referralobjectno—
before_after.referral.beforeany, or null or objectyes—
before_after.referral.afterany, or null or objectyes—
before_after.spend_any_amountobjectno—
before_after.spend_any_amount.beforeany, or null or objectyes—
before_after.spend_any_amount.afterany, or null or objectyes—
before_after.rewardobjectno—
before_after.reward.beforeany, or null or object (ApiReward)yes—
before_after.reward.afterany, or null or object (ApiReward)yes—
currencystring, or nullyes—
exampleslist of objectyesWhat a plain order of each size earns now, before and after the change, under every offer running now.
examples[].order_amountstringyes—
examples[].points_beforeintegeryes—
examples[].points_afterintegeryes—
point_valueobjectyes—
point_value.points_per_currency_unit_beforeinteger, or nullyes—
point_value.points_per_currency_unit_afterinteger, or nullyes—
liabilityany, or null or object or objectyesWhen the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; unavailable when the account has more than one store.
version_afterstring, matching ^[0-9a-f]{32}$, or nullyesThe version the programme would have.
confirmationany, or null or objectyesFor a key tagged for an assistant: send token back as the Matrix-Confirmation header with the same request. Null when nothing would change.
audit_idstring, or nullyes—

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.

Example

export STORE='<key of a store from GET /stores>'
export OFFER='<id of an offer from GET /stores/{store_key}/programme/offers>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme/offers/$OFFER/remove" \
  -H "Authorization: Bearer $MATRIX_API_KEY" \
  -H "Idempotency-Key: offer-remove-2026-09-16-0001" \
  -H "Content-Type: application/json" \
  -d '{}'

The offer, removed:

{
  "object": "offer_removed",
  "offer_id": "campaign_mfk2q",
  "store_key": "example-store",
  "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f6",
  "changed": [
    "rules"
  ],
  "as_paid": {
    "earn_rate_per_major_unit": 1,
    "earn_rates_per_major_unit": [
      1
    ],
    "welcome_bonus_points": 0,
    "ways_to_earn": []
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000001a"
}

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

{
  "object": "offer_removed_preview",
  "offer_id": "campaign_mfk2q",
  "dry_run": true,
  "outcome": "would_apply",
  "store_key": "example-store",
  "changed": [
    "rules"
  ],
  "before_after": {},
  "currency": "GBP",
  "examples": [
    {
      "order_amount": "10.00",
      "points_before": 10,
      "points_after": 10
    },
    {
      "order_amount": "50.00",
      "points_before": 50,
      "points_after": 50
    },
    {
      "order_amount": "100.00",
      "points_before": 100,
      "points_after": 100
    }
  ],
  "point_value": {
    "points_per_currency_unit_before": 100,
    "points_per_currency_unit_after": 100
  },
  "liability": null,
  "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f6",
  "confirmation": {
    "token": "mct_EXAMPLE.EXAMPLE",
    "expires_at": "2026-09-16T10:10:00.000Z"
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000001b"
}

Other answers

StatusWhat it meansError pages
400invalid_request: a parameter or the body could not be read; param names it.invalid_request
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not hold write_programme (required_scope says so). test_mode_key: a test key can preview a change and never make one. assistant_limit: the key is an assistant's (every Claude connection, and a key made for an assistant), the merchant has not lifted its fixed limits (/me key.limits_lifted; only a person can lift them, on the app's API keys screen), and the change moves the store's money further than one step. It is measured from the programme as it stood before the assistant's changes began, so steps add up; that starting point holds until assistant_window_hours pass with no assistant change, and a change by the owner to what the programme pays starts afresh (a new label or reason does not; nor does a change by an assistant key whose limits are lifted). What a point is worth, the currency and when points expire never change. What an order earns at most doubles: a new offer counts as paying every order, against what every order earned; and the everyday rate, which still prices some refunds (an API refund that names no order, and an order paid before amounts were recorded), is not lowered to fund an offer (then the figures are the everyday rate's own, least_allowed is the least it may be for every order to earn what it did, up to the field's two decimal places — null when no rate within its own step would do — and on an offer's route no param is named). Every other figure that pays shoppers at most doubles, one at zero stays there, and referrals that are off stay off. Spending any amount that is off stays off; while it is on, the fewest points one code may take and the smallest order it can be used on at most halve, and the most points at most double (a limit cannot be removed); switching it off is never limited. An offer may only narrow who and when it pays. A reward at most doubles what it gives for each point, keeps its kind, and its smallest order at most halves; a new reward stays within one step of a reward of its kind. The preview is refused the same way. problem says which limit; before, asked and most_allowed or least_allowed give the figure in the unit of param, and sending that limit back is accepted.forbidden_scope, test_mode_key, assistant_limit
404not_found: no such member on this account, or (cancelling a code) the member holds no code with that ref.not_found
409idempotency_key_in_use: a request with this key is still running (Retry-After). settings_changed: the programme changed after it was read, so nothing was changed. offer_read_only: the rule was set up in a way the API does not change; change it in the app. offer_ended: the campaign has ended, so it can only be removed.idempotency_key_in_use, settings_changed, offer_read_only, offer_ended
413payload_too_large: the body is over 4,096 bytes.payload_too_large
422idempotency_key_reused: this Idempotency-Key was used for a different request in the last 24 hours.idempotency_key_reused
428confirmation_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, confirmation_invalid
429rate_limited: too many requests for this key (Retry-After). programme_write_limit: this account has made 100 programme changes through the API in the last 24 hours (limit, used); nothing was changed.rate_limited, programme_write_limit
503change_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, access_not_recorded

The earn rules in force at this store

GET /stores/{store_key}/programme/rules — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/programme/rules.

Needs: the read_programme permission (in the app: “Your programme settings — rates, rewards, rules”). A key with write_programme can call it too.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—

The answer

The effective rules list.

FieldTypeAlways thereWhat it is
datalist of object, openyes—
has_morebooleanyes—
next_cursorstring, or nullyes—
versionstring, matching ^[0-9a-f]{32}$yesWhich stored programme this is: it changes whenever the store's programme settings change (not its look or expiry), so two reads with the same version saw the same programme. Send it as if_version with a programme change (POST /stores/{store_key}/programme) and the change is refused, 409 settings_changed, if the programme has changed since you read it. The embedded admin's save checks the same version.

An open object has no fixed set of fields: it is sent as the platform holds it, in its own field names.

Headers on the answer: Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. What each header means.

Example

export STORE='<key of a store from GET /stores>'
curl "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/programme/rules" \
  -H "Authorization: Bearer $MATRIX_API_KEY"

The answer:

{
  "data": [
    {
      "id": "base_earn",
      "award": {
        "type": "perMajorUnit",
        "pointsPerUnit": 1
      },
      "event": "order_paid",
      "reason": "Points earned on your order",
      "condition": {
        "type": "always"
      }
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "version": "3f1c9a2b7d4e5f60718293a4b5c6d7e8"
}

Other answers

StatusWhat it meansError pages
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not carry the scope this call needs; required_scope says which.forbidden_scope
404not_found: no such resource on this account — indistinguishable from one that does not exist at all.not_found
429rate_limited: too many requests for this key; bucket says which allowance. Retry-After says how long to wait.rate_limited

A store's design: the colours, words and look of the rewards panel, the product-page line and the account page

GET /stores/{store_key}/design — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/design.

Needs: the read_programme permission (in the app: “Your programme settings — rates, rewards, rules”). A key with write_programme can call it too.

The settings the Design screen in your Shopify admin saves, every one present (anything unusable in what is stored reads as its default, as the shopper surfaces read it), and the design's version for a change to send back as if_version.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—

The answer

The design.

FieldTypeAlways thereWhat it is
objectalways designyes—
store_keystringyes—
versionstring, matching ^[0-9a-f]{32}$yesWhich design this is: what a design change sends back as if_version.
designobject (Design)yesThe Design screen's settings: the colours, words, panel and product-line look, the account page's layout and the terms link, in the names the programme read serves as appearance.
design.programmeNamestring, up to 30 charactersyesWhat the programme is called: the page title and the panel heading. Never empty: an empty one reads as "Rewards".
design.pointsNamestring, up to 16 charactersyesWhat one point is called, like "star". Never empty.
design.pointsNamePluralstring, up to 16 charactersyesThe plural, like "stars". Never empty.
design.headingstring, up to 60 charactersyesThe panel heading when it should differ from programmeName; empty uses programmeName.
design.storeNamestring, up to 60 charactersyesThe store's name as shoppers and merchants read it, wherever they are told which shop: the account page, the shared-balance line, "where else you can spend" and the Matrix Rewards website. Empty uses the name Shopify has for the shop now (GET /stores/{store_key}/brand, from_shopify.name), else the name it was installed with. Control characters are removed and the ends trimmed.
design.signedOutPromptstring, up to 140 charactersyesWhat a shopper who is not signed in reads.
design.rewardsHeadingstring, up to 60 charactersyesThe rewards section heading; empty shows the section with none.
design.earnHeadingstring, up to 60 charactersyesThe ways-to-earn section heading; empty shows the section with none.
design.activityHeadingstring, up to 60 charactersyesThe recent-activity section heading; empty shows the section with none.
design.accentstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe one colour, like "#4f46e5" (stored in lower case); null follows the theme.
design.cornerRadiusinteger, from 0 to 24, or nullyesCorner rounding in pixels; null follows the theme, and 0 is square.
design.densityone of comfortable, compactyes—
design.textScaleone of small, normal, largeyesThe panel's text size beside the text around it.
design.fontFamilyone of theme, system, serif, monoyesThe panel's typeface: the theme's, or one of three named stacks.
design.showRewardsbooleanyes—
design.showWaysToEarnbooleanyes—
design.showActivitybooleanyes—
design.showExpirybooleanyes—
design.showSharedWalletbooleanyesThe shared-balance line, at an account with more than one store.
design.showProductPointsbooleanyesThe "Points on this product" line on product pages, everywhere at once.
design.showRewardsSitebooleanyesA link on the account page to the Matrix Rewards website, where a shopper signs in with their email to see the points they hold at each store that uses Matrix Rewards. Off unless the merchant switches it on.
design.termsUrlstring, or nullyesA full https:// address for the programme's terms; null hides the link.
design.otherShopsobjectyesThe shops sharing this store's balance, by store key: whether the account page lists each under "Where else you can spend your points", and the web address its link opens (null: the shop's own Shopify address). A shop that does not share the balance is never listed, whatever is set here. A shop with no entry is listed. Keys are store keys (lowercase letters, digits and hyphens); at most 50.
design.productWidgetobjectyesThe product-page line's look.
design.productWidget.variantone of boxed, tinted, plain, badgeyes—
design.productWidget.iconone of none, star, gift, coinyes—
design.productWidget.linesobjectyesWhich of its lines show (each also needs the placed block's own checkbox).
design.productWidget.lines.earnbooleanyes—
design.productWidget.lines.redeembooleanyes—
design.productWidget.lines.signinbooleanyes—
design.productWidget.wordsobjectyesIts sentences; empty is the default in the shopper's language. [points], [reward] and [amount] are filled in on the page.
design.productWidget.words.earnstring, up to 80 charactersyes—
design.productWidget.words.ratestring, up to 80 charactersyes—
design.productWidget.words.readystring, up to 80 charactersyes—
design.productWidget.words.gapstring, up to 80 charactersyes—
design.productWidget.words.signinstring, up to 80 charactersyes—
design.accountPageobjectyesThe Rewards page in the shopper's Shopify account, in Shopify's own named values: that page takes the store's checkout and accounts branding, so no colour or font is set here.
design.accountPage.widthone of base, largeyes—
design.accountPage.cardsone of outlined, shaded, plainyes—
design.accountPage.cornersone of none, small, base, largeyes—
design.accountPage.buttonone of primary, secondaryyes—
design.accountPage.progressbooleanyesA progress bar towards the next reward.
design.accountPage.orderlist of one of codes, rewards, ways, activity, birthday, referralyesThe sections in order, each once.
design.panelobjectyesThe storefront rewards panel's look.
design.panel.variantone of card, minimal, boldyes—
design.panel.surfacestring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesA background colour for the panel; null follows the theme.
design.panel.layoutone of grid, listyes—
design.panel.wordsobjectyesThe words on its two links; empty is the default in the shopper's language.
design.panel.words.signInstring, up to 40 charactersyes—
design.panel.words.usestring, up to 40 charactersyes—
design.brandobject (BrandDesign)yesThe store's branding on the Matrix Rewards website (app.matrixrewards.co.uk): its logos, cover picture, colours and slogan. Each null uses what Shopify has for the shop. Never reaches the storefront or the account page.
design.brand.logostring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesThe store's logo. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
design.brand.squareLogostring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesThe logo for square places (the store's tile). null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
design.brand.coverImagestring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesA wide picture for the store's card. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
design.brand.colourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe brand colour, or null for Shopify's primary brand colour.
design.brand.textColourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe text colour on it, or null for Shopify's, else worked out. Used only where it is readable on the colour (the WCAG AA contrast for normal text); otherwise black or white is used.
design.brand.sloganstring, up to 80 characters, or nullyesA line under the store's name, or null for Shopify's slogan; empty shows none. Up to 80 characters.
design.brand.fontstring, or nullyesThe typeface of the store's name and points on the website, from a fixed list of fonts we host (no family of your own: a page only ever sets one of these). null uses ours, playfair-display; system is the shopper's device font. Shopify has no font setting, so there is nothing of Shopify's to fall back on. One of playfair-display, fraunces, lora, inter, dm-sans, nunito, space-grotesk, system.

Headers on the answer: Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. What each header means.

Example

export STORE='<key of a store from GET /stores>'
curl "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/design" \
  -H "Authorization: Bearer $MATRIX_API_KEY"

The answer:

{
  "object": "design",
  "store_key": "example-store",
  "version": "7c1e2d3f4a5b60718293a4b5c6d7e8f0",
  "design": {
    "programmeName": "Rewards",
    "pointsName": "point",
    "pointsNamePlural": "points",
    "heading": "",
    "storeName": "",
    "signedOutPrompt": "Sign in to see your points.",
    "rewardsHeading": "What you can get",
    "earnHeading": "Ways to earn",
    "activityHeading": "Recent activity",
    "accent": null,
    "cornerRadius": null,
    "density": "comfortable",
    "textScale": "normal",
    "fontFamily": "theme",
    "showRewards": true,
    "showWaysToEarn": true,
    "showActivity": true,
    "showExpiry": true,
    "showSharedWallet": true,
    "showProductPoints": true,
    "showRewardsSite": false,
    "termsUrl": null,
    "otherShops": {},
    "productWidget": {
      "variant": "boxed",
      "icon": "none",
      "lines": {
        "earn": true,
        "redeem": true,
        "signin": true
      },
      "words": {
        "earn": "",
        "rate": "",
        "ready": "",
        "gap": "",
        "signin": ""
      }
    },
    "accountPage": {
      "width": "base",
      "cards": "outlined",
      "corners": "base",
      "button": "primary",
      "progress": true,
      "order": [
        "codes",
        "rewards",
        "ways",
        "activity",
        "birthday",
        "referral"
      ]
    },
    "panel": {
      "variant": "card",
      "surface": null,
      "layout": "grid",
      "words": {
        "signIn": "",
        "use": ""
      }
    },
    "brand": {
      "logo": null,
      "squareLogo": null,
      "coverImage": null,
      "colour": null,
      "textColour": null,
      "slogan": null,
      "font": null
    }
  }
}

Other answers

StatusWhat it meansError pages
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not carry the scope this call needs; required_scope says which.forbidden_scope
404not_found: no such resource on this account — indistinguishable from one that does not exist at all.not_found
429rate_limited: too many requests for this key; bucket says which allowance. Retry-After says how long to wait.rate_limited

Change the design: colours, words, the panel and product-line look, the account page layout, the terms link

POST /stores/{store_key}/design — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/design.

Needs: the write_programme permission (in the app: “Change your programme: the earn rate, the joining and birthday bonuses, what a point is worth, when points expire, the rewards and the offers. Every change is previewable and logged”).

The Design screen's save, through a key: each setting is checked exactly as that screen's save checks it, and a refusal names the setting (param) in the screen's own words. Only the settings sent change. The change is laid over one reading of the design, so a change made elsewhere in between is refused (409 settings_changed), never undone; a change to the programme's rates in between is not. The design it replaces is kept as the Design screen's one step of undo. A preview (dry_run=true) shows each setting that would change, before and after. Asking for the design already set changes nothing. Counts toward the 100 programme changes per account through the API in any 24 hours, and is written to the audit log in the same transaction as the change.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—
Idempotency-Keyheaderyesstring, 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_runquerynobooleantrue 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-Confirmationheadernostring, up to 1,024 charactersThe 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.

FieldTypeRequiredWhat it is
programmeNamestring, up to 30 charactersnoWhat the programme is called: the page title and the panel heading. Never empty: an empty one reads as "Rewards".
pointsNamestring, up to 16 charactersnoWhat one point is called, like "star". Never empty.
pointsNamePluralstring, up to 16 charactersnoThe plural, like "stars". Never empty.
headingstring, up to 60 charactersnoThe panel heading when it should differ from programmeName; empty uses programmeName.
storeNamestring, up to 60 charactersnoThe store's name as shoppers and merchants read it, wherever they are told which shop: the account page, the shared-balance line, "where else you can spend" and the Matrix Rewards website. Empty uses the name Shopify has for the shop now (GET /stores/{store_key}/brand, from_shopify.name), else the name it was installed with. Control characters are removed and the ends trimmed.
signedOutPromptstring, up to 140 charactersnoWhat a shopper who is not signed in reads.
rewardsHeadingstring, up to 60 charactersnoThe rewards section heading; empty shows the section with none.
earnHeadingstring, up to 60 charactersnoThe ways-to-earn section heading; empty shows the section with none.
activityHeadingstring, up to 60 charactersnoThe recent-activity section heading; empty shows the section with none.
accentstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullnoThe one colour, like "#4f46e5" (stored in lower case); null follows the theme.
cornerRadiusinteger, from 0 to 24, or nullnoCorner rounding in pixels; null follows the theme, and 0 is square.
densityone of comfortable, compactno—
textScaleone of small, normal, largenoThe panel's text size beside the text around it.
fontFamilyone of theme, system, serif, mononoThe panel's typeface: the theme's, or one of three named stacks.
showRewardsbooleanno—
showWaysToEarnbooleanno—
showActivitybooleanno—
showExpirybooleanno—
showSharedWalletbooleannoThe shared-balance line, at an account with more than one store.
showProductPointsbooleannoThe "Points on this product" line on product pages, everywhere at once.
showRewardsSitebooleannoA link on the account page to the Matrix Rewards website, where a shopper signs in with their email to see the points they hold at each store that uses Matrix Rewards. Off unless the merchant switches it on.
termsUrlstring, or nullnoA full https:// address for the programme's terms; null or "" hides the link.
otherShopsobjectnoThe shops sharing this store's balance, by store key: whether the account page lists each under "Where else you can spend your points", and the web address its link opens (null: the shop's own Shopify address). A shop that does not share the balance is never listed, whatever is set here. A shop with no entry is listed. Sent entries are laid over the stored ones; "" or null for the address uses the Shopify address. Keys are store keys (lowercase letters, digits and hyphens); at most 50.
productWidgetobjectnoThe product-page line's look.
productWidget.variantone of boxed, tinted, plain, badgeno—
productWidget.iconone of none, star, gift, coinno—
productWidget.linesobjectnoWhich of its lines show (each also needs the placed block's own checkbox).
productWidget.lines.earnbooleanno—
productWidget.lines.redeembooleanno—
productWidget.lines.signinbooleanno—
productWidget.wordsobjectnoIts sentences; empty is the default in the shopper's language. [points], [reward] and [amount] are filled in on the page.
productWidget.words.earnstring, up to 80 charactersno—
productWidget.words.ratestring, up to 80 charactersno—
productWidget.words.readystring, up to 80 charactersno—
productWidget.words.gapstring, up to 80 charactersno—
productWidget.words.signinstring, up to 80 charactersno—
accountPageobjectnoThe Rewards page in the shopper's Shopify account, in Shopify's own named values: that page takes the store's checkout and accounts branding, so no colour or font is set here.
accountPage.widthone of base, largeno—
accountPage.cardsone of outlined, shaded, plainno—
accountPage.cornersone of none, small, base, largeno—
accountPage.buttonone of primary, secondaryno—
accountPage.progressbooleannoA progress bar towards the next reward.
accountPage.orderlist of one of codes, rewards, ways, activity, birthday, referralnoThe sections in order, each once.
panelobjectnoThe storefront rewards panel's look.
panel.variantone of card, minimal, boldno—
panel.surfacestring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullnoA background colour for the panel; null follows the theme.
panel.layoutone of grid, listno—
panel.wordsobjectnoThe words on its two links; empty is the default in the shopper's language.
panel.words.signInstring, up to 40 charactersno—
panel.words.usestring, up to 40 charactersno—
if_versionstring, matching ^[0-9a-f]{32}$noThe version from a design read. If the design has changed since, nothing is changed (409 settings_changed).
brandobjectnoThe branding settings to change, one at a time: { "brand": { "logo": "img_…" } } keeps the rest.
brand.logostring, matching ^(none\|img_[0-9a-f]{32})$, or nullnoThe store's logo. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
brand.squareLogostring, matching ^(none\|img_[0-9a-f]{32})$, or nullnoThe logo for square places (the store's tile). null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
brand.coverImagestring, matching ^(none\|img_[0-9a-f]{32})$, or nullnoA wide picture for the store's card. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
brand.colourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullnoThe brand colour, or null for Shopify's primary brand colour.
brand.textColourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullnoThe text colour on it, or null for Shopify's, else worked out. Used only where it is readable on the colour (the WCAG AA contrast for normal text); otherwise black or white is used.
brand.sloganstring, up to 80 characters, or nullnoA line under the store's name, or null for Shopify's slogan; empty shows none. Up to 80 characters.
brand.fontstring, or nullnoThe typeface of the store's name and points on the website, from a fixed list of fonts we host (no family of your own: a page only ever sets one of these). null uses ours, playfair-display; system is the shopper's device font. Shopify has no font setting, so there is nothing of Shopify's to fall back on. One of playfair-display, fraunces, lora, inter, dm-sans, nunito, space-grotesk, system.

The answer

The change, or with dry_run=true the preview of it. A request already answered is answered again, byte for byte (Idempotency-Replayed).

One shape: DesignChange.

FieldTypeAlways thereWhat it is
objectalways design_changeyes—
changedlist of one of programmeName, pointsName, pointsNamePlural, heading, storeName, signedOutPrompt, rewardsHeading, earnHeading, activityHeading, accent, cornerRadius, density, textScale, fontFamily, showRewards, showWaysToEarn, showActivity, showExpiry, showSharedWallet, showProductPoints, showRewardsSite, termsUrl, otherShops, productWidget, accountPage, panelyesThe settings that changed (a group by its own name). Empty when the change asked for the design already set.
designobject (Design)yesThe Design screen's settings: the colours, words, panel and product-line look, the account page's layout and the terms link, in the names the programme read serves as appearance.
design.programmeNamestring, up to 30 charactersyesWhat the programme is called: the page title and the panel heading. Never empty: an empty one reads as "Rewards".
design.pointsNamestring, up to 16 charactersyesWhat one point is called, like "star". Never empty.
design.pointsNamePluralstring, up to 16 charactersyesThe plural, like "stars". Never empty.
design.headingstring, up to 60 charactersyesThe panel heading when it should differ from programmeName; empty uses programmeName.
design.storeNamestring, up to 60 charactersyesThe store's name as shoppers and merchants read it, wherever they are told which shop: the account page, the shared-balance line, "where else you can spend" and the Matrix Rewards website. Empty uses the name Shopify has for the shop now (GET /stores/{store_key}/brand, from_shopify.name), else the name it was installed with. Control characters are removed and the ends trimmed.
design.signedOutPromptstring, up to 140 charactersyesWhat a shopper who is not signed in reads.
design.rewardsHeadingstring, up to 60 charactersyesThe rewards section heading; empty shows the section with none.
design.earnHeadingstring, up to 60 charactersyesThe ways-to-earn section heading; empty shows the section with none.
design.activityHeadingstring, up to 60 charactersyesThe recent-activity section heading; empty shows the section with none.
design.accentstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe one colour, like "#4f46e5" (stored in lower case); null follows the theme.
design.cornerRadiusinteger, from 0 to 24, or nullyesCorner rounding in pixels; null follows the theme, and 0 is square.
design.densityone of comfortable, compactyes—
design.textScaleone of small, normal, largeyesThe panel's text size beside the text around it.
design.fontFamilyone of theme, system, serif, monoyesThe panel's typeface: the theme's, or one of three named stacks.
design.showRewardsbooleanyes—
design.showWaysToEarnbooleanyes—
design.showActivitybooleanyes—
design.showExpirybooleanyes—
design.showSharedWalletbooleanyesThe shared-balance line, at an account with more than one store.
design.showProductPointsbooleanyesThe "Points on this product" line on product pages, everywhere at once.
design.showRewardsSitebooleanyesA link on the account page to the Matrix Rewards website, where a shopper signs in with their email to see the points they hold at each store that uses Matrix Rewards. Off unless the merchant switches it on.
design.termsUrlstring, or nullyesA full https:// address for the programme's terms; null hides the link.
design.otherShopsobjectyesThe shops sharing this store's balance, by store key: whether the account page lists each under "Where else you can spend your points", and the web address its link opens (null: the shop's own Shopify address). A shop that does not share the balance is never listed, whatever is set here. A shop with no entry is listed. Keys are store keys (lowercase letters, digits and hyphens); at most 50.
design.productWidgetobjectyesThe product-page line's look.
design.productWidget.variantone of boxed, tinted, plain, badgeyes—
design.productWidget.iconone of none, star, gift, coinyes—
design.productWidget.linesobjectyesWhich of its lines show (each also needs the placed block's own checkbox).
design.productWidget.lines.earnbooleanyes—
design.productWidget.lines.redeembooleanyes—
design.productWidget.lines.signinbooleanyes—
design.productWidget.wordsobjectyesIts sentences; empty is the default in the shopper's language. [points], [reward] and [amount] are filled in on the page.
design.productWidget.words.earnstring, up to 80 charactersyes—
design.productWidget.words.ratestring, up to 80 charactersyes—
design.productWidget.words.readystring, up to 80 charactersyes—
design.productWidget.words.gapstring, up to 80 charactersyes—
design.productWidget.words.signinstring, up to 80 charactersyes—
design.accountPageobjectyesThe Rewards page in the shopper's Shopify account, in Shopify's own named values: that page takes the store's checkout and accounts branding, so no colour or font is set here.
design.accountPage.widthone of base, largeyes—
design.accountPage.cardsone of outlined, shaded, plainyes—
design.accountPage.cornersone of none, small, base, largeyes—
design.accountPage.buttonone of primary, secondaryyes—
design.accountPage.progressbooleanyesA progress bar towards the next reward.
design.accountPage.orderlist of one of codes, rewards, ways, activity, birthday, referralyesThe sections in order, each once.
design.panelobjectyesThe storefront rewards panel's look.
design.panel.variantone of card, minimal, boldyes—
design.panel.surfacestring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesA background colour for the panel; null follows the theme.
design.panel.layoutone of grid, listyes—
design.panel.wordsobjectyesThe words on its two links; empty is the default in the shopper's language.
design.panel.words.signInstring, up to 40 charactersyes—
design.panel.words.usestring, up to 40 charactersyes—
design.brandobject (BrandDesign)yesThe store's branding on the Matrix Rewards website (app.matrixrewards.co.uk): its logos, cover picture, colours and slogan. Each null uses what Shopify has for the shop. Never reaches the storefront or the account page.
design.brand.logostring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesThe store's logo. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
design.brand.squareLogostring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesThe logo for square places (the store's tile). null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
design.brand.coverImagestring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesA wide picture for the store's card. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
design.brand.colourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe brand colour, or null for Shopify's primary brand colour.
design.brand.textColourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe text colour on it, or null for Shopify's, else worked out. Used only where it is readable on the colour (the WCAG AA contrast for normal text); otherwise black or white is used.
design.brand.sloganstring, up to 80 characters, or nullyesA line under the store's name, or null for Shopify's slogan; empty shows none. Up to 80 characters.
design.brand.fontstring, or nullyesThe typeface of the store's name and points on the website, from a fixed list of fonts we host (no family of your own: a page only ever sets one of these). null uses ours, playfair-display; system is the shopper's device font. Shopify has no font setting, so there is nothing of Shopify's to fall back on. One of playfair-display, fraunces, lora, inter, dm-sans, nunito, space-grotesk, system.
store_keystringyes—
versionstring, matching ^[0-9a-f]{32}$yesThe design's version after the change.
audit_idstring, or nullyes—

One shape: DesignChangePreview.

FieldTypeAlways thereWhat it is
objectalways design_change_previewyes—
dry_runalways trueyes—
outcomeone of would_apply, no_changeyes—
store_keystringyes—
changedlist of one of programmeName, pointsName, pointsNamePlural, heading, storeName, signedOutPrompt, rewardsHeading, earnHeading, activityHeading, accent, cornerRadius, density, textScale, fontFamily, showRewards, showWaysToEarn, showActivity, showExpiry, showSharedWallet, showProductPoints, showRewardsSite, termsUrl, otherShops, productWidget, accountPage, panelyesThe settings that would change.
before_afterobjectyesEach setting that would change, whole (a group as a group), before and after. {} when none would.
before_after.programmeNameobjectno—
before_after.programmeName.beforestring, up to 30 charactersyes—
before_after.programmeName.afterstring, up to 30 charactersyes—
before_after.pointsNameobjectno—
before_after.pointsName.beforestring, up to 16 charactersyes—
before_after.pointsName.afterstring, up to 16 charactersyes—
before_after.pointsNamePluralobjectno—
before_after.pointsNamePlural.beforestring, up to 16 charactersyes—
before_after.pointsNamePlural.afterstring, up to 16 charactersyes—
before_after.headingobjectno—
before_after.heading.beforestring, up to 60 charactersyes—
before_after.heading.afterstring, up to 60 charactersyes—
before_after.storeNameobjectno—
before_after.storeName.beforestring, up to 60 charactersyes—
before_after.storeName.afterstring, up to 60 charactersyes—
before_after.signedOutPromptobjectno—
before_after.signedOutPrompt.beforestring, up to 140 charactersyes—
before_after.signedOutPrompt.afterstring, up to 140 charactersyes—
before_after.rewardsHeadingobjectno—
before_after.rewardsHeading.beforestring, up to 60 charactersyes—
before_after.rewardsHeading.afterstring, up to 60 charactersyes—
before_after.earnHeadingobjectno—
before_after.earnHeading.beforestring, up to 60 charactersyes—
before_after.earnHeading.afterstring, up to 60 charactersyes—
before_after.activityHeadingobjectno—
before_after.activityHeading.beforestring, up to 60 charactersyes—
before_after.activityHeading.afterstring, up to 60 charactersyes—
before_after.accentobjectno—
before_after.accent.beforestring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyes—
before_after.accent.afterstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyes—
before_after.cornerRadiusobjectno—
before_after.cornerRadius.beforeinteger, from 0 to 24, or nullyes—
before_after.cornerRadius.afterinteger, from 0 to 24, or nullyes—
before_after.densityobjectno—
before_after.density.beforeone of comfortable, compactyes—
before_after.density.afterone of comfortable, compactyes—
before_after.textScaleobjectno—
before_after.textScale.beforeone of small, normal, largeyes—
before_after.textScale.afterone of small, normal, largeyes—
before_after.fontFamilyobjectno—
before_after.fontFamily.beforeone of theme, system, serif, monoyes—
before_after.fontFamily.afterone of theme, system, serif, monoyes—
before_after.showRewardsobjectno—
before_after.showRewards.beforebooleanyes—
before_after.showRewards.afterbooleanyes—
before_after.showWaysToEarnobjectno—
before_after.showWaysToEarn.beforebooleanyes—
before_after.showWaysToEarn.afterbooleanyes—
before_after.showActivityobjectno—
before_after.showActivity.beforebooleanyes—
before_after.showActivity.afterbooleanyes—
before_after.showExpiryobjectno—
before_after.showExpiry.beforebooleanyes—
before_after.showExpiry.afterbooleanyes—
before_after.showSharedWalletobjectno—
before_after.showSharedWallet.beforebooleanyes—
before_after.showSharedWallet.afterbooleanyes—
before_after.showProductPointsobjectno—
before_after.showProductPoints.beforebooleanyes—
before_after.showProductPoints.afterbooleanyes—
before_after.showRewardsSiteobjectno—
before_after.showRewardsSite.beforebooleanyes—
before_after.showRewardsSite.afterbooleanyes—
before_after.termsUrlobjectno—
before_after.termsUrl.beforestring, or nullyes—
before_after.termsUrl.afterstring, or nullyes—
before_after.otherShopsobjectno—
before_after.otherShops.beforeobjectyesThe shops sharing this store's balance, by store key: whether the account page lists each under "Where else you can spend your points", and the web address its link opens (null: the shop's own Shopify address). A shop that does not share the balance is never listed, whatever is set here. A shop with no entry is listed. Keys are store keys (lowercase letters, digits and hyphens); at most 50.
before_after.otherShops.afterobjectyesThe shops sharing this store's balance, by store key: whether the account page lists each under "Where else you can spend your points", and the web address its link opens (null: the shop's own Shopify address). A shop that does not share the balance is never listed, whatever is set here. A shop with no entry is listed. Keys are store keys (lowercase letters, digits and hyphens); at most 50.
before_after.productWidgetobjectno—
before_after.productWidget.beforeobjectyes—
before_after.productWidget.before.variantone of boxed, tinted, plain, badgeyes—
before_after.productWidget.before.iconone of none, star, gift, coinyes—
before_after.productWidget.before.linesobjectyesWhich of its lines show (each also needs the placed block's own checkbox).
before_after.productWidget.before.lines.earnbooleanyes—
before_after.productWidget.before.lines.redeembooleanyes—
before_after.productWidget.before.lines.signinbooleanyes—
before_after.productWidget.before.wordsobjectyesIts sentences; empty is the default in the shopper's language. [points], [reward] and [amount] are filled in on the page.
before_after.productWidget.before.words.earnstring, up to 80 charactersyes—
before_after.productWidget.before.words.ratestring, up to 80 charactersyes—
before_after.productWidget.before.words.readystring, up to 80 charactersyes—
before_after.productWidget.before.words.gapstring, up to 80 charactersyes—
before_after.productWidget.before.words.signinstring, up to 80 charactersyes—
before_after.productWidget.afterobjectyes—
before_after.productWidget.after.variantone of boxed, tinted, plain, badgeyes—
before_after.productWidget.after.iconone of none, star, gift, coinyes—
before_after.productWidget.after.linesobjectyesWhich of its lines show (each also needs the placed block's own checkbox).
before_after.productWidget.after.lines.earnbooleanyes—
before_after.productWidget.after.lines.redeembooleanyes—
before_after.productWidget.after.lines.signinbooleanyes—
before_after.productWidget.after.wordsobjectyesIts sentences; empty is the default in the shopper's language. [points], [reward] and [amount] are filled in on the page.
before_after.productWidget.after.words.earnstring, up to 80 charactersyes—
before_after.productWidget.after.words.ratestring, up to 80 charactersyes—
before_after.productWidget.after.words.readystring, up to 80 charactersyes—
before_after.productWidget.after.words.gapstring, up to 80 charactersyes—
before_after.productWidget.after.words.signinstring, up to 80 charactersyes—
before_after.accountPageobjectno—
before_after.accountPage.beforeobjectyes—
before_after.accountPage.before.widthone of base, largeyes—
before_after.accountPage.before.cardsone of outlined, shaded, plainyes—
before_after.accountPage.before.cornersone of none, small, base, largeyes—
before_after.accountPage.before.buttonone of primary, secondaryyes—
before_after.accountPage.before.progressbooleanyesA progress bar towards the next reward.
before_after.accountPage.before.orderlist of one of codes, rewards, ways, activity, birthday, referralyesThe sections in order, each once.
before_after.accountPage.afterobjectyes—
before_after.accountPage.after.widthone of base, largeyes—
before_after.accountPage.after.cardsone of outlined, shaded, plainyes—
before_after.accountPage.after.cornersone of none, small, base, largeyes—
before_after.accountPage.after.buttonone of primary, secondaryyes—
before_after.accountPage.after.progressbooleanyesA progress bar towards the next reward.
before_after.accountPage.after.orderlist of one of codes, rewards, ways, activity, birthday, referralyesThe sections in order, each once.
before_after.panelobjectno—
before_after.panel.beforeobjectyes—
before_after.panel.before.variantone of card, minimal, boldyes—
before_after.panel.before.surfacestring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesA background colour for the panel; null follows the theme.
before_after.panel.before.layoutone of grid, listyes—
before_after.panel.before.wordsobjectyesThe words on its two links; empty is the default in the shopper's language.
before_after.panel.before.words.signInstring, up to 40 charactersyes—
before_after.panel.before.words.usestring, up to 40 charactersyes—
before_after.panel.afterobjectyes—
before_after.panel.after.variantone of card, minimal, boldyes—
before_after.panel.after.surfacestring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesA background colour for the panel; null follows the theme.
before_after.panel.after.layoutone of grid, listyes—
before_after.panel.after.wordsobjectyesThe words on its two links; empty is the default in the shopper's language.
before_after.panel.after.words.signInstring, up to 40 charactersyes—
before_after.panel.after.words.usestring, up to 40 charactersyes—
before_after.brandobjectno—
before_after.brand.beforeobject (BrandDesign)yesThe store's branding on the Matrix Rewards website (app.matrixrewards.co.uk): its logos, cover picture, colours and slogan. Each null uses what Shopify has for the shop. Never reaches the storefront or the account page.
before_after.brand.before.logostring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesThe store's logo. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
before_after.brand.before.squareLogostring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesThe logo for square places (the store's tile). null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
before_after.brand.before.coverImagestring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesA wide picture for the store's card. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
before_after.brand.before.colourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe brand colour, or null for Shopify's primary brand colour.
before_after.brand.before.textColourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe text colour on it, or null for Shopify's, else worked out. Used only where it is readable on the colour (the WCAG AA contrast for normal text); otherwise black or white is used.
before_after.brand.before.sloganstring, up to 80 characters, or nullyesA line under the store's name, or null for Shopify's slogan; empty shows none. Up to 80 characters.
before_after.brand.before.fontstring, or nullyesThe typeface of the store's name and points on the website, from a fixed list of fonts we host (no family of your own: a page only ever sets one of these). null uses ours, playfair-display; system is the shopper's device font. Shopify has no font setting, so there is nothing of Shopify's to fall back on. One of playfair-display, fraunces, lora, inter, dm-sans, nunito, space-grotesk, system.
before_after.brand.afterobject (BrandDesign)yesThe store's branding on the Matrix Rewards website (app.matrixrewards.co.uk): its logos, cover picture, colours and slogan. Each null uses what Shopify has for the shop. Never reaches the storefront or the account page.
before_after.brand.after.logostring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesThe store's logo. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
before_after.brand.after.squareLogostring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesThe logo for square places (the store's tile). null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
before_after.brand.after.coverImagestring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesA wide picture for the store's card. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
before_after.brand.after.colourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe brand colour, or null for Shopify's primary brand colour.
before_after.brand.after.textColourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe text colour on it, or null for Shopify's, else worked out. Used only where it is readable on the colour (the WCAG AA contrast for normal text); otherwise black or white is used.
before_after.brand.after.sloganstring, up to 80 characters, or nullyesA line under the store's name, or null for Shopify's slogan; empty shows none. Up to 80 characters.
before_after.brand.after.fontstring, or nullyesThe typeface of the store's name and points on the website, from a fixed list of fonts we host (no family of your own: a page only ever sets one of these). null uses ours, playfair-display; system is the shopper's device font. Shopify has no font setting, so there is nothing of Shopify's to fall back on. One of playfair-display, fraunces, lora, inter, dm-sans, nunito, space-grotesk, system.
version_afterstring, matching ^[0-9a-f]{32}$, or nullyesThe version the design would have.
confirmationany, or null or objectyesFor a key tagged for an assistant: send token back as the Matrix-Confirmation header with the same request. Null when nothing would change.
audit_idstring, or nullyes—

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.

Example

export STORE='<key of a store from GET /stores>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/design" \
  -H "Authorization: Bearer $MATRIX_API_KEY" \
  -H "Idempotency-Key: design-2026-09-16-0001" \
  -H "Content-Type: application/json" \
  -d '{"accent": "#4f46e5", "panel": {"variant": "bold"}}'

The change, made:

{
  "object": "design_change",
  "changed": [
    "accent",
    "panel"
  ],
  "design": {
    "programmeName": "Rewards",
    "pointsName": "point",
    "pointsNamePlural": "points",
    "heading": "",
    "storeName": "",
    "signedOutPrompt": "Sign in to see your points.",
    "rewardsHeading": "What you can get",
    "earnHeading": "Ways to earn",
    "activityHeading": "Recent activity",
    "accent": "#4f46e5",
    "cornerRadius": null,
    "density": "comfortable",
    "textScale": "normal",
    "fontFamily": "theme",
    "showRewards": true,
    "showWaysToEarn": true,
    "showActivity": true,
    "showExpiry": true,
    "showSharedWallet": true,
    "showProductPoints": true,
    "showRewardsSite": false,
    "termsUrl": null,
    "otherShops": {},
    "productWidget": {
      "variant": "boxed",
      "icon": "none",
      "lines": {
        "earn": true,
        "redeem": true,
        "signin": true
      },
      "words": {
        "earn": "",
        "rate": "",
        "ready": "",
        "gap": "",
        "signin": ""
      }
    },
    "accountPage": {
      "width": "base",
      "cards": "outlined",
      "corners": "base",
      "button": "primary",
      "progress": true,
      "order": [
        "codes",
        "rewards",
        "ways",
        "activity",
        "birthday",
        "referral"
      ]
    },
    "panel": {
      "variant": "bold",
      "surface": null,
      "layout": "grid",
      "words": {
        "signIn": "",
        "use": ""
      }
    },
    "brand": {
      "logo": null,
      "squareLogo": null,
      "coverImage": null,
      "colour": null,
      "textColour": null,
      "slogan": null,
      "font": null
    }
  },
  "store_key": "example-store",
  "version": "7c1e2d3f4a5b60718293a4b5c6d7e8f1",
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000020"
}

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

{
  "object": "design_change_preview",
  "dry_run": true,
  "outcome": "would_apply",
  "store_key": "example-store",
  "changed": [
    "accent",
    "panel"
  ],
  "before_after": {
    "accent": {
      "before": null,
      "after": "#4f46e5"
    },
    "panel": {
      "before": {
        "variant": "card",
        "surface": null,
        "layout": "grid",
        "words": {
          "signIn": "",
          "use": ""
        }
      },
      "after": {
        "variant": "bold",
        "surface": null,
        "layout": "grid",
        "words": {
          "signIn": "",
          "use": ""
        }
      }
    }
  },
  "version_after": "7c1e2d3f4a5b60718293a4b5c6d7e8f1",
  "confirmation": {
    "token": "mct_EXAMPLE.EXAMPLE",
    "expires_at": "2026-09-16T10:10:00.000Z"
  },
  "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000021"
}

Other answers

StatusWhat it meansError pages
400invalid_request: a parameter or the body could not be read; param names it.invalid_request
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not hold write_programme (required_scope says so). test_mode_key: a test key can preview a change and never make one.forbidden_scope, test_mode_key
404not_found: no store with that key on this account.not_found
409idempotency_key_in_use: a request with this key is still running (Retry-After). settings_changed: the design changed after it was read — by if_version, or by the reading this change was built over — so nothing was changed; read it again and resend.idempotency_key_in_use, settings_changed
413payload_too_large: the body is over 4,096 bytes.payload_too_large
422idempotency_key_reused: this Idempotency-Key was used for a different request in the last 24 hours.idempotency_key_reused
428confirmation_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, confirmation_invalid
429rate_limited: too many requests for this key (Retry-After). programme_write_limit: this account has made 100 programme changes through the API in the last 24 hours (limit, used); nothing was changed.rate_limited, programme_write_limit
503change_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, access_not_recorded

A store's name and branding as the Matrix Rewards website shows them, and what Shopify has

GET /stores/{store_key}/brand — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/brand.

Needs: the read_programme permission (in the app: “Your programme settings — rates, rewards, rules”). A key with write_programme can call it too.

The finished name, logos, cover, colours and slogan (each the design's setting, else Shopify's), what Shopify has for the shop, and the design's branding settings. Change them with POST /stores/{store_key}/design (storeName, brand).

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—

The answer

The branding.

FieldTypeAlways thereWhat it is
objectone of store_brandingyes—
store_keystringyes—
namestringyes—
sloganstring, or nullyes—
logo_urlstring, or nullyes—
square_logo_urlstring, or nullyes—
cover_image_urlstring, or nullyes—
colourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyes—
text_colourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesReadable on colour (WCAG AA contrast for normal text).
fontstringyesThe typeface the website draws the name and points in: the design's choice, else playfair-display. One of playfair-display, fraunces, lora, inter, dm-sans, nunito, space-grotesk, system.
from_shopifyobject (ShopifyBrand)yesWhat Shopify has for the shop: its name and its brand settings (Settings → Brand), read without a key and kept, again at most once a day. Picture addresses are Shopify's CDN only; anything else Shopify sent is left out.
from_shopify.namestring, or nullyes—
from_shopify.sloganstring, or nullyes—
from_shopify.shortDescriptionstring, or nullyes—
from_shopify.logostring, or nullyes—
from_shopify.squareLogostring, or nullyes—
from_shopify.coverImagestring, or nullyes—
from_shopify.colourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyes—
from_shopify.textColourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyes—
shopify_read_atstring, or nullyesWhen Shopify was last read for this store; null before the first read.
designobject (BrandDesign)yesThe store's branding on the Matrix Rewards website (app.matrixrewards.co.uk): its logos, cover picture, colours and slogan. Each null uses what Shopify has for the shop. Never reaches the storefront or the account page.
design.logostring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesThe store's logo. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
design.squareLogostring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesThe logo for square places (the store's tile). null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
design.coverImagestring, matching ^(none\|img_[0-9a-f]{32})$, or nullyesA wide picture for the store's card. null uses the picture Shopify has (Settings → Brand); none shows none; img_… is a picture uploaded to this store (POST /stores/{store_key}/images). Another store's picture is refused.
design.colourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe brand colour, or null for Shopify's primary brand colour.
design.textColourstring, matching ^#(?:[0-9a-fA-F]{3}\|[0-9a-fA-F]{6})$, or nullyesThe text colour on it, or null for Shopify's, else worked out. Used only where it is readable on the colour (the WCAG AA contrast for normal text); otherwise black or white is used.
design.sloganstring, up to 80 characters, or nullyesA line under the store's name, or null for Shopify's slogan; empty shows none. Up to 80 characters.
design.fontstring, or nullyesThe typeface of the store's name and points on the website, from a fixed list of fonts we host (no family of your own: a page only ever sets one of these). null uses ours, playfair-display; system is the shopper's device font. Shopify has no font setting, so there is nothing of Shopify's to fall back on. One of playfair-display, fraunces, lora, inter, dm-sans, nunito, space-grotesk, system.

Headers on the answer: Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. What each header means.

Example

export STORE='<key of a store from GET /stores>'
curl "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/brand" \
  -H "Authorization: Bearer $MATRIX_API_KEY"

The answer:

{
  "object": "store_branding",
  "store_key": "example-store",
  "name": "Example Store",
  "slogan": "Small batch, made well",
  "logo_url": "https://app.matrixrewards.co.uk/brand-images/img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
  "square_logo_url": "https://cdn.shopify.com/s/files/1/0000/0001/files/square.png",
  "cover_image_url": null,
  "colour": "#1a5c5c",
  "text_colour": "#ffffff",
  "font": "playfair-display",
  "from_shopify": {
    "name": "Example Store",
    "slogan": "Small batch, made well",
    "shortDescription": null,
    "logo": null,
    "squareLogo": "https://cdn.shopify.com/s/files/1/0000/0001/files/square.png",
    "coverImage": null,
    "colour": "#1a5c5c",
    "textColour": "#ffffff"
  },
  "shopify_read_at": "2026-09-16T10:00:00Z",
  "design": {
    "logo": "img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
    "squareLogo": null,
    "coverImage": null,
    "colour": null,
    "textColour": null,
    "slogan": null,
    "font": null
  }
}

Other answers

StatusWhat it meansError pages
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not carry the scope this call needs; required_scope says which.forbidden_scope
404not_found: no such resource on this account — indistinguishable from one that does not exist at all.not_found
429rate_limited: too many requests for this key; bucket says which allowance. Retry-After says how long to wait.rate_limited

The pictures uploaded to a store, newest first

GET /stores/{store_key}/images — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/images.

Needs: the read_programme permission (in the app: “Your programme settings — rates, rewards, rules”). A key with write_programme can call it too.

Every picture kept for the store (at most 20), for the design's brand settings to name.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—

The answer

The pictures.

FieldTypeAlways thereWhat it is
datalist of object (BrandImage)yes—
data[].objectone of brand_imageyes—
data[].idstring, matching ^img_[0-9a-f]{32}$yesWhat the design's brand.logo, brand.squareLogo or brand.coverImage takes.
data[].store_keystringyes—
data[].content_typeone of image/png, image/jpeg, image/webpyes—
data[].widthintegeryes—
data[].heightintegeryes—
data[].bytesintegeryes—
data[].sourceone of upload, shopify_filesyes—
data[].urlstringyesWhere the picture is served, to anyone, for good.
data[].created_atstringyes—
has_moreone of falseyes—
next_cursorany, or nullyes—

Headers on the answer: Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. What each header means.

Example

export STORE='<key of a store from GET /stores>'
curl "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/images" \
  -H "Authorization: Bearer $MATRIX_API_KEY"

The answer:

{
  "data": [
    {
      "object": "brand_image",
      "id": "img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
      "store_key": "example-store",
      "content_type": "image/png",
      "width": 512,
      "height": 512,
      "bytes": 24816,
      "source": "upload",
      "url": "https://app.matrixrewards.co.uk/brand-images/img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
      "created_at": "2026-09-16T10:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Other answers

StatusWhat it meansError pages
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not carry the scope this call needs; required_scope says which.forbidden_scope
404not_found: no such resource on this account — indistinguishable from one that does not exist at all.not_found
429rate_limited: too many requests for this key; bucket says which allowance. Retry-After says how long to wait.rate_limited

Upload a picture for the store's branding (a logo, a square logo, a cover)

POST /stores/{store_key}/images — the full address is https://app.matrixrewards.co.uk/api/2026-10/stores/{store_key}/images.

Needs: the write_programme permission (in the app: “Change your programme: the earn rate, the joining and birthday bonuses, what a point is worth, when points expire, the rewards and the offers. Every change is previewable and logged”).

Send the file itself (Content-Type: image/png, image/jpeg or image/webp, up to 524,288 bytes, 16 to 4096 pixels a side), or JSON naming a picture in your Shopify Files (source_url, https://cdn.shopify.com/ only). The type is read from the file's own bytes; SVG and anything else are refused. The same picture twice is the same picture (created: false). Then name its id in the design: POST /stores/{store_key}/design with { "brand": { "logo": "img_…" } }. A store keeps at most 20; the oldest the design does not use makes room.

Parameters

NameInRequiredTypeWhat it is
store_keypathyesstring—

Request body

Sent as application/json.

FieldTypeRequiredWhat it is
source_urlstringyesA picture in your Shopify Files: an https://cdn.shopify.com/ address. No other address is fetched.

The answer

Kept (created: true), or the same picture already kept (created: false).

FieldTypeAlways thereWhat it is
objectone of brand_imageyes—
idstring, matching ^img_[0-9a-f]{32}$yesWhat the design's brand.logo, brand.squareLogo or brand.coverImage takes.
store_keystringyes—
content_typeone of image/png, image/jpeg, image/webpyes—
widthintegeryes—
heightintegeryes—
bytesintegeryes—
sourceone of upload, shopify_filesyes—
urlstringyesWhere the picture is served, to anyone, for good.
created_atstringyes—
createdbooleanyesFalse when the same picture was already kept for the store (nothing new was stored).

Headers on the answer: Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. What each header means.

Example

export STORE='<key of a store from GET /stores>'
curl -X POST "https://app.matrixrewards.co.uk/api/2026-10/stores/$STORE/images" \
  -H "Authorization: Bearer $MATRIX_API_KEY"

The answer:

{
  "object": "brand_image",
  "id": "img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
  "store_key": "example-store",
  "content_type": "image/png",
  "width": 512,
  "height": 512,
  "bytes": 24816,
  "source": "upload",
  "url": "https://app.matrixrewards.co.uk/brand-images/img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
  "created_at": "2026-09-16T10:00:00Z",
  "created": true
}

Other answers

StatusWhat it meansError pages
400invalid_request: a parameter or the body could not be read; param names it.invalid_request
401unauthenticated: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.unauthenticated
403forbidden_scope: the key does not hold write_programme (required_scope says so). test_mode_key: a test key keeps nothing.forbidden_scope, test_mode_key
404not_found: no such resource on this account — indistinguishable from one that does not exist at all.not_found
409too_many_images: the store keeps at most 20 pictures and the design uses every one.too_many_images
413payload_too_large: the picture is over 524,288 bytes, or the JSON body over 4,096 bytes.payload_too_large
422invalid_image: not a PNG, JPEG or WebP by its own bytes, or under 16 or over 4096 pixels on a side; or sent with another Content-Type.invalid_image
429rate_limited: too many requests for this key; bucket says which allowance. Retry-After says how long to wait.rate_limited
502shopify_file_unavailable: Shopify did not send the picture at source_url. Nothing was kept; try again, or upload the file.shopify_file_unavailable
503change_not_recorded: the picture was kept but the audit log could not be written. Send it again: the same picture is not kept twice.change_not_recorded

Last updated: 14 October 2026