Webhook Signing Secrets
Each endpoint you register — in the dashboard at /dashboard/developers/webhooks or with POST /v1/webhook_subscriptions (secret-key auth, REST API reference) — gets its own whsec_* signing secret. Every delivery to that endpoint is signed with it (x-vonpay-signature: t=<unix-seconds>,v1=<hex>, HMAC-SHA256 of ${t}.${rawBody} keyed by the raw secret string, prefix included); your merchant API key is never used. The verifier is on Webhook Signature Verification.
Lifecycle
1. Create
Dashboard — Add endpoint → URL + event types → Create. API — POST /v1/webhook_subscriptions:
{
"url": "https://your-app.example.com/webhooks/vonpay",
"enabledEvents": ["charge.succeeded", "charge.refunded", "payment_intent.succeeded"],
"description": "Production order-fulfillment hook"
}
Response (201):
{
"id": "b6f2c1a0-3e4d-4c8b-9a1f-2d5e7c9b0a13",
"object": "webhook_subscription",
"url": "https://your-app.example.com/webhooks/vonpay",
"enabledEvents": ["charge.succeeded", "charge.refunded", "payment_intent.succeeded"],
"status": "active",
"signingSecret": "whsec_NbR4mP9k2qL7vX1tWj3...",
"apiVersion": "2026-05-04",
"createdAt": "2026-05-04T18:30:00Z"
}
See the REST API reference for the full field list and the other endpoints.
2. View-once
The full signingSecret appears in the create and rotate responses only. Store it server-side immediately: the dashboard shows only a truncated prefix afterwards, GET /v1/webhook_subscriptions/{id} never returns it, and there is no "show me the secret again" path. If you have lost it, rotate.
3. Rotate
Rotating mints the new secret — you cannot load it into your handler beforehand — and within moments of the call every delivery is signed with only the new one, a single v1= entry. There is no dual-signature grace window on our side, and a delivery you answer with 400, 401 or 403 is not retried automatically: a handler still holding only the old secret rejects every event delivered between the rotation and its redeploy, and nothing alerts you; those deliveries can be resent from the dashboard afterwards, but only if you notice. So during your switch-over your handler must accept both secrets — call the verifier once per secret and accept if either passes — and drop the old one as soon as the new one verifies: past that point the only party who can still produce a signature it accepts is someone else holding that secret.
Dashboard — /dashboard/developers/webhooks → the endpoint's Rotate signing secret action. API:
POST /v1/webhook_subscriptions/b6f2c1a0-3e4d-4c8b-9a1f-2d5e7c9b0a13/rotate_signing_secret
Authorization: Bearer vp_sk_live_…
The response has the same shape as Create, with the new signingSecret. Have your deploy ready before you make the call.
Deploy sequence:
- First, ship a handler that reads a second secret from a separate variable and, when it is set, runs the verifier against each secret in turn. Nothing has changed yet — the second variable is empty.
- Rotate (dashboard or
POST /rotate) and capture the new secret. - Put the new secret in that second variable and redeploy.
- Confirm at least one inbound webhook verifies under the new secret.
- Remove the old secret and redeploy again.
Steps 1 → 2 are the window; keep it as short as your deploy takes. If the old secret leaked, do not accept it even briefly — use the compromise path, which closes the gap with no window at all.
4. Revoke
Revoking disables the endpoint permanently: its record is retained for audit, its secret can never be reissued, and it cannot be re-activated — create a fresh endpoint to resume delivery. Use it when decommissioning a handler, consolidating endpoints, or retiring the original after a compromise response.
Dashboard — /dashboard/developers/webhooks → the endpoint's Delete endpoint action. API:
DELETE /v1/webhook_subscriptions/b6f2c1a0-3e4d-4c8b-9a1f-2d5e7c9b0a13
Authorization: Bearer vp_sk_live_…
Returns 204 No Content. Nothing new is enqueued for a deleted endpoint. To stop deliveries temporarily instead, PATCH the subscription to status: "paused" (active and paused are the only caller-settable statuses), or use the endpoint's Pause endpoint action in the dashboard.
Compromise path
If a whsec_* is exposed (committed to a public repo, leaked in a log, captured in a bug report), stand up a fresh endpoint rather than rotating in place — it gives you a clean audit boundary between pre- and post-compromise events, which one URL cannot:
- Create a new endpoint at a new URL (a path suffix or a fresh subdomain). Every event at the new URL is post-compromise; every event at the old URL is untrusted.
- Point your configuration at the new endpoint.
- Audit events received at the old endpoint between the leak and the new endpoint going live: reconcile each against the API (
GET /v1/sessions/{sessionId}for hosted sessions,GET /v1/payment_intents/{id}for payment intents) before acting on any business-state change. - Revoke the old endpoint once the new path is healthy.
- Rotate any credential adjacent in the leak — API keys, database passwords, webhook URLs carrying endpoint ids.