Webhooks
Charm can push loyalty events to your server the moment they happen, so an
ERP, CDP or a no-code tool like Make or Zapier reacts in seconds instead of
polling /customers on a schedule.
Topics
Section titled “Topics”| Topic | Fires when |
|---|---|
points.earned | A customer earns points (orders, rules, API awards) |
points.adjusted | A balance is corrected or deducted — points is signed, negative for deductions |
reward.redeemed | Points are spent on a reward |
tier.changed | A customer reaches a new VIP tier |
tier.approaching | A customer gets close to the next tier |
store_credit.issued | Store credit lands on a customer’s account |
referral.completed | A referred friend’s qualifying order completes |
Subscribe
Section titled “Subscribe”Webhooks are managed over the API with a key that carries the
webhooks:manage scope:
POST /api/v1/webhooks{ "url": "https://example.com/charm-hook", "topics": ["points.earned", "tier.changed"]}The response includes the endpoint’s signing secret exactly once — store
it; it is never shown again (lose it and you delete + recreate the endpoint,
which costs nothing). GET /webhooks lists your endpoints with delivery
health, DELETE /webhooks/{id} stops deliveries. A store can hold up to 10
endpoints, and the URL must be public https.
What a delivery looks like
Section titled “What a delivery looks like”POST https://example.com/charm-hookContent-Type: application/jsonX-Charm-Topic: points.earnedX-Charm-Event-Id: evt_kFb2p9qArM3wX-Charm-Hmac-Sha256: q1nZ…=
{ "id": "evt_kFb2p9qArM3w", "topic": "points.earned", "created_at": "2026-08-20T13:00:00.000Z", "shop": "your-store.myshopify.com", "payload": { "customer_id": "7712345", "points": 120, "balance_after": 340, "source": "order" }}payload.customer_id is the numeric Shopify customer id — the same value the
customer endpoints accept.
Verify the signature
Section titled “Verify the signature”X-Charm-Hmac-Sha256 is the base64 HMAC-SHA256 of the raw request body,
keyed with the endpoint’s secret. Verify before trusting anything:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) { const expected = createHmac("sha256", secret).update(rawBody, "utf8").digest("base64"); const a = Buffer.from(expected); const b = Buffer.from(String(header || "")); return a.length === b.length && timingSafeEqual(a, b);}Compute it over the raw bytes — re-serializing the parsed JSON will produce a different string and a false mismatch.
Delivery semantics
Section titled “Delivery semantics”- Acknowledge fast. Respond with any
2xxwithin 5 seconds. Do the real work after responding if it might take longer. - At-least-once. A delivery that isn’t acknowledged is retried with
backoff (1 min → 24 h, 7 attempts over ~33 hours), then dropped. Duplicates
are possible — dedupe on the event
id, which stays the same across every retry. - Redirects are not followed. Register the final URL.
- Failing endpoints switch off. After 25 consecutive failed attempts the
endpoint is disabled (
GET /webhooksshowsdisabled_reason); delete and recreate it once your receiver is healthy.
Related
Section titled “Related”- API overview — keys, scopes, rate limits, idempotency
- MCP for AI assistants — the same events, asked about in natural language