VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Get started
Introduction
Quickstart
How Voidhash works
Glossary
Versions and releases
Concepts
Teams, projects and environments
API keys
Products and perks
Offers
Subscription lifecycle
People and identity
Events
Features
Paywalls in your app
Feature flags and A/B tests
Paywall components
Push notifications
Ad attribution in your app
Client SDKs
React Native
Swift
Kotlin
Web
Your backend
Node.js
PHP
Go
Rust
Other languages
Payment sources
Forward store notifications
Developer tools
CLI
MCP server
Query analytics with VoidQL
Other languages
Check access
Receive webhooks

Receive webhooks

Verify Voidhash webhook signatures by hand and handle events in any language.

Voidhash signs every webhook request with your endpoint's signing secret. Verify the signature before you trust a request: anyone who knows your URL can send it a forged event. This page verifies with your language's HMAC-SHA256 and constant-time comparison functions; the Node.js, PHP, Go and Rust libraries do it for you.

Create an endpoint

In Studio, open Settings → Webhooks, select New webhook, and enter your endpoint's public HTTPS URL, its environment and the events it receives. Studio shows the signing secret (whsec_…) once; store it in your secret manager, here as VOIDHASH_WEBHOOK_SECRET. See Endpoints for every field.

The signature scheme

Each request carries two headers for verification:

  • X-Webhook-Timestamp: the Unix time, in seconds, when the attempt was signed.
  • X-Webhook-Signature: v1= followed by a lowercase hex HMAC-SHA256 digest.

The digest covers the string {timestamp}.{body}: the timestamp header's value, a period, and the raw request body exactly as received. The HMAC key is the full secret string, whsec_ prefix included, encoded as UTF-8.

v1 names the scheme. Reject signatures with any other prefix.

Verify a request

  1. Read the raw request body as bytes, before any JSON parser touches it. Re-serialized JSON differs from what Voidhash signed.
  2. Read X-Webhook-Timestamp and X-Webhook-Signature. Reject the request if either is missing or sent more than once.
  3. Check that the timestamp is within 300 seconds of your server's clock, in either direction. This stops an attacker from replaying a captured request later.
  4. Compute HMAC-SHA256 over {timestamp}.{body} with your secret, and hex-encode it.
  5. Compare v1={digest} with the signature header using a constant-time comparison.
  6. If any check fails, answer 400 and do nothing else.

Every retry is signed again with a new timestamp, so a retried delivery passes the timestamp check.

Example in Python

This Flask handler verifies a request with the standard library:

app.py
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["VOIDHASH_WEBHOOK_SECRET"]  # whsec_...
TOLERANCE_SECONDS = 300


@app.post("/webhooks/voidhash")
def voidhash_webhook():
    body = request.get_data()  # raw bytes
    timestamp = request.headers.get("X-Webhook-Timestamp", "")
    signature = request.headers.get("X-Webhook-Signature", "")

    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        abort(400)

    digest = hmac.new(
        SECRET.encode("utf-8"),
        timestamp.encode("utf-8") + b"." + body,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(f"v1={digest}", signature):
        abort(400)

    event_type = request.headers["X-Webhook-Event"]
    payload = json.loads(body)
    # Store the event, then process it after answering.
    return "", 200

Handle events

Read the event type from the X-Webhook-Event header. Every payload except test.ping repeats it in its type field. Webhook events lists every type and payload field.

  • Answer first. Answer any 2xx within 30 seconds: store the event, answer 200, and do the work afterwards. A redirect, a 4xx or 5xx, a timeout or a connection error is a failed attempt.
  • Skip duplicates. Delivery is at least once, and payloads carry no event ID. Build an idempotency key from type, the subject's ID (subscriptionId, purchaseId or personId) and occurredAt (erasedAt for person.deleted). Record each key and skip ones you have seen.
  • Don't rely on order. Compare occurredAt before you overwrite newer state, or check access for the current answer.
  • Accept unknown types. New event types may be added. Answer 200 for types you don't handle, including test.ping.

Retries

A failed delivery is retried 5 minutes, 30 minutes, 2 hours and 24 hours after the attempt before it, then marked exhausted. After 20 consecutive failed attempts Voidhash sets the endpoint to Failed and stops sending to it. See Retries and the delivery log.

Test locally

Voidhash can't reach localhost, so expose your local server through a tunnel such as ngrok http 3000 or cloudflared tunnel --url http://localhost:3000, and create a Test endpoint with the tunnel's HTTPS URL. Then:

  • Select Send test in the endpoint's actions menu, or run voidhash webhook test <webhook>, which waits for your server's answer and prints it. A test only goes to an endpoint that is Active.
  • Make a sandbox or simulated purchase to receive real lifecycle events. See Test purchases (Help center).

Troubleshooting

  • Every signature fails. Your framework parsed the body before you verified it, or you are using another endpoint's secret. Each endpoint has its own secret, and rotating replaces it at once.
  • Signatures fail only sometimes. Check your server's clock. Drift beyond 300 seconds rejects valid requests; keep clocks synced with NTP.
  • A proxy rewrites the body. Some proxies re-encode JSON or change line endings. Verify against the bytes your application receives, and turn off body rewriting on the webhook route.

Next steps

  • Webhook events: every event type and its payload.
  • Webhook delivery: environments, retries and the delivery log.
  • Check access

Something wrong or missing on this page? Tell us by email or on Discord.

PreviousCheck accessNextPayment sources

On this page

Create an endpointThe signature schemeVerify a requestExample in PythonHandle eventsRetriesTest locallyTroubleshootingNext steps