Add an earning rule
POST /program/earn-rules| Scope | program:write |
| Customer session token | Not accepted: call from your server with an API key |
| Rate limit class | write_shopify (limits) |
| Idempotency key | Required (idempotency) |
| MCP tool | charm_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.
Request body
Section titled “Request body”| Field | Type | Description |
|---|---|---|
type | string | The earning rule type, one of GET /program/rule-types?kind=earning. |
fields | object | The 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) |
enabled | boolean | Defaults to true. (optional) |
title | string or null | The merchant’s own title shown to shoppers instead of the catalog’s, in the shop’s primary language. Null clears it. (optional, nullable) |
title_translations | object | The 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_run | boolean | Preview 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_revision | integer | The 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) |
Response 201
Section titled “Response 201”Returns ProgramWriteResult.
| Field | Type | Description |
|---|---|---|
dry_run | boolean | True when nothing was written: the diff shows what the same call without dry_run would do. |
revision | integer | The program revision after the write (unchanged on a dry run). Send it back as if_revision on the next write. |
operation | string: create, update, delete, none | |
diff | array of FieldChange | |
summary | string or null | The admin’s one-line summary of the rule after the change. (nullable) |
earning_rule | EarningRule or null | (optional, nullable) |
reward | Reward or null | (optional, nullable) |
tier | Tier or null | (optional, nullable) |
program | Program or null | (optional, nullable) |
position | integer | Tier writes: the tier’s place in the ladder, 0 = the entry tier. (optional) |
tiers_enabled | boolean | Tier writes: whether VIP tiers are live. While they are off, adding or removing a tier moves nobody. (optional) |
member_impact | TierMemberImpact or null | (optional, nullable) |
references | array of object | Removing 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" } ]}Example
Section titled “Example”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"}'Errors
Section titled “Errors”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.