Webhook delivery
How Voidhash signs, sends, retries and logs webhook requests.
A webhook is an HTTPS endpoint on your server that Voidhash calls when something changes in your project: a subscription renews, a purchase is refunded, a person's data is erased. Renewals, cancellations and refunds happen at the store, often while the app is closed, so webhooks are how your backend hears about them.
This page describes what Voidhash sends and when. To build the receiving endpoint, follow the guide for Node.js, PHP, Go, Rust or another language. Webhook events lists every event type and payload.
A webhook tells you that something changed, not what the person can use now. For a yes/no access decision, check access from your backend instead.
Endpoints
In Studio, open Settings → Webhooks and select New webhook. Fill in:

- Name: what the endpoint is for, such as
Orders backend. Up to 100 characters. - URL: the public URL Voidhash sends
POSTrequests to. - Environment: Production or Test. See Environments.
- Events: the event types this endpoint receives. Select at least one.
- Description (optional): a note for your team.
On Voidhash Cloud the URL must resolve to a public address. localhost and private network
addresses are refused, both when you save and on every delivery.
Select Create webhook. Studio shows the endpoint's signing secret once: whsec_ followed by 64
hex characters. Store it in your server's secret manager next to your secret key.
To send a signed test.ping event, open the endpoint's actions menu and select Send test. The
delivery appears under Recent deliveries with your server's answer. A test only goes to an
endpoint that is Active.
You can also manage endpoints with the CLI (voidhash webhook create, edit,
enable, disable, rotate, test, deliveries, retry), the
API, or a server library.
Environments
Every endpoint belongs to production or test, like API keys. The environment decides which purchase events the endpoint receives:
| Endpoint | Receives purchase events from |
|---|---|
production | Production purchases, plus store sandbox purchases while they unlock production apps. |
test | Store sandbox purchases (TestFlight, Google Play testing), simulated purchases from development mode, and test mode payments such as Stripe test mode. |
person.deleted belongs to no purchase, so it reaches every endpoint that subscribes to it. Point
a test endpoint at your staging server.
What Voidhash sends
Each event is one POST request with a JSON body and three headers:
| Header | Value |
|---|---|
X-Webhook-Event | The event type, such as subscription.renewed. |
X-Webhook-Timestamp | The Unix time, in seconds, when this attempt was signed. |
X-Webhook-Signature | v1= and an HMAC-SHA256 signature. See Signatures. |
The body is the event payload itself, with no envelope around it. Its type field repeats the
event type:
{
"type": "purchase.completed",
"projectId": "proj_r3k8v2m1x9q4",
"personId": "person_k2v9x4m8q1r3",
"distinctId": "user_123",
"productId": "prod_x7m2k9v4q8r1",
"productSlug": "lifetime",
"providerProductId": "com.example.lifetime",
"provider": "apple-app-store",
"source": null,
"verified": true,
"environment": "production",
"occurredAt": "2026-11-05T09:12:44.000Z",
"purchaseId": "pur_v8q2m4k9x1r7",
"purchaseKind": "one_time",
"providerKey": "2000000123456789",
"providerTransactionId": "2000000123456789",
"purchasedAt": "2026-11-05T09:12:44.000Z",
"amount": { "currency": "USD", "grossAmount": 4999 }
}Webhook events lists every field and every event type.
Signatures
Voidhash signs every request with the endpoint's signing secret. Anyone who knows your URL can send it a forged event, so verify the signature before you trust a request.
X-Webhook-Signatureisv1=followed by a lowercase hex HMAC-SHA256 digest.- The digest covers the string
{timestamp}.{body}: theX-Webhook-Timestampvalue, 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. v1names the scheme. Reject signatures with any other prefix.
The server libraries reject a timestamp more than 300 seconds from your server's clock, in either direction, so a captured request can't be replayed later. Every retry is signed again with a new timestamp, so a retried delivery passes this check. To verify without a library, see Receive webhooks over HTTP.
Respond to deliveries
Answer with any 2xx status within 30 seconds. Anything else counts as a failed attempt: a
3xx (redirects are not followed), a 4xx or 5xx, a timeout, or a connection error.
Verify the signature, store the event, answer 200, and do the work afterwards. A handler that
does slow work before it answers times out and receives the same event again.
Retries
A failed delivery is retried on a fixed schedule, measured from the previous attempt:
| Attempt | When |
|---|---|
| 1 | Right after the event |
| 2 | 5 minutes after attempt 1 |
| 3 | 30 minutes later |
| 4 | 2 hours later |
| 5 | 24 hours later |
After the fifth failed attempt the delivery is exhausted and is not retried again. Each retry
goes to the endpoint's current URL and is signed with its current secret, with a new timestamp.
Automatic disabling
After 20 consecutive failed attempts across all of an endpoint's deliveries, Voidhash sets the endpoint to Failed and stops sending to it. Any successful delivery resets the count.
A Failed or Disabled endpoint receives no new events, and events that happen meanwhile are not queued for it. To turn it back on, select Edit in its actions menu and switch Enabled on. That also clears the failure count.
Delivery log
Select an endpoint in Settings → Webhooks to see its Recent deliveries. Each delivery has a status:

| Status | Meaning |
|---|---|
pending | Waiting for its first attempt. |
in_progress | Being sent. |
succeeded | Your endpoint answered 2xx. |
failed | The last attempt failed; Next retry shows when the next one runs. |
exhausted | All 5 attempts failed. |
Open a delivery to see its Payload and its Delivery attempts: each attempt's status code, duration, error and up to 2 KB of your response body.
To send a failed or exhausted delivery again now, open it and select Retry. The retry
reuses the original payload.
Duplicates and ordering
Delivery is at least once. A timeout after your server processed an event, or a manual retry,
sends the same event again. Payloads carry no event ID, so build an idempotency key from
type, the subject's ID (subscriptionId, purchaseId or personId) and occurredAt. Record
each key you process and skip ones you have seen.
Events are not guaranteed to arrive in order, because each delivery retries on its own schedule.
Compare occurredAt before you overwrite newer state, or
check access for the current answer.
Rotate the secret
Rotate a secret that may have leaked: select Rotate secret in the endpoint's actions menu, or
run voidhash webhook rotate. Studio shows the new secret once.
Every delivery after rotation, including retries of earlier events, is signed with the new secret. Deploy the new secret right away; deliveries your server rejects in the meantime fail and are retried on the schedule above.
Next steps
- Webhook events: every event type and its payload.
- Receive webhooks with Node.js, PHP, Go, Rust or another language.
- Webhook endpoints in the API
Something wrong or missing on this page? Tell us by email or on Discord.