Gareth Wilson Gareth Wilson

Guide to Tremendous Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationAPI only: POST /webhooks, plus list, retrieve and delete
EndpointsOne per organization. Posting again with a new url changes it
SubscriptionEvery event in the account. No pre-selection
EnvironmentsProduction (api.tremendous.com) and sandbox (testflight.tremendous.com), each with its own webhook
Test eventsPOST /webhooks/{id}/simulate with an event name
Signature headerTremendous-Webhook-Signature
SchemeHMAC-SHA256, hex, always prefixed with sha256=
Signed contentThe raw request body only
SecretThe webhook's private_key, returned by the create, list and retrieve calls
Success response200
RetriesUp to 17 times, with exponential backoff
Idempotency keyuuid in the body
Source IPsProduction: 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.

AreaEvents
RewardsREWARDS.DELIVERY.SUCCEEDED, REWARDS.DELIVERY.FAILED, REWARDS.CANCELED, REWARDS.FLAGGED
Fraud reviewFRAUD_REVIEWS.RELEASED, FRAUD_REVIEWS.BLOCKED
OrdersORDERS.CREATED, ORDERS.APPROVED, ORDERS.CANCELED, ORDERS.FAILED
FundingFUNDING_SOURCES.CREATED, FUNDING_SOURCES.DELETED, FUNDING_SOURCES.FUNDED
Top-upsTOPUPS.CREATED, TOPUPS.FULLY_CREDITED, TOPUPS.PARTIALLY_CREDITED, TOPUPS.REVERSED, TOPUPS.REJECTED
InvoicesINVOICES.CREATED, INVOICES.DELETED, INVOICES.PAID
Campaigns and teamCAMPAIGNS.CREATED, CAMPAIGNS.DELETED, MEMBERS.CREATED, MEMBERS.DELETED
Catalog and reportsPRODUCTS.ADDED, PRODUCTS.UPDATED, PRODUCTS.REMOVED, REPORTS.GENERATION.SUCCEEDED, REPORTS.GENERATION.FAILED
Connected organizationsCONNECTED_ORGANIZATIONS.REGISTERED, CONNECTED_ORGANIZATIONS.STATUS.APPROVED, CONNECTED_ORGANIZATIONS.STATUS.REJECTED, CONNECTED_ORGANIZATIONS.OAUTH.GRANTED, CONNECTED_ORGANIZATIONS.DELETED, CONNECTED_ORGANIZATION_MEMBERS.DELETED

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": {}
  }
}
  • event is the event name. Route on it.
  • uuid is the webhook's unique ID. Tremendous asks you to check it so you don't act on the same webhook twice.
  • payload.resource names the resource by type (such as rewards, orders or funding_sources) and id.
  • payload.meta is {} for most events. For ORDERS.CREATED it holds the created order, and for REWARDS.DELIVERY.FAILED it 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_WEBHOOK_PRIVATE_KEY. 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-Signature header on every delivery, answers with a 200 straight 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 200 quickly 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 200 from the webhook route.
  • Deduplicate on uuid regardless, 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.