Webhook Signing Secrets
Webhook endpoints and their whsec_* signing secrets are created, rotated, and revoked either in the developer dashboard → Webhooks or programmatically via the public Webhook Subscriptions API (POST /v1/webhook_subscriptions, secret-key auth) — see the REST API reference. Every webhook Von Payments delivers is signed with the per-endpoint whsec_* secret — not your merchant API key (vp_sk_test_* / vp_sk_live_*). See Webhook Signature Verification for the verifier.
Each webhook endpoint you register in the developer dashboard → Webhooks gets its own whsec_* signing secret. This page covers the lifecycle: create, view-once, rotate, revoke.
Registering a webhook endpoint generates a per-endpoint signing secret. You see it once at creation time — store it immediately; you cannot retrieve it again.
That secret is what signs every delivery to the endpoint: the x-vonpay-signature header is t=<unix-seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of ${t}.${rawBody} keyed by the whsec_* secret (the raw UTF-8 string, prefix included). The merchant API key is not used for webhook signing. The verifier algorithm and reference implementations are on the Webhook Signature Verification page.
Lifecycle
1. Create
Creating a webhook endpoint mints a fresh whsec_* signing secret bound to that endpoint, via the dashboard or the public API.
Dashboard path — /dashboard/developers/webhooks → Add endpoint → enter your URL + select event types → Create. The full secret is shown once on the success page — copy it immediately.
API path — POST /v1/webhook_subscriptions (server-to-server, secret-key auth) with body:
{
"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"
}
The full signingSecret appears in the create (and rotate) response only. Store it server-side immediately — it is not retrievable later. See the REST API reference for the full field list + the other endpoints.
2. View-once
After creation, the dashboard shows only a truncated prefix (the first ~16 characters) of the secret. The remaining bytes are hashed at rest and cannot be retrieved — reads of the subscription (GET /v1/webhook_subscriptions/{id}) never return the secret at all. Treat the create-time copy as authoritative.
If you've lost the secret, rotate (Section 3) — there is no "show me the secret again" path by design.
3. Rotate
Rotation is atomic, not graced: the moment you rotate, the previous secret stops verifying and every subsequent delivery is signed with only the new secret (a single v1= entry). There is no dual-signature grace window. To avoid a verification-failure gap, have your handler accept both the old and the new secret across the rollout, or rely on Von Payments' automatic delivery retries to cover the brief window while you deploy the new secret.
This bounds the blast radius of a rotation:
- An attacker can't withdraw funds with a webhook secret; the worst case is forging events to your handler, and only if they also control your inbound endpoint.
- Endpoint secrets are scoped to one endpoint, not your whole API surface.
- Because rotation is atomic, accept both secrets during the deploy (or lean on delivery retries) so you can roll the new secret to your handler env without a verification-failure gap.
Rotate via either path:
Dashboard path — /dashboard/developers/webhooks/{id} → Rotate signing secret → new secret shown once → update your handler env → confirm an inbound webhook verifies cleanly under the new secret.
API path:
POST /v1/webhook_subscriptions/b6f2c1a0-3e4d-4c8b-9a1f-2d5e7c9b0a13/rotate_signing_secret
Authorization: Bearer vp_sk_live_…
Response is the same shape as Create, with a new signingSecret. The previous secret stops verifying immediately, so every delivery after the cutover is signed with only the new secret — deploy it before (or accept both secrets across) the rollout.
Recommended deploy sequence:
- Rotate (dashboard or
POST /rotate) and capture the new secret. - Push the new secret to wherever your handler reads its environment — your host's env-var settings, or your secrets manager.
- Redeploy your handler. Your handler now verifies with the new secret. Because rotation is atomic, keep the old secret accepted alongside the new one until the redeploy is live (or rely on delivery retries) so in-flight events aren't dropped — once rotated, events signed with the old secret will NOT verify.
- Confirm at least one inbound webhook verifies cleanly under the new secret.
- Done. The old secret stopped verifying the moment you rotated, so it can be dropped from your handler once the redeploy is live.
If you need to fully break trust with the old secret immediately (a leak, not a routine rotation), see Compromise path below — that flow stands up a parallel endpoint and gives you a clean audit boundary before retiring the original.
4. Revoke
Revoking an endpoint permanently disables it — under the hood this is the DELETE (soft-delete) shown below: the server flips the subscription's status to disabled and stamps deleted_at. Different from rotate: rotate keeps the endpoint alive with a new secret; revoke deactivates the endpoint entirely. (The only caller-settable statuses via PATCH are active and paused; disabled is set server-side by the delete.) Use revoke when:
- You're decommissioning the handler endpoint
- You're consolidating multiple endpoints into one
- You've created a parallel endpoint as part of a compromise response (below)
Revoked endpoints cannot be re-activated. Their record is retained for audit; their signing secret can never be reissued. To resume webhook delivery, create a fresh endpoint.
Revoke an endpoint from the dashboard at /dashboard/developers/webhooks/{id} → Revoke endpoint, or programmatically:
DELETE /v1/webhook_subscriptions/b6f2c1a0-3e4d-4c8b-9a1f-2d5e7c9b0a13
Authorization: Bearer vp_sk_live_…
Returns 204 No Content. (To stop deliveries temporarily without deleting, PATCH the subscription to status: "paused" instead — see the REST API reference.) Pending events queued for the now-deleted endpoint move to the dead-letter queue and are NOT retried — your handler will not see them.
Compromise path
If a whsec_* is exposed (committed to a public repo, leaked in a log, captured in a bug report), prefer standing up a fresh endpoint over a simple in-place rotation. A routine rotation invalidates the leaked secret immediately, so it does cut off the attacker's forging ability at once — but the reason to prefer a fresh endpoint is the clean audit boundary: you can't cleanly separate pre/during/post-compromise events on the same URL, not because the old secret lingers.
Recommended runbook:
- Stand up a fresh endpoint at a NEW URL (e.g., add a path suffix or use a fresh subdomain). This gives you a clean audit boundary — every event arriving at the new URL is post-compromise; every event at the old URL is pre/during-compromise and untrusted.
- Update the merchant's UI (or platform-integrator config) to point at the new endpoint.
- Audit events received at the OLD endpoint during the window between leak time and the new endpoint going live. Treat them as untrusted; reconcile against the source-of-truth API (
GET /v1/sessions/{sessionId}for hosted sessions; for payment intents reconcile via the capture/void call responses or the events channel/v1/public/payment_intents/{id}/events— there is no single-payment-intent retrieve endpoint) before acting on any business-state changes. - Revoke the old endpoint (step 4 above) once you're satisfied the new path is healthy.
- Rotate any secondary credentials that may have been adjacent in the leak (API keys, DB passwords, webhook URLs that contained endpoint IDs).
This costs one extra deploy and a brief period of running two endpoints in parallel — that's the price of a clean compromise response.