Skip to content

Charm API

The Charm API is a shop-scoped REST API for server-to-server integrations. It reads and writes the same points ledger the app itself uses, so anything you do through the API shows up in the customer’s history, in analytics, and in the storefront widget exactly as if it had happened inside Charm.

Base URL: https://charm.appfleece.app/api/v1

The machine-readable spec is served unauthenticated at /api/v1/openapi.json (OpenAPI 3.1) — point your client generator or API tooling at it.

The API is available on every plan. Access is switched on per store — message us in the in-app support chat (Settings → API has a button) and we’ll enable it for you. The ask-first step exists so we know who is integrating and can help from day one, not to upsell.

What stores build with it

  • In-store purchases earn points. Your loyalty program shouldn’t stop at the webshop door. A small bridge from your POS or cash register awards points per receipt (points/earn with the receipt number as source_ref), so omnichannel customers see one balance everywhere.
  • Loyalty context in your helpdesk. Show balance, tier and recent history next to every ticket, and let support grant goodwill points right from the helpdesk after a rough experience (points/adjust with a reason the customer sees).
  • Reward actions that happen outside Shopify. Reviews on an external platform, wholesale-portal orders, event attendance, a packaging-return program — anything your systems can observe can earn points.
  • Mirror balances into your CRM or data warehouse. Nightly reads of customers and transactions feed segmentation, BI dashboards and accrual reporting without touching the app.
  • One-off imports and corrections. Migrating from a legacy program or fixing a batch of balances beats clicking through the admin one customer at a time.

Authentication

Create API keys in Settings → API in the Charm admin. Each key has:

  • a label, so you know which integration it belongs to
  • a set of scopes (below) — grant only what the integration needs; the read-only preset is pre-selected
  • a token starting with chrm_live_, shown exactly once at creation (and once more if you rotate it). Charm stores only a hash — a lost token means rotating or minting a new key.

Lost the token, or want to swap it out on a schedule? Rotate a key from its detail view (the edit icon next to any key in Settings → API): it mints a fresh token under the same name and scopes and revokes the old one in the same action. Rotation requires the access grant, like creating a key — it mints a credential either way. Revoking a key never requires the grant: taking access away is never gated. A revoked key can be permanently deleted once it’s revoked — deletion is a second, explicit step, never a shortcut around revocation.

Send the token as a bearer token on every request:

Terminal window
curl https://charm.appfleece.app/api/v1/shop \
-H "Authorization: Bearer chrm_live_..."

The shop is always resolved from the key itself — there is no shop parameter, and one supplied in a request is ignored. Keys are server-side credentials: never ship one in a browser, mobile app or theme (there is no CORS support on purpose).

The Pause API switch in Settings → API stops all API traffic for the shop immediately without revoking any keys.

Scopes

ScopeGrants
shop:readShop profile, currency, points name, plan
customers:readCustomer list and detail — balance, tier, referral code, email
customers:writeExclude / re-enroll customers, set or clear a tier override
points:readA customer’s points transaction history
points:writeEarn, deduct and adjust points
redemptions:readRedemption rules and reward catalog
redemptions:writeRedeem rewards and reverse redemptions — mints real discount codes / store credit

There is no wildcard scope, and every :write scope automatically includes its :read sibling — a key that can change what it cannot see would help nobody. redemptions:write deserves respect: combined with points:write it can manufacture discounts, which is why both carry the tightest limits below.

Endpoints

MethodPathScope
GET/shopshop:read
GET/customers?email=&cursor=&limit=customers:read
GET/customers/{customer_id}customers:read
GET/customers/{customer_id}/transactionspoints:read
GET/programprogram:read
GET/tiersprogram:read
GET/rewardsredemptions:read
POST/customers/{customer_id}/points/earnpoints:write
POST/customers/{customer_id}/points/adjustpoints:write
POST/customers/{customer_id}/points/deductpoints:write
POST/customers/{customer_id}/excludecustomers:write
POST/customers/{customer_id}/reenrollcustomers:write
POST/customers/{customer_id}/tier-overridecustomers:write
DELETE/customers/{customer_id}/tier-overridecustomers:write
POST/customers/{customer_id}/redemptionsredemptions:write
POST/redemptions/{redemption_id}/reverseredemptions:write

{customer_id} accepts a Shopify customer GID (gid://shopify/Customer/123) or the bare numeric id (123). Email is a query filter on the collection endpoint, never a path segment.

Program configuration is read-only in v1 — rules, tiers and referral settings are managed in the Charm admin.

A ready-made Bruno collection with one request per operation lives in the app repository under docs/api/bruno/.

Idempotency

Every POST and DELETE requires an Idempotency-Key header (any unique string, e.g. a UUID). Retrying with the same key replays the original response — marked with Idempotency-Replayed: true — instead of running the operation again, so a crashed integration can retry blindly without awarding points twice or minting two discount codes. Keys are remembered for 24 hours; reusing one with a different request body returns 409 idempotency_conflict.

Points earning is additionally idempotent at the ledger level: source_ref (required on earn) is unique per customer, so the same event can never award twice even across different idempotency keys.

Rate limits

Limits protect your own store first — every Shopify API call the Charm API makes competes with order processing and points awarding for the same per-store budget.

ClassLimit
Reads10 req/s, 600/min per key
Writes (points, customer state)5 req/s per key, 300/min per shop
Shopify-touching writes (redemptions, tier overrides)20/min per shop, plus a daily cap: 1 000 by default, 5 000 on Unlimited
Points volumea daily budget that scales with your shop’s normal issuance

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset for the tightest applicable bucket; a 429 includes Retry-After. Shopify-touching writes are also shed pre-emptively with a 503 when your store’s Shopify API budget is running low — back off and retry after the indicated delay. Need a higher cap for a migration or a Plus store? Contact support — limits are adjustable per shop without a deploy.

Errors

Errors use one envelope, and every response carries an X-Request-Id header (also echoed in the body) — include it when contacting support:

{
"error": {
"code": "insufficient_points",
"message": "Customer has 120 points; the reward costs 500.",
"request_id": "req_abc123"
}
}
CodeStatusMeaning
unauthorized401No bearer token presented
invalid_token401Token unknown or revoked
insufficient_scope403Key lacks the required scope
access_required403API access is not enabled for this store yet — ask us in the support chat
api_paused403The merchant paused API access
not_found404Unknown path or resource
idempotency_conflict409Same key, different request body
validation_error422Malformed input
insufficient_points422Balance cannot cover the operation
rate_limited429A limit above was hit — honor Retry-After
internal_error500Unexpected failure on our side
shopify_unavailable503Shopify is throttled or unreachable — retry later

Pagination

List endpoints return { "data": [...], "next_cursor": "..." }. Pass the cursor back as ?cursor= to fetch the next page; a null cursor means the end. limit defaults to 50 (max 250). There is no total count — pages are stable under concurrent writes, counts would not be.

History attribution

Writes show up in the customer’s points history. A reason you supply is shown to the shopper verbatim; without one, a localized default (“Awarded via API”) is used in the shopper’s language. Every API transaction also records which key made it, so support can always answer “who changed this balance”.