# 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](https://aff-api.uppromote.com/docs/v2/webhook-overview-1623756m0), the `target_url` must:

* Use HTTPS with a valid certificate, at a public, non-local IP address.
* Accept `POST` only.
* Respond with `200` within 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:

```json
{
  "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

1. Get an API token for the UpPromote affiliate API.
2. Create one subscription per event, each with its own URL:

```bash
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"}'

```

1. 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:

```bash
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

```javascript
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.

```javascript
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](/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](/webhooks/guides/implement-webhook-idempotency).

### 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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## 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](https://hookdeck.com) verifies UpPromote requests, routes each event by path and retries delivery to your systems.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle UpPromote webhooks reliably in minutes.