Flows

Webhooks

Open in app

Receive real-time events instead of polling. Register an HTTPS endpoint, subscribe to the events you care about, and verify the signature on every delivery.

Available events

payment.succeededeventoptionalA checkout payment cleared; an account is being provisioned
payment.failedeventoptionalA checkout payment failed
kyc.updatedeventoptionalA trader's KYC status changed
payout.completedeventoptionalA payout settled to the trader's Connect account
payout.failedeventoptionalA payout was reversed or could not be settled
*wildcardoptionalSubscribe to every event type

Register an endpoint

Webhook endpoints are tenant-level objects owned by your app, not by an end user. Authenticate with your app's OAuth bearer token carrying the webhooks scope (the api superscope and the * wildcard also satisfy it); a token without any of them is rejected 403 V2_SCOPE_MISSING. No end-user session is involved.

POST/v2/webhook-endpoints
App token
Request body
urlstring
required
Your HTTPS receiver
eventsstring[]
required
e.g. ["payment.succeeded","kyc.updated"] or ["*"]
descriptionstringoptionalInternal label
curl
curl -X POST http://localhost:8000/v2/webhook-endpoints \
  -H "Authorization: Bearer <app_access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/api/webhooks",
    "events": ["payment.succeeded", "kyc.updated"],
    "description": "Production receiver"
  }'
200 OK — secret shown once
{
  "id": "whe_...",
  "url": "https://yourapp.com/api/webhooks",
  "events": ["payment.succeeded", "kyc.updated"],
  "active": true,
  "description": "Production receiver",
  "secret": "whsec_...   // store it — used to verify signatures"
}

Delivery format

Each delivery is a POST with a JSON envelope and signed headers.

POST https://yourapp.com/api/webhooks
Content-Type: application/json
X-Hyperscaled-Event: payment.succeeded
X-Hyperscaled-Delivery: whd_...
X-Hyperscaled-Timestamp: 1750640000
X-Hyperscaled-Signature: t=1750640000,v1=9f86d081...

{
  "type": "payment.succeeded",
  "data": { "payment_id": "pay_...", "user_id": "usr_...", "stripe_payment_intent_id": "pi_..." },
  "timestamp": "2026-06-22T23:13:20+00:00"
}

Verify the signature

HMAC-SHA256 over `{timestamp}.{raw_body}` using your endpoint secret. Reject deliveries older than ~5 minutes.

app/api/hsc-webhook/route.ts
import crypto from "node:crypto";

// Accepts one secret or a comma-separated list, so you can keep the old and
// new secret live at the same time while rotating.
const SECRETS = (process.env.HSC_WEBHOOK_SECRET ?? "")
  .split(",").map((s) => s.trim()).filter(Boolean);

export async function POST(req: Request) {
  const raw = await req.text();
  const header = req.headers.get("x-hyperscaled-signature") ?? "";

  // The header may carry SEVERAL v1= signatures during a secret rotation:
  //   t=1730000000,v1=<new>,v1=<previous>
  // Object.fromEntries would silently keep only the last one — collect them all.
  let ts = NaN;
  const signatures: string[] = [];
  for (const part of header.split(",")) {
    const eq = part.indexOf("=");
    if (eq === -1) continue;
    const key = part.slice(0, eq).trim();
    const value = part.slice(eq + 1).trim();
    if (key === "t") ts = Number(value);
    else if (key === "v1") signatures.push(value);
  }
  if (!Number.isFinite(ts) || signatures.length === 0) {
    return new Response("bad signature", { status: 400 });
  }

  // Reject stale deliveries (replay protection).
  if (Math.abs(Date.now() / 1000 - ts) > 300) return new Response("stale", { status: 400 });

  const ok = SECRETS.some((secret) => {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${ts}.${raw}`)
      .digest("hex");
    // timingSafeEqual throws on a length mismatch — guard before comparing.
    return signatures.some(
      (sig) =>
        sig.length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),
    );
  });
  if (!ok) return new Response("bad signature", { status: 400 });

  const event = JSON.parse(raw); // { type, data, timestamp }
  // ...handle event.type
  return new Response("ok");
}

Handle multiple signatures

During a secret rotation the platform signs each delivery with every currently-active secret and sends one v1= per secret. A verifier that parses the header into an object keeps only the last one and will reject every delivery for the whole grace window. Accept the delivery if any signature matches.

This app ships a receiver

See app/api/hsc-webhook/route.ts in this repo for a working verifier wired to HSC_WEBHOOK_SECRET.

List & remove endpoints

GET/v2/webhook-endpoints
App token
200 OK
[
  {
    "id": "whe_...",
    "url": "https://yourapp.com/api/webhooks",
    "events": ["payment.succeeded", "kyc.updated"],
    "active": true,
    "description": "Production receiver"
  }
]
GET
/v2/webhook-endpoints

Runs live against your environment using the app's server-side credentials and your session. Sign in to the dashboard first for authenticated reads.

Deactivate with DELETE /v2/webhook-endpoints/{id}.