Elixpo Pay Docs

Dashboard

Webhooks

Elixpo Pay POSTs signed events to your app's webhook endpoint. Set the URL and choose which events to receive under Entitlement webhook on your product's page; verify each delivery with your per-app signing secret.

Events you can subscribe to

  • entitlement.updated — a buyer's access was granted, changed, or expired. Required — this is how you fulfill purchases.
  • payment.captured — a payment succeeded. Optional; useful for receipts, analytics, or your own ledger.

Each endpoint only receives the events it's subscribed to. The required event is always on; toggle the optional ones in the dashboard. More event types will appear here over time.

Request

http
POST <your endpoint>
Content-Type: application/json
X-Elixpo-Pay-Event:     entitlement.updated
X-Elixpo-Pay-Timestamp: 1718500000
X-Elixpo-Pay-Signature: sha256=<hex HMAC of `${timestamp}.${rawBody}`>
json
{
  "id": "whd_…",
  "type": "entitlement.updated",
  "created": 1718500000,
  "data": {
    "app": "lixblogs",
    "uid": "u_123",
    "tier": "member",
    "status": "active",
    "active": true,
    "expires_at": "2026-07-16 12:00:00",
    "version": 3
  }
}

payment.captured

Same envelope and signature; the type and data differ. Delivered only if you've enabled it.

json
{
  "id": "whd_…",
  "type": "payment.captured",
  "created": 1718500000,
  "data": {
    "app": "lixblogs",
    "uid": "u_123",
    "transaction_id": "txn_…",
    "provider_payment_id": "pay_…",
    "provider_order_id": "order_…",
    "currency": "INR",
    "amount": 19900,
    "tier": "member"
  }
}

Verifying

Recompute the HMAC over `${timestamp}.${rawBody}`using your ELIXPO_PAY_WEBHOOK_SECRET (the whsec_… from the dashboard) and compare in constant time. Reject stale timestamps. Branch on type since one endpoint may receive several event types.

javascript
import crypto from "node:crypto";

export async function POST(req) {
  const raw = await req.text();
  const ts = req.headers.get("x-elixpo-pay-timestamp");
  const sig = (req.headers.get("x-elixpo-pay-signature") || "").replace("sha256=", "");

  const expected = crypto
    .createHmac("sha256", process.env.ELIXPO_PAY_WEBHOOK_SECRET)
    .update(ts + "." + raw)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
    return new Response("bad signature", { status: 401 });
  }

  const evt = JSON.parse(raw);
  if (evt.type === "entitlement.updated") {
    // Upsert users.tier = evt.data.tier with expiry evt.data.expires_at,
    // ignoring deliveries with a lower data.version than you've seen.
  }
  return Response.json({ ok: true });
}

Idempotency & ordering

  • Each entitlement carries a monotonic version — ignore any entitlement.updated whose version is ≤ the one you've already applied.
  • Respond 2xx quickly; non-2xx responses are recorded as failed deliveries for retry/inspection.
  • The same grant may arrive from both the instant client confirmation and the provider webhook — fulfillment is idempotent on our side.

Checkout sessionsEntitlements API