Guide to UpPromote Webhooks: Features and Best Practices
UpPromote is a Shopify app for running affiliate and influencer programmes: affiliates sign up, share links or coupons, and earn commission on the orders they refer. Its webhooks tell your systems when referrals arrive or change status, when affiliates join or are approved, and when payouts are made, so you can sync commissions to accounting or onboard affiliates elsewhere.
This guide covers UpPromote's API-only subscriptions, why each event needs its own URL, verifying X-UpPromote-Signature, and a retry window of about 15 minutes.
What are UpPromote webhooks?
UpPromote webhook subscriptions are managed through its affiliate API v2, not in the app. Each subscription pairs one event with one target_url. The API can list, create, update and delete subscriptions.
The body is the referral, affiliate or payment object itself. There's no event name in the body and no documented event header, so the URL a request arrives on is the only reliable way to know which event it is.
UpPromote webhook features
| Feature | Details |
|---|---|
| Signature | hex HMAC-SHA256 |
| Signature header | x-uppromote-signature |
| Body | JSON |
From UpPromote's webhook overview, the target_url must:
- Use HTTPS with a valid certificate, at a public, non-local IP address.
- Accept
POSTonly. - Respond with
200within 6 seconds. - Not redirect.
A failed delivery (timeout, network error or a non-2xx response) is retried up to 3 times, 5 minutes apart. After that, UpPromote gives up on it.
Events and payloads
| Event | Meaning |
|---|---|
referral.new | Occurs whenever there is a new referral in the store. |
referral.approved | Occurs whenever a referral is approved. |
referral.denied | Occurs whenever a referral is denied. |
referral.status-changed | Occurs whenever status of a referral is changed. |
affiliate.new | Occurs when there is a new affiliate in the store (signup or create manual). |
affiliate.approved | Occurs whenever an affiliate is approved. |
affiliate.inactive | Occurs whenever an affiliate is denied/deactivated. |
affiliate.status-changed | Occurs whenever status of an affiliate is changed. |
UpPromote also documents payment.paid, sent when an affiliate payment is marked as paid or processed.
A referral looks like this in the referral.new example:
{
"id": 1781959,
"order_id": null,
"order_number": null,
"customer_id": null,
"quantity": null,
"total_sales": "0.00",
"commission_rule": {
"program_id": 29486,
"commission_rate": null,
"commission_type": "Percent Of Sale"
},
"commission_adjustment": "0.00",
"status": "pending",
"commission": "100.00",
"refund_id": null,
"tracking_type": "Performance bonus",
"affiliate": {
"id": 2029458,
"email": "testhoook-tiktok-okela@gmail.com",
"first_name": "Test",
"last_name": "Hook"
},
"coupon_applied": null,
"customer_email": null,
"created_at": "2025-09-25T04:59:43Z"
}
Amounts such as total_sales and commission are strings. Affiliate events carry the affiliate record, with contact details, status, program_id, affiliate links, coupons and paid, approved, pending and denied amounts. Since referral.approved and referral.status-changed can carry the same status, the shape alone can't tell every referral event apart.
Setting up UpPromote webhooks
- Get an API token for the UpPromote affiliate API.
- Create one subscription per event, each with its own URL:
curl -X POST 'https://aff-api.uppromote.com/api/v2/webhook-subscriptions' \
-H 'Accept: application/json' \
-H "Authorization: $UPPROMOTE_API_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"target_url": "https://example.com/webhooks/uppromote/referral.new", "event": "referral.new"}'
- After the first subscription exists, open the UpPromote app, go to Settings > Integrations and use Get Secret Key. Store it as
UPPROMOTE_SECRET_KEY.
To route through Hookdeck, create an UpPromote source with the secret key and use its URL, plus a per-event path, as each target_url. For local development:
npx hookdeck-cli listen 3000 uppromote --path /webhooks/uppromote
Securing UpPromote webhooks
X-UpPromote-Signature is the lowercase hex HMAC-SHA256 of the raw request body, keyed with the Secret Key. The PHP and Python samples on the overview page compare it in constant time. The Ruby sample reads HTTP_X_SIGNATURE, which is a different header, so follow the others.
Node.js verification
const crypto = require('node:crypto');
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const signature = headers['x-uppromote-signature'];
if (typeof signature !== 'string') return false;
const key = Buffer.from(secret, 'utf8');
const message = rawBody;
const candidates = [signature];
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
const received = Buffer.from(candidate, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The handler takes the event from the URL path, then deduplicates on the event, object ID and status.
const express = require('express');
const app = express();
const EVENTS = new Set([
'referral.new', 'referral.approved', 'referral.denied', 'referral.status-changed',
'affiliate.new', 'affiliate.approved', 'affiliate.inactive', 'affiliate.status-changed',
'payment.paid',
]);
app.post('/webhooks/uppromote/:event', express.raw({ type: '*/*' }), async (req, res) => {
const eventName = req.params.event;
if (!EVENTS.has(eventName)) return res.sendStatus(404);
const secret = process.env.UPPROMOTE_SECRET_KEY;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
let record;
try {
record = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (!record || typeof record !== 'object' || Array.isArray(record)) return res.sendStatus(400);
if (record.id === undefined || record.id === null) return res.sendStatus(400);
const key = `${eventName}:${record.id}:${record.status}`;
try {
if (await alreadyHandled(key)) return res.sendStatus(200);
await queue.add('uppromote-event', { event: eventName, id: record.id, status: record.status, record });
await markHandled(key);
} catch {
return res.sendStatus(503);
}
res.sendStatus(200);
});
Replace queue, alreadyHandled and markHandled with your own job queue and deduplication store.
Hookdeck's UpPromote source type runs the same check. Instead of a 401, UpPromote receives 200, and the failed check is recorded on the request in Hookdeck. Nothing time-based is signed by UpPromote, and Hookdeck sets no age limit.
Make UpPromote webhooks production-ready. Hookdeck Event Gateway answers UpPromote within its 6-second limit and keeps referrals and payouts until your service has processed them, well past UpPromote's last retry.
UpPromote webhook limitations and pain points
Nothing names the event
The Problem: A request on a shared URL could be any referral or affiliate event.
Why It Happens: The body is the bare object, and UpPromote documents no event header.
Workarounds:
- Give each subscription its own path, such as
/webhooks/uppromote/referral.approved. - Keep the mapping of subscriptions to paths in code, and recreate it through the API.
How Hookdeck Can Help: Hookdeck keeps the request path, so one source can take every subscription and route each path to its own destination.
Retries end after about 15 minutes
The Problem: A short outage can lose referrals or payout notices.
Why It Happens: UpPromote retries 3 times, 5 minutes apart, then stops.
Workarounds:
- Reconcile with the API's referral and payment endpoints after any outage.
- Alert on endpoint errors so you know when to reconcile.
How Hookdeck Can Help: Hookdeck stores the first attempt and retries your service on your schedule, with manual replay.
A 6-second limit and exactly 200
The Problem: Handlers that write to accounting before responding can time out, and responses other than 200 risk being counted as failures.
Why It Happens: UpPromote asks for 200 within 6 seconds. The overview says both that 200 is required and that non-2xx responses are retried.
Workarounds:
- Queue the event and return exactly
200. - Do heavy work in a background worker.
How Hookdeck Can Help: Hookdeck replies to UpPromote at once and lets your service take longer.
Secret Key only after the first subscription
The Problem: You can't configure verification until a subscription already exists.
Why It Happens: UpPromote shows Get Secret Key under Settings > Integrations only after one subscription has been created.
Workarounds:
- Create the first subscription against a URL that holds requests until you have the key.
- Then set the key and start processing.
How Hookdeck Can Help: Point the first subscription at a Hookdeck source. Requests are stored as they arrive, and once the key is set you can replay them.
Best practices
One URL per event
This is the only reliable way to know the event. Reject unknown paths before doing any work.
Deduplicate on ID and status
UpPromote warns that retries can send a webhook more than once. A key from the event, object ID and status covers repeated status changes. See the idempotency guide.
Treat amounts as strings
Parse total_sales and commission with a decimal library rather than floating point.
Respond first
Queue the event, then update accounting or your CRM in a worker. More on this in why to process webhooks asynchronously.
Conclusion
UpPromote sends bare referral, affiliate and payment objects, one event per subscription, signed with a lowercase hex HMAC-SHA256 in X-UpPromote-Signature. Give each event its own URL, return 200 within 6 seconds, and reconcile after outages, because retries stop after three attempts.
Hookdeck Event Gateway verifies UpPromote requests, routes each event by path and retries delivery to your systems.
Get started with Hookdeck for free and handle UpPromote webhooks reliably in minutes.