Guide to Tremendous Webhooks: Features and Best Practices
Tremendous webhooks tell your systems when rewards are delivered or fail, when orders are approved or rejected, and when funds land in your balance. If you send gift cards, payouts or research incentives through Tremendous, webhooks are how those outcomes reach your own records without polling the API.
This guide covers how the Tremendous webhook endpoint is configured and signed, the events and their payload, how retries work, and the practices that keep a Tremendous receiver reliable.
What are Tremendous webhooks?
Each Tremendous organization can have one webhook endpoint, and you configure it through the API. That endpoint hears about every event in the account: you can't choose a subset, so your receiver filters on the event name. When something happens, Tremendous sends an HTTPS POST with a JSON body.
The body is a notification. It names the event and the resource it concerns, by type and ID, and for most events nothing more. You fetch the resource from the API when you need its details.
Every request is signed. The Tremendous-Webhook-Signature header holds sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with the webhook's private_key.
Tremendous webhook features
| Feature | Details |
|---|---|
| Configuration | API only: POST /webhooks, plus list, retrieve and delete |
| Endpoints | One per organization. Posting again with a new url changes it |
| Subscription | Every event in the account. No pre-selection |
| Environments | Production (api.tremendous.com) and sandbox (testflight.tremendous.com), each with its own webhook |
| Test events | POST /webhooks/{id}/simulate with an event name |
| Signature header | Tremendous-Webhook-Signature |
| Scheme | HMAC-SHA256, hex, always prefixed with sha256= |
| Signed content | The raw request body only |
| Secret | The webhook's private_key, returned by the create, list and retrieve calls |
| Success response | 200 |
| Retries | Up to 17 times, with exponential backoff |
| Idempotency key | uuid in the body |
| Source IPs | Production: 34.150.143.187, 34.86.45.235, 35.188.236.188, 34.150.208.194. Sandbox: 35.194.78.88, 34.150.132.8, 34.145.182.28, 35.221.12.21 |
Common events
Tremendous documents 36 event types. Names are upper-case and dotted.
| Area | Events |
|---|---|
| Rewards | REWARDS.DELIVERY.SUCCEEDED, REWARDS.DELIVERY.FAILED, REWARDS.CANCELED, REWARDS.FLAGGED |
| Fraud review | FRAUD_REVIEWS.RELEASED, FRAUD_REVIEWS.BLOCKED |
| Orders | ORDERS.CREATED, ORDERS.APPROVED, ORDERS.CANCELED, ORDERS.FAILED |
| Funding | FUNDING_SOURCES.CREATED, FUNDING_SOURCES.DELETED, FUNDING_SOURCES.FUNDED |
| Top-ups | TOPUPS.CREATED, TOPUPS.FULLY_CREDITED, TOPUPS.PARTIALLY_, TOPUPS.REVERSED, TOPUPS.REJECTED |
| Invoices | INVOICES.CREATED, INVOICES.DELETED, INVOICES.PAID |
| Campaigns and team | CAMPAIGNS.CREATED, CAMPAIGNS.DELETED, MEMBERS.CREATED, MEMBERS.DELETED |
| Catalog and reports | PRODUCTS.ADDED, PRODUCTS.UPDATED, PRODUCTS.REMOVED, REPORTS.GENERATION.SUCCEEDED, REPORTS.GENERATION.FAILED |
| Connected organizations | CONNECTED_, CONNECTED_, CONNECTED_, CONNECTED_, CONNECTED_, CONNECTED_ |
ORDERS.APPROVED fires only when order approvals are turned on. A GET /webhooks/{id}/events call lists the event types your endpoint can receive.
Here is Tremendous's example request body:
{
"event": "CAMPAIGNS.CREATED",
"uuid": "5ccc7bb1-7659-4e23-a407-77d8cd9c62f5",
"created_utc": "2021-04-06T20:05:01.037-04:00",
"payload": {
"resource": {
"id": "2V3PCCL7QXDA",
"type": "campaigns"
},
"meta": {}
}
}
eventis the event name. Route on it.uuidis the webhook's unique ID. Tremendous asks you to check it so you don't act on the same webhook twice.payload.resourcenames the resource bytype(such asrewards,ordersorfunding_sources) andid.payload.metais{}for most events. ForORDERS.CREATEDit holds the created order, and forREWARDS.DELIVERY.FAILEDit holds the failure details.
Setting up Tremendous webhooks
There is no dashboard form for webhooks. Create the endpoint with your API key, starting in the sandbox:
curl -X POST https://testflight.tremendous.com/api/v2/webhooks \
-H "Authorization: Bearer $TREMENDOUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/webhooks/tremendous" }'
The response includes the webhook's id and private_key. Store the key as TREMENDOUS_. Unlike many providers, Tremendous returns it again from the list and retrieve calls, so you can recover it later.
To check your receiver, ask Tremendous to fire a test event:
curl -X POST https://testflight.tremendous.com/api/v2/webhooks/$WEBHOOK_ID/simulate \
-H "Authorization: Bearer $TREMENDOUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "event": "INVOICES.PAID" }'
Repeat the setup against https://api.tremendous.com/api/v2 for production. The two environments have separate webhooks, separate keys and separate source IPs.
If you use Hookdeck, create a source with the Tremendous source type, paste the private_key into it, and use the Hookdeck source URL as the webhook url.
Securing Tremendous webhooks
Read Tremendous-Webhook-Signature, check that it starts with sha256=, and take the rest. Compute the hex HMAC-SHA256 of the raw request body with the private_key as the key, and compare the two in constant time.
const crypto = require('crypto');
const express = require('express');
function verifyTremendousSignature(rawBody, signatureHeader, privateKey) {
if (!signatureHeader || !privateKey || !signatureHeader.startsWith('sha256=')) return false;
const received = Buffer.from(signatureHeader.slice('sha256='.length).toLowerCase(), 'utf8');
const expected = Buffer.from(
crypto.createHmac('sha256', privateKey).update(rawBody).digest('hex'),
'utf8'
);
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
app.post('/webhooks/tremendous', express.raw({ type: '*/*' }), (req, res) => {
const valid = verifyTremendousSignature(
req.body,
req.get('tremendous-webhook-signature'),
process.env.TREMENDOUS_WEBHOOK_PRIVATE_KEY
);
if (!valid) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on event.uuid, route on event.event, and return 200 for events you ignore.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
def verify_tremendous_signature(raw_body: bytes, signature_header, private_key: str) -> bool:
if not signature_header or not private_key or not signature_header.startswith("sha256="):
return False
received = signature_header[len("sha256="):].lower()
expected = hmac.new(private_key.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(received.encode("utf-8"), expected.encode("utf-8"))
# In FastAPI: raw_body = await request.body()
# verify_tremendous_signature(raw_body, request.headers.get("tremendous-webhook-signature"),
# os.environ["TREMENDOUS_WEBHOOK_PRIVATE_KEY"])
Tremendous's Ruby example compares with ==. A constant-time comparison, as above, is the better choice. The signature covers only the body and there is no timestamp, so there is no replay window to check. Tremendous's fixed source IPs give you an extra network-level check if you want one.
Make Tremendous webhooks production-ready. Hookdeck Event Gateway verifies the
Tremendous-Webhook-Signatureheader on every delivery, answers with a200straight away, and routes reward, order and funding events to the services that need them.
Tremendous webhook limitations and pain points
One endpoint for everything
The Problem: Several services need different Tremendous events, but the organization can only register one URL.
Why It Happens: Tremendous allows a single webhook per organization, and it receives every event. Tremendous suggests building your own receiver that fans events out to the rest of your infrastructure.
Workarounds:
- Run one receiver that verifies, stores and then dispatches events by name to internal queues.
- Return
200quickly for events nobody needs.
How Hookdeck Can Help: Point the one Tremendous webhook at Hookdeck and add a connection per service, each with a filter on event. Hookdeck does the fan-out without a receiver of your own.
Only 200 counts as success
The Problem: Tremendous retries events your service already handled.
Why It Happens: Tremendous asks for a 200 specifically. A 201, 202 or 204 from your framework may be treated as a failure, and the same webhook is sent again, up to 17 times.
Workarounds:
- Return exactly
200from the webhook route. - Deduplicate on
uuidregardless, since retries also follow timeouts.
How Hookdeck Can Help: Hookdeck answers Tremendous with a 200 as soon as it has the request, whatever your service returns, and its deduplication can drop repeated uuid values.
Thin payloads
The Problem: A REWARDS.DELIVERY.SUCCEEDED event doesn't tell you which recipient or amount it was.
Why It Happens: For most events, the body holds only the resource type and ID. meta is populated only for ORDERS.CREATED and REWARDS.DELIVERY.FAILED.
Workarounds:
- Fetch the reward or order from the API in a background job, using
payload.resource.id. - Store the IDs Tremendous returned when you created the order, so you can match the event to your own records without a fetch.
How Hookdeck Can Help: Hookdeck keeps each request body, so you can check exactly what meta contained for a failed delivery when you investigate it later.
No replay protection
The Problem: A captured request still verifies if someone resends it.
Why It Happens: Tremendous signs only the body, without a timestamp.
Workarounds:
- Deduplicate on
uuid. - Allow only Tremendous's published IPs for the environment you are in.
How Hookdeck Can Help: Hookdeck's deduplication can reject a uuid it has already seen before the request reaches your service.
Best practices
Verify against the raw body
Tremendous warns that any change to the body, such as calling JSON.stringify on parsed JSON, produces a different signature. Use express.raw({ type: '*/*' }) in Express, await request.text() in Next.js and await request.body() in FastAPI.
Test in the sandbox with simulate
Use POST /webhooks/{id}/simulate in the sandbox to send each event your code handles, including REWARDS.DELIVERY.FAILED, before you go live.
Keep sandbox and production apart
The environments have different keys and IPs. Use separate secrets, and if you allowlist IPs, use the list for the right environment.
Acknowledge fast and process asynchronously
Tremendous recommends processing webhooks out of the request cycle, for example in a worker queue, so it gets a timely response and doesn't retry. See why to process webhooks asynchronously and our guide to webhook idempotency.
Conclusion
Tremendous sends every event in an organization to one endpoint and signs each request with Tremendous-Webhook-Signature, a sha256=-prefixed hex HMAC-SHA256 of the raw body keyed with the webhook's private_key. Verify against the raw bytes, return exactly 200, deduplicate on uuid, and fetch details from the API when meta is empty.
Hookdeck Event Gateway verifies every Tremendous signature, acknowledges with a 200, and fans each event out to the services that need it, which a single-endpoint provider otherwise leaves to you.
Get started with Hookdeck for free and handle Tremendous webhooks reliably in minutes.