LedgerCore
Webhooks

Clerk Webhook

LedgerCore uses Clerk for authentication and Clerk Billing for subscription management. Clerk sends user lifecycle + billing events to this webhook so we can keep our users, billing_subscriptions, and user_credits tables in sync in real time.

Endpoint

POST /api/webhook/clerk

Authentication

Clerk signs each payload using Svix-style signature headers. The gateway verifies them with the webhook secret:

Svix-Id:        msg_2x...
Svix-Timestamp: 1761750400
Svix-Signature: v1,base64=...

Set CLERK_WEBHOOK_SECRET to the secret Clerk provides when you create the endpoint in your Clerk dashboard. In development mode the verification step can be bypassed for testing, but the production build always verifies.

Supported event types

Keep payloads raw

The gateway reads the raw request body for signature verification. Do not run JSON body parsers before this route if you proxy it — Express's express.raw() is used internally.

EventEffect on LedgerCore
user.createdInserts a row in users with a fresh internal UUID.
user.updatedUpdates email / metadata on the existing row.
user.deletedSoft-deletes the user and revokes API keys.
subscription.activeStarts or renews a billing_subscriptions row and triggers credit grant.
subscription.canceledMarks the subscription cancelled at period end.
subscription.past_dueFlags the subscription and disables credit burning on next request.
subscriptionItem.activeUsed for plan changes — upgrades/downgrades grant top-up credits.
paymentAttempt.updatedLogged for observability; only succeeded attempts grant credits.

Payload shape

Clerk's event envelope:

{
  "type": "subscription.active",
  "data": {
    "id": "sub_2x...",
    "payer_id": "user_32T5...",
    "user_id": "user_32T5...",
    "status": "active",
    "plan": {
      "id": "cplan_2x...",
      "name": "Professional"
    },
    "period_start": 1761750400,
    "period_end": 1764342400
  },
  "object": "event",
  "timestamp": 1761750401
}

The gateway dispatches based on type — see src/core/webhook/clerkWebhook.ts for the full handler matrix.

Credit granting

When a subscription.active or subscriptionItem.active event arrives, LedgerCore:

  1. Looks up the Clerk plan id (cplan_*) in our subscription_plans table.
  2. Reads the associated credit amount for that plan.
  3. Calls the stored function grant_user_credits(internalUserId, applicationId, amount, 'purchase', ...).
  4. Emits credit_balance_updated + credits_granted on the user's WebSocket room.

The whole flow is idempotent — receiving the same event twice won't double-credit the user, since the transaction metadata is keyed on the Clerk event id.

Responses

StatusWhen
200Event accepted and processed (or skipped).
401Signature verification failed.
400Unknown event type or malformed payload.
500Handler raised — Clerk will retry.

Testing

Clerk's dashboard ships a "Send test event" button that fires a realistic payload. For manual testing against a running local gateway you can use the Clerk CLI:

clerk webhook forward --url http://localhost:8090/api/webhook/clerk

Then trigger the event in Clerk's admin UI; the CLI will tunnel it to your local gateway with a valid signature.

Was this page helpful?