FaucetPay Webhook and Callback Verification: Two Systems, Two Different Checks
FaucetPay webhook and callback verification means two different things depending on which system sent the notification. If you're reacting to a payout.sent or payout.failed event from the v2 scoped-key API, you verify it by recomputing an HMAC-SHA256 signature over the raw request body and comparing it to the X-FaucetPay-Signature header. If you're reacting to a Merchant API payment notification (an IPN-style callback), the posted fields are not proof of anything by themselves — you verify by calling FaucetPay's token endpoint and trusting that response instead. Mixing these two checks, or skipping verification and trusting the incoming POST body directly, is the actual security gap in most broken integrations, not a missing library.
Most faucet rewards are tiny. FaucetPay can help you collect small payouts from supported faucets, PTC sites and reward platforms in one microwallet before withdrawing later.
Set up FaucetPay to collect small rewards →FaucetPay has two notification systems, not one
The word "webhook" gets used loosely for both. The v2 scoped-key API has an actual event-driven webhook system: you subscribe to payout events and FaucetPay POSTs a signed JSON body to your endpoint when one fires. The older Merchant API, used for accepting crypto payments through a hosted checkout page, sends a different kind of server-to-server notification commonly called an IPN or callback, built around a one-time verification token rather than a cryptographic signature. If you're not sure which one your integration receives, check which FaucetPay feature triggered it: a payout your faucet sent through the API, or a payment a customer made through your Merchant checkout form.
The v2 payout webhook: what it looks like
FaucetPay's v2 documentation describes subscribing to payout.sent and payout.failed events, configured from the faucet's Manage page. Each delivery is a POST containing an event id, the event name, your faucet id, a created_at timestamp, and a data object with the payout's recipient, amount, currency, payout_id, payout_user_hash and message. Every delivery carries an X-FaucetPay-Signature header formatted as an HMAC-SHA256 hex digest, prefixed with sha256=.
Verifying the v2 webhook signature correctly
The signature is computed over the exact raw bytes of the request body using a webhook secret you configure, not over a JSON-decoded and re-serialized version of it. Your handler needs to capture the raw body before any parsing, compute the same HMAC-SHA256 using your stored secret, and compare the result to the header value using a constant-time comparison rather than a plain string equality check, since a fast-fail comparison can leak timing information about how much of the signature matched.
The single most common way this check silently fails
The signature almost never matches when a framework has already parsed the request body into a JSON object by the time your verification code runs, and you then re-serialize that object to compute the HMAC. Re-serializing JSON does not reliably reproduce the exact original bytes — key order, whitespace and number formatting can all differ — so the computed signature mismatches even for a completely legitimate delivery. The fix is capturing and hashing the raw request body before any JSON parsing happens, which usually means adjusting how your framework's body parser is configured for that specific route.
Registering the webhook endpoint is deliberately locked down
Webhook endpoints are configured from the faucet's Manage page and require an active session plus two-factor authentication — a scoped API key cannot register or change a webhook delivery URL by itself. This is a deliberate restriction, not a bug: it means a leaked scoped key alone cannot redirect payout notifications to an attacker-controlled endpoint.
Why a localhost or internal URL gets rejected outright
FaucetPay's webhook system requires a public HTTPS endpoint and explicitly rejects internal IP addresses as a server-side request forgery protection. If you're testing locally, a plain localhost or private-network URL will not work as the registered webhook target; you need a public HTTPS tunnel or a deployed staging endpoint during development, not just an internal address exposed on your own network.
The Merchant API callback: what it looks like
When a customer completes a payment through a FaucetPay Merchant checkout page, FaucetPay posts a form-encoded callback to the callback_url configured on your payment form. The posted fields include a one-time token, a transaction_id, the merchant_username and payer_username, the amount and currency the buyer was charged in, the amount and currency you priced the item in if a conversion occurred, and the custom field you originally set — typically your own order or user identifier.
The posted callback fields are not proof by themselves
Anyone can send a POST request to a public callback URL with fabricated field values that look identical to a real FaucetPay callback. FaucetPay's own documentation is explicit that the callback alone should never be trusted; the authoritative record is retrieved separately by calling back to FaucetPay's verification endpoint with the token from the callback, which returns the real payment details from FaucetPay's own system rather than whatever was posted to your server.
The token is single-use, which is the actual security guarantee
Each verification token works exactly once. This is what makes the verify-endpoint approach meaningful: even if an attacker discovers or guesses a token format, a token that was already consumed by the real verification call cannot be reused to fake a second confirmation. Your handler should treat a token that fails verification, or one that verifies successfully but does not match a payment you were expecting, as untrusted and should not deliver the purchased goods or credit the balance based on it.
Matching the payment to your own order, safely
Use the custom field to look up the specific order or account the callback should apply to, and compare the verified amount1, currency1 and merchant_username returned by the token endpoint against what your system expected for that order — not against the same fields as posted in the original callback, which is the data you cannot yet trust. Public example integrations for this endpoint typically also compare merchant_username case-insensitively, since account usernames can be returned with different casing than however you stored them.
The retry schedule means your endpoint must be idempotent
If your server does not respond with an HTTP 200 to a Merchant callback, FaucetPay's documented retry schedule resends it: immediately, then after roughly 5, 15, 30, 60, 120 and 240 minutes, spanning several hours. If your handler is not idempotent — for example, if it credits a balance or fulfills an order every time the callback is received rather than checking whether that specific transaction_id was already processed — a delayed 200 response on your end can result in the same payment being fulfilled more than once across retries.
Respond fast, verify after acknowledging the delivery
A slow verification step inside your callback handler increases the chance FaucetPay's request times out from its side before your 200 response is sent, triggering an unnecessary retry. A common pattern is acknowledging receipt quickly, then performing the token verification and fulfillment as a follow-up step, as long as that follow-up step is still what actually gates delivering the purchased item — the fast acknowledgment should never itself be treated as proof the payment is valid.
Keep both secrets out of client-side and version-controlled code
The webhook HMAC secret and any Merchant integration credentials belong in server-side configuration or a secrets manager, the same as an API key. A webhook secret committed to a public repository or embedded in front-end code defeats the signature check entirely, since anyone who can read the secret can also forge a signature that will pass verification.
Build a Verification Checklist before going live
Confirm each of these before relying on either notification type in production.
- You know which system applies: v2 payout webhook or Merchant API callback.
- For webhooks: the raw request body is captured before any JSON parsing.
- For webhooks: the HMAC comparison uses a constant-time equality check.
- For webhooks: the registered endpoint is public HTTPS, not localhost or an internal address.
- For callbacks: every request is verified against the token endpoint before fulfillment.
- For callbacks: verified fields are compared against your own expected order data, not the posted fields.
- For callbacks: the handler is idempotent against the transaction_id or token, tolerant of repeated retries.
- Both secrets are stored server-side only, never in client code or a public repository.
Worked example — signature verified against the wrong bytes
A developer's framework parses every incoming request body into JSON automatically before their route handler runs. They compute the HMAC over JSON.stringify(req.body) instead of the original bytes. Every delivery fails verification even though the webhook secret is correct. The fix is configuring that specific route to expose the raw body, or capturing it in middleware before the JSON parser runs, and hashing that instead.
Worked example — callback trusted without calling the verify endpoint
An integration reads amount1 and merchant_username directly from the posted callback and immediately marks an order as paid. Someone discovers the public callback URL and posts a fabricated request with a large amount and no real payment behind it. Because the handler never called the token verify endpoint, the fake request is accepted as genuine. Adding the verify-endpoint call as a mandatory step before fulfillment closes this gap.
Worked example — webhook registered against a local development URL
During testing, a developer points the webhook URL at a plain local address exposed over their home network rather than a public HTTPS endpoint. Deliveries are rejected outright by FaucetPay's SSRF protection before they ever leave FaucetPay's infrastructure. Switching to a public HTTPS tunnel for local development, or testing against a real staging deployment, resolves it.
Worked example — a slow handler causes duplicate fulfillment
A Merchant callback handler performs verification and order fulfillment in the same synchronous request, and under load occasionally takes long enough that FaucetPay's request times out before the 200 response is sent. FaucetPay's retry schedule resends the callback, and because the handler re-runs fulfillment on every retry without checking whether that transaction_id was already processed, the customer's order is fulfilled twice. Tracking processed transaction_id values and skipping fulfillment for ones already handled fixes it without needing to change the response timing.
The practical implementation sequence
Work through this when setting up or auditing either notification type.
- 1. Confirm which system applies: v2 payout webhook or Merchant API callback.
- 2. For webhooks, register a public HTTPS endpoint from the faucet's Manage page and store the webhook secret server-side.
- 3. For webhooks, capture the raw request body before parsing, compute the HMAC-SHA256, and compare it in constant time.
- 4. For callbacks, never act on posted fields directly — call the token verify endpoint first.
- 5. For callbacks, match the verified response to your own order record via the custom field.
- 6. Make both handlers idempotent against a unique identifier — payout_id for webhooks, transaction_id or token for callbacks.
- 7. Test each path with a real delivery to a public staging endpoint before relying on it in production.
If the real problem is authenticating requests you send, not receive
This page covers verifying notifications FaucetPay sends to you. If instead your own requests to FaucetPay's API are failing with a 403 invalid-key error, that is an outbound authentication problem with a completely different diagnosis, covered separately.
If the real problem is the checkout form itself
If customers cannot reach or complete the Merchant checkout page at all, the issue is with the payment form setup rather than callback verification, since verification only matters once a payment has actually been completed and FaucetPay is trying to notify you about it.
Sources checked on August 15, 2026
FaucetPay's current official API reference was the primary source for both the v2 webhook system and the Merchant API callback. A public GitHub integration was checked only to confirm how the older token-verification pattern is typically implemented in practice, not as a source of current FaucetPay behavior.
- FaucetPay — API reference (v2 webhooks, Merchant API callback and verification) — https://faucetpay.io/api-docs
- FaucetPay — Help desk — https://faucetpay.io/help
- GitHub — FaucetPay-Deposit-API-PHP, example Merchant callback handler, used only to confirm real-world implementation patterns — https://github.com/ourtecads/FaucetPay-Deposit-API-PHP/blob/main/callback.php
Be careful with websites that promise unrealistic rewards, ask for deposits before withdrawal, or require suspicious wallet connections. Small reward sites should never need your seed phrase.
FAQ
Are FaucetPay webhooks and FaucetPay Merchant callbacks the same thing?
No. The v2 payout webhook is an event-driven, HMAC-signed delivery for payout.sent and payout.failed events. The Merchant API callback is a separate IPN-style notification for completed checkout payments, verified with a one-time token instead of a signature.
Why does my FaucetPay webhook signature never match?
The most common cause is computing the HMAC over a JSON-parsed and re-serialized body instead of the original raw bytes. Capture and hash the raw request body before any JSON parsing happens.
Can I trust the fields posted in a FaucetPay Merchant callback?
Not by themselves. Anyone can POST fabricated data to a public callback URL. Always call FaucetPay's token verification endpoint and use that response as the authoritative record before fulfilling an order.
Why is my FaucetPay Merchant callback firing multiple times for one payment?
FaucetPay retries a callback on a set schedule if it doesn't receive an HTTP 200 response in time. Make your handler idempotent against the transaction_id or token so repeated deliveries don't cause duplicate fulfillment.
Why won't FaucetPay accept my webhook URL during local testing?
FaucetPay's webhook system requires a public HTTPS endpoint and rejects internal or local addresses as an SSRF protection. Use a public HTTPS tunnel or a real staging deployment instead of a local network address.
Can a scoped API key register or change a FaucetPay webhook URL?
No. Webhook endpoint configuration requires an active dashboard session with two-factor authentication and cannot be done with a scoped API key alone, which limits what a leaked key can be used for.