Skip to content

Add an earning rule

POST /program/earn-rules
Scopeprogram:write
Customer session tokenNot accepted: call from your server with an API key
Rate limit classwrite_shopify (limits)
Idempotency keyRequired (idempotency)
MCP toolcharm_create_earning_rule

Adds a way to earn to the live program, as the admin’s “Add rule” does: fields you omit take the catalog defaults, a field outside its range is a 422 with its path. Call with dry_run: true first and show the merchant the diff and summary before repeating the call without it. The widget and hub pick the change up immediately. The referral rule is a program switch and cannot be created here.

FieldTypeDescription
typestringThe earning rule type, one of GET /program/rule-types?kind=earning.
fieldsobjectThe earning rule’s configuration, keyed as GET /program/rule-types?kind=earning documents for the type. Omitted keys take the catalog defaults; unknown keys and out-of-range values are a 422 with the field path. (optional)
enabledbooleanDefaults to true. (optional)
titlestring or nullThe merchant’s own title shown to shoppers instead of the catalog’s, in the shop’s primary language. Null clears it. (optional, nullable)
title_translationsobjectThe title in other languages: { "sk": "Recenzia s fotkou" }. Keys are the shop’s published languages (GET /program/rule-types lists them under languages); null removes one, languages you omit keep theirs. A value for the primary language sets title. (optional)
dry_runbooleanPreview only: validate, resolve references and return the diff and summary, but write nothing. Costs no idempotency key and is limited as a read. Call with this first, show the merchant, then repeat without it. (optional)
if_revisionintegerThe program revision you last read (from GET /program or a previous write). When the program changed since, the write is refused with 409 revision_conflict instead of overwriting a concurrent admin edit. (optional)

Returns ProgramWriteResult.

FieldTypeDescription
dry_runbooleanTrue when nothing was written: the diff shows what the same call without dry_run would do.
revisionintegerThe program revision after the write (unchanged on a dry run). Send it back as if_revision on the next write.
operationstring: create, update, delete, none
diffarray of FieldChange
summarystring or nullThe admin’s one-line summary of the rule after the change. (nullable)
earning_ruleEarningRule or null(optional, nullable)
rewardReward or null(optional, nullable)
tierTier or null(optional, nullable)
programProgram or null(optional, nullable)
positionintegerTier writes: the tier’s place in the ladder, 0 = the entry tier. (optional)
tiers_enabledbooleanTier writes: whether VIP tiers are live. While they are off, adding or removing a tier moves nobody. (optional)
member_impactTierMemberImpact or null(optional, nullable)
referencesarray of objectRemoving a tier: rules and live campaigns that name it. They keep working, but their per-tier part no longer applies to anyone. (optional)
{
"dry_run": false,
"revision": 1,
"operation": "create",
"diff": [
{
"path": "path",
"before": null,
"after": null
}
],
"summary": "summary",
"earning_rule": {
"id": "id",
"type": "earn",
"enabled": true,
"label": "Points per order",
"points": 120,
"points_per_currency": 120,
"limit": 1,
"title": "title",
"title_translations": {},
"fields": {},
"summary": "summary"
},
"reward": {
"id": "id",
"type": "earn",
"enabled": true,
"name": "Gold",
"points_cost": 120,
"discount_type": "percentage",
"discount_value": 1,
"code_valid_days": 1,
"title": "title",
"title_translations": {},
"fields": {},
"summary": "summary"
},
"tier": {
"id": "id",
"name": "Gold",
"handle": "handle",
"name_translations": {},
"threshold": 1,
"points_multiplier": 120,
"combine": "any",
"criteria": [
null
],
"benefits": [
null
]
},
"program": {
"enabled": true,
"program_name": "Rewards Club",
"points_name": "points",
"points_expiry": {
"enabled": null,
"months": null
},
"reward_expiry": {
"enabled": null,
"code_valid_days": null
},
"eligibility": {
"customer_audience": null,
"excluded_customer_tags": null,
"excluded_product_tags": null
},
"referral": {
"enabled": true,
"code_prefix": "REF",
"discount_type": "percentage",
"discount_value": 1,
"minimum_purchase": 1
},
"memberships_enabled": false,
"tiers_enabled": false,
"earning_rules": [
null
],
"redemption_rules": [
null
],
"revision": 1,
"updated_at": "2026-10-01T09:30:00.000Z"
},
"position": 1,
"tiers_enabled": false,
"member_impact": {
"members_evaluated": 1,
"promoted": 1,
"demoted": 1,
"replaced": 1,
"gift_codes": 1,
"gifts": [
{
"tier_id": null,
"tier_name": null,
"label": null,
"codes": null
}
],
"per_tier": [
{
"tier_id": null,
"name": null,
"members": null
}
],
"members_in_removed_tier": 1,
"truncated": false
},
"references": [
{
"kind": "earning_rule",
"id": "id",
"label": "Points per order"
}
]
}
Terminal window
curl -X POST "https://charm.appfleece.app/api/v1/program/earn-rules" \
-H "Authorization: Bearer chrm_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"type":"earn"}'

unauthorized · invalid_token · insufficient_scope · access_required · validation_error · rate_limited · idempotency_conflict · shopify_unavailable

Every error body carries code, message, request_id and a doc_url pointing at the matching entry in the error catalog.