Guide to Tebex Webhooks: Features and Best Practices
Tebex webhooks notify your systems about monetization activity in your store: a payment completes, a payment is refunded or disputed, a recurring payment renews. If you're building on Tebex, webhooks are how you react to purchases without polling.
This guide covers how Tebex webhooks work, the events you'll handle, its two-step signature (get this exactly right), the validation ping, IP allowlisting, and the best practices for production.
What are Tebex webhooks?
Tebex webhooks are HTTP POSTs delivered to endpoints you configure per store. Each carries an X-Signature header, and the signature construction is unusual: it's a two-step hash, not a plain HMAC of the body. Tebex also allowlists its source IPs and sends a validation ping you must answer to activate an endpoint.
Tebex webhook features
| Feature | Details |
|---|---|
| Configuration | Creator Panel > Developers > Webhooks > Endpoints (per-endpoint secret + event types) |
| Signature header | X-Signature (hex) |
| Signature scheme | HMAC-SHA256(sha256(rawBody), secret), hex (two-step) |
| Source IPs | 18.209.80.3, 54.87.231.232 (docs suggest returning 404 for others) |
| Setup ping | validation.webhook: respond 200 with {"id": "<webhook id>"} |
| Acknowledgement | Always respond 2XX; any other status triggers retries |
| SDK | None |
Common events
Tebex event types are dotted:
| Event | Fires when |
|---|---|
payment.completed | A payment completes |
payment.declined | A payment is declined |
payment.refunded | A payment is refunded |
payment.dispute.opened | A dispute is opened (.won / .lost / .closed follow) |
recurring-payment.renewed | A recurring payment renews |
validation.webhook | The setup ping (respond with the webhook id) |
Recurring events also include recurring-payment.started / .ended and recurring-payment.cancellation.requested / .aborted. Select event types per endpoint in the Creator Panel.
Setting up Tebex webhooks
Configure endpoints under Creator Panel > Developers > Webhooks > Endpoints, set the secret, and select event types. On setup, Tebex sends a validation.webhook event; respond 200 with {"id": "<the webhook id>"} echoing the id, or the endpoint won't activate. Optionally restrict the endpoint to Tebex's source IPs (returning 404 for others).
Securing Tebex webhooks
The signature is two-step: first SHA256-hash the raw body, then HMAC-SHA256 that hash with your webhook secret, hex-encoded. It is not a plain HMAC of the body. Use the raw body (JSON body-parsers break the signature).
const crypto = require("crypto");
const SECRET = process.env.TEBEX_WEBHOOK_SECRET;
const ALLOWED_IPS = new Set(["18.209.80.3", "54.87.231.232"]);
function verify(rawBody, signature) {
// Two-step: HMAC-SHA256( sha256(body), secret )
const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
const expected = crypto.createHmac("sha256", SECRET).update(bodyHash).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!ALLOWED_IPS.has(req.ip)) return res.sendStatus(404);
if (!verify(req.body, req.headers["x-signature"])) return res.sendStatus(401);
const payload = JSON.parse(req.body);
// Setup ping: echo the webhook id
if (payload.type === "validation.webhook") {
return res.status(200).json({ id: payload.id });
}
res.sendStatus(200); // always respond 2XX
processQueue.add(payload); // process asynchronously
});
The same two-step check in Python (matching Tebex's PHP reference):
import hashlib
import hmac
import os
SECRET = os.environ["TEBEX_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, signature: str) -> bool:
body_hash = hashlib.sha256(raw_body).hexdigest()
expected = hmac.new(SECRET, body_hash.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
Tebex webhook limitations and pain points
The two-step signature
The Problem: The signature is HMAC-SHA256(sha256(body), secret), you HMAC the SHA256 hash of the body, not the body. Almost every "obvious" implementation (a plain HMAC of the body) fails.
Why It Happens: Tebex's documented construction hashes the body first, then HMACs the hash.
Workarounds:
- SHA256 the raw body, then HMAC-SHA256 that hash with the secret, hex-encode, and compare.
How Hookdeck Can Help: Hookdeck verifies the Tebex signature at the edge, so your application receives pre-verified events without reproducing the two-step hash.
The validation ping gates activation
The Problem: The endpoint won't activate until you answer the validation.webhook ping with {"id": "<webhook id>"}. Miss it and you receive nothing.
Why It Happens: Tebex validates the endpoint before delivering events.
Workarounds:
- Detect
validation.webhookand echo theidwith a 200.
How Hookdeck Can Help: Hookdeck can answer the validation ping and forward verified events, so activation isn't a manual concern.
IP allowlisting and raw body
The Problem: Tebex delivers only from two IPs (and suggests returning 404 to others), and the signature is over the raw body, so a JSON parser upstream breaks verification.
Why It Happens: Tebex uses a fixed egress and signs the raw body.
Workarounds:
- Allowlist the two IPs, and capture the raw body before parsing.
How Hookdeck Can Help: Hookdeck verifies against the bytes as received and gives you a dedicated ingestion URL, so IP and raw-body handling are managed for you.
Retries on non-2XX
The Problem: Any non-2XX response triggers retries, so a handler that errors out gets repeated deliveries.
Why It Happens: Tebex treats non-2XX as "not delivered".
Workarounds:
- Return 2XX once verified and enqueued; handle processing errors internally, and dedupe.
How Hookdeck Can Help: Hookdeck returns success to Tebex and retries to your downstream on its own schedule, so a transient error doesn't cause a retry storm. See our guide to webhook idempotency.
Best practices
Verify the two-step signature over the raw body
SHA256 the raw body, HMAC-SHA256 that hash with your secret, hex-encode, and compare in constant time.
Answer the validation ping
Echo {"id": "<webhook id>"} with a 200 to activate the endpoint.
Allowlist the source IPs
Restrict deliveries to 18.209.80.3 and 54.87.231.232.
Always return 2XX, process asynchronously
Return 2XX once verified and defer work to a queue; handle failures internally. See why to process webhooks asynchronously.
Make Tebex webhooks production-ready
Hookdeck verifies the two-step signature, deduplicates, and durably queues every payment event
Conclusion
Tebex webhooks use a distinctive two-step signature, HMAC-SHA256(sha256(rawBody), secret), over the raw body, plus a validation.webhook ping you answer with the webhook id and a two-IP allowlist. Get the two-step hash right, activate via the ping, allowlist the IPs, and always return 2XX.
Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique payment events.
Get started with Hookdeck for free and handle Tebex webhooks reliably in minutes.