Webhooks
Verify Vortex-Signature, handle deliveries, and project lifecycle events into your app.
Webhooks
Outbound webhooks push billing and payment lifecycle events to your app. Verify the signature against the raw body before parsing JSON.
Register an endpoint
curl -sS -X POST "https://api.sandbox.vortex.nyc/v1/webhook-endpoints" \
-H "Authorization: Bearer $VORTEX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: wh_endpoint_1" \
-d '{
"url": "https://example.com/vortex/webhooks",
"enabledEvents": ["*"]
}'
Store the endpoint secret from the response as VORTEX_WEBHOOK_SECRET.
Verify before parse
Use the billing webhook helpers when you can. They verify Vortex-Signature first.
Next.js App Router
import { handleVortexWebhookRequest } from "@vortex/billing/webhooks";
export async function POST(request: Request) {
return handleVortexWebhookRequest({
request,
secret: process.env.VORTEX_WEBHOOK_SECRET!,
async onEvent(event, context) {
// Idempotent write keyed by event.id / context.deliveryId
await recordVortexEvent({
eventId: event.id,
deliveryId: context.deliveryId,
type: event.type,
payload: event,
});
},
});
}
Raw body hosts
If your framework already consumed the body, use handleVortexWebhookPayload with the raw bytes and headers instead. Do not re-serialize JSON and verify that — signatures will fail.
Develop locally: vortex listen
No ngrok, no verification bypass:
npx @vortexnyc/payments-listen http://localhost:3000/webhooks
Forwarding sandbox webhooks to http://localhost:3000/webhooks
Secret whsec_9f2c4e...
Put it in .env as VORTEX_WEBHOOK_SECRET.
→ invoice.paid 200 wd_m3k1_ab12cd34
Every forwarded delivery carries a real Vortex-Signature signed over
the exact bytes with your endpoint’s secret — your handler runs the
identical verification path it will run in production. A wrong secret fails
verification locally, exactly as it would live. Sandbox keys only; the
route does not exist for production keys.
Test your handler in 30 seconds
Fire a realistic signed event at any registered endpoint — no real payment needed. The event is built from the canonical serializer’s example payload, signed with your endpoint’s secret, and delivered through the same retry pipeline as production traffic:
curl -sS -X POST "https://api.sandbox.vortex.nyc/v1/webhook-endpoints/$ENDPOINT_ID/test" \
-H "Authorization: Bearer $VORTEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "eventType": "invoice.paid", "dispatch": true }'
dispatch: true sends it immediately; omit it to create the delivery and inspect it first under /v1/webhook-deliveries. Any cataloged event type works — payment.captured, invoice.payment_failed, payout.succeeded, payment_method.created — list them all at /v1/webhook-event-types.
Test events are sandbox-only: a production key is refused. Fabricated events have no place near live money.
Verify with the SDK
The SDK exports the verification helpers — same wire format, discriminated result, no configuration to get wrong:
import {
verifyVortexWebhook,
narrowVortexEvent,
} from "@vortex-api/vortex/2026-08";
const result = await verifyVortexWebhook({
payload: rawBody, // the RAW body string
header: request.headers.get("Vortex-Signature"),
secret: process.env.VORTEX_WEBHOOK_SECRET!, // used exactly as displayed
});
if (!result.ok) {
return new Response(result.reason, { status: 401 }); // tamper/stale/missing each named
}
const event = narrowVortexEvent(result.event); // typed union of known events
if (event === null) {
// An event type newer than your SDK. Acknowledge it (2xx) and ignore, or
// log it — your forward-compatibility decision, made explicit.
}
The secret (whsec_...) is the HMAC key verbatim — there is no base64
decoding step to get wrong. A wrong secret fails with invalid_signature,
never a mystery 500.
Delivery semantics
These are the numbers the dispatcher actually runs, not aspirations:
- Success is a 2xx. Anything else is a failure for that attempt.
- Respond fast, queue the work. Acknowledge, then process — a handler that does slow work inline risks timeouts and duplicate processing on retry.
- Retries: up to 8 attempts with exponential backoff starting at 1 minute and doubling to a 64-minute cap (1m, 2m, 4m, 8m, 16m, 32m, 64m).
- 4xx (except 429) is terminal for that delivery — a client error means
the request will not get better by retrying; fix the handler and redrive
from
/v1/webhook-deliveries/redrive. 429 and 5xx and network failures retry. - Failing endpoints back off as a whole: after consecutive failures the
endpoint enters a backoff window (same exponential schedule) and its
health is reported as
degraded(1+ consecutive failures) orfailing(3+) under/v1/webhook-endpoints/health. Endpoints are never silently auto-disabled — deliveries queue and resume. - Every delivery carries
Vortex-Signature,Vortex-Delivery-Id,Vortex-Event-Id, andUser-Agent: Vortex-Webhooks/1.0. - No IP allowlist. Delivery IPs are not stable, and IP allowlists are a weaker control than the signature you are already verifying. If your infrastructure demands network-level filtering, front your handler with a URL nobody can guess and verify signatures — both are under your control.
Troubleshooting, by symptom
- 401/403 at your handler — wrong secret (each endpoint has its OWN
whsec_; rotating mints a new one), or you verified a re-serialized body instead of the raw bytes. - Your framework 404s the route — some hosts redirect
example.com→www.example.com; register the exact final URL. Redirect middleware in front of webhook routes is the classic silent killer. - Cloudflare in front of your handler — Bot Fight Mode and similar managed challenges block server-to-server POSTs outright; IP allowlisting does not fix it. Exempt the webhook path from bot protection.
- Everything 2xx but events “missing” — check the endpoint’s
eventTypesfilter, then/v1/webhook-deliveriesfor the event id; the delivery record shows the exact status code and response body we received. - Testing locally — fire a signed event at any registered endpoint in 30 seconds (see above), sandbox-only.
Delivery ops
The public API exposes deliveries, replay, redrive, and endpoint health under /v1/webhook-*. Operator routes add inspection and provider-webhook replay for platform ops.
Inbound processor webhooks
Provider webhooks hit Vortex on /webhooks/* with X-Provider-Signature. Your product app should consume Vortex outbound events, not raw processor payloads.