Skip to content
Payments docs are in beta — the OpenAPI contract is the source of truth.API reference
VortexPayments
Esc
↑↓navigate↵open⌘Jpreview
On this page

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) or failing (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, and User-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 eventTypes filter, then /v1/webhook-deliveries for 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.

Last updated on September 25, 2026

Was this page helpful?