Webhooks

Register an endpoint, verify signed events, and handle retries and idempotency.

Webhooks push purchase, subscription, and entitlement events to your backend the moment they happen — so you can provision access, update records, and trigger email without polling.

Register an endpoint#

Register a publicly reachable HTTPS URL and subscribe it to the events you care about:

bash
curl https://api.cashsdk.com/v1/webhooks \
  -H "Authorization: Bearer csk_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.yourapp.com/hooks/cashsdk",
    "events": ["purchase.completed", "subscription.renewed", "refund.issued"]
  }'

CashSDK returns a signing secret (whsec_...). Store it as an environment variable; you'll use it to verify every incoming request.

Event types#

EventFires when
purchase.completedA one-time or first subscription purchase is validated
subscription.renewedThe store bills a renewal successfully
subscription.canceledAuto-renew is turned off (access continues until period end)
subscription.expiredA subscription lapses without renewing
trial.startedA free trial begins
refund.issuedApple or Google refunds or revokes a purchase
entitlement.grantedAn entitlement becomes active (purchase or manual grant)
entitlement.revokedAn entitlement is removed (expiry, refund, manual)

Sample payload#

Every event shares an envelope: a stable id, a type, a created timestamp, and a data object.

json
{
  "id": "evt_2f9c1a",
  "type": "purchase.completed",
  "created": 1752787200,
  "data": {
    "app_user_id": "u_812",
    "product_id": "pro_monthly",
    "entitlement": "pro",
    "transaction_id": "txn_9ab21",
    "expires_at": "2026-08-17T00:00:00Z",
    "environment": "production"
  }
}

Verify the signature#

Each request carries a CashSDK-Signature header — an HMAC-SHA256 of the raw request body keyed with your signing secret. Compute the HMAC over the unparsed body and compare in constant time before trusting anything.

js
import crypto from "node:crypto";

// Use the raw body string, not a re-serialized object.
export function verify(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);

  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express example (body captured with express.raw())
app.post("/hooks/cashsdk", (req, res) => {
  const ok = verify(
    req.body,                          // Buffer / raw string
    req.header("CashSDK-Signature"),
    process.env.WEBHOOK_SIGNING_SECRET
  );
  if (!ok) return res.status(400).send("bad signature");

  const event = JSON.parse(req.body);
  // ... handle event.type
  res.sendStatus(200);
});

Verify against the raw bytes you received. Parsing to JSON and re-stringifying can reorder keys or change whitespace and will break the HMAC.

Retries & idempotency#

Respond with a 2xx quickly (do heavy work asynchronously). If your endpoint errors, times out, or returns a non-2xx, CashSDK retries with exponential backoff over several hours.

Because a retry can deliver an event you already processed, treat handlers as idempotent: dedupe on the event id. Record processed ids and skip repeats.

js
if (await seen(event.id)) return res.sendStatus(200);
await process(event);
await markSeen(event.id);

Send a test event#

Fire a synthetic event to your endpoint from the dashboard, or with the MCP send_test_webhook tool, to confirm your signature check and handler before you depend on live traffic.

text
send_test_webhook(url="https://api.yourapp.com/hooks/cashsdk", type="purchase.completed")

Next steps#