# Guide to Partnero Webhooks: Features and Best Practices

Partnero runs affiliate, referral and partner programmes for SaaS and e-commerce businesses: it signs up partners, attributes customers to them, records transactions and calculates commissions. That data usually needs to reach other systems, such as a CRM for new partners, an accounting tool for commissions or a payout process for rewards. Partnero's outgoing webhooks send each change as it happens.

This guide covers Partnero's 15 programme events, the envelope and its missing event ID, verifying the `Signature` header against PHP-encoded JSON, and the 5-second response window.

## What are Partnero webhooks?

Webhooks belong to a programme. You add them under **Program Settings > Webhooks**, or through Partnero's Webhooks API, and choose which partner, customer, transaction and reward events each one receives. The dashboard can also send a test webhook for a chosen event.

Each delivery is a JSON `POST` with the `event` name, the `url` it was sent to, the `webhook_key` of the subscription, a `created_at` time and an event-specific `data` object.

Partnero signs every request with HMAC-SHA256 over the raw body and puts the hex digest in a header called simply `Signature`.

## Partnero webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Program Settings > Webhooks**, or the Webhooks API |
| Signature | hex HMAC-SHA256 |
| Signature header | `signature` |
| Signed content | The raw request body |
| Key | The webhook's signing secret (`signature` in the API) |
| Body | JSON, PHP `json_encode` output |
| Event routing | Body field `event` |
| Timeout | 5 seconds |
| Retries | Immediate, 1 min, 5 min, 30 min, 2 h |
| After retries | Marked failed in the dashboard logs |

## Events and payloads

| Event | Triggered When | Common Use Cases |
| --- | --- | --- |
| `partner.created` | New partner signs up or is created | Welcome email, mailing list, CRM |
| `partner.updated` | Partner information is updated | Sync profile data |
| `partner.approved` | Partner application is approved | Grant access, notify the partner |
| `partner.rejected` | Partner application is rejected | Notify, clean up |
| `partner.archived` | Partner is archived | Deactivate downstream accounts |
| `partner.deleted` | Partner is deleted | Remove records |
| `customer.created` | New customer is created | Attribute a referral |
| `customer.updated` | Customer information is updated | Sync customer data |
| `customer.deleted` | Customer is deleted | Remove records |
| `transaction.created` | New transaction is recorded | Update CRM, accounting |
| `transaction.updated` | Transaction is updated | Reconcile amounts |
| `transaction.deleted` | Transaction is deleted (refund) | Reverse accounting entries |
| `reward.created` | Commission/reward is created | Track pending commissions |
| `reward.approved` | Reward is approved for payout | Schedule payout reminders |
| `reward.paid` | Reward is marked as paid | Reconcile payouts |

### Event Payload Structure

| Field | Description |
| --- | --- |
| `event` | The event name, e.g. `transaction.created` |
| `url` | The destination URL the webhook was sent to |
| `webhook_key` | Key of the webhook subscription that fired |
| `created_at` | ISO 8601 timestamp of when the event was dispatched |
| `data` | Event-specific payload |

Partnero shows `transaction.created` data as `{key, amount, currency, action, customer, partner, reward, created_at}` and `reward.approved` data as `{id, amount, currency, partner, transaction_key, approved_at}`, but only `partner.created` has a full example, and the other 12 events have none. Read `data` defensively.

Partnero's guidance says to deduplicate on event IDs, yet the envelope has no event ID. The nearest stable values are the resource's own ID or key inside `data`, plus `event` and `created_at`.

### Published payload example

A complete `partner.created` delivery from the [webhook samples library](https://github.com/hookdeck/webhook-samples/blob/main/providers/partnero/latest/partner.created.json).

```json
{
  "event": "partner.created",
  "url": "https://your-server.com/webhooks/partnero",
  "webhook_key": "wh_abc123",
  "created_at": "2025-01-15T10:30:00.000000Z",
  "data": {
    "id": "partner_abc123",
    "email": "partner@partnero.com",
    "name": "John",
    "surname": "Doe",
    "status": "approved",
    "created_at": "2025-01-15T10:30:00.000000Z",
    "metadata": {}
  }
}

```

## Setting up Partnero webhooks

### Prerequisites

* A Partnero programme you can edit
* A public HTTPS endpoint

### Register Your Endpoint

1. In Partnero, go to **Program Settings > Webhooks**.
2. Click **Add Webhook** and enter your endpoint URL.
3. Select the events to receive, for example `partner.created`, `transaction.created` and `reward.approved`.
4. Save and **copy the signing secret**.

With the Webhooks API, the create response includes the webhook's `signature` value, which is the same signing secret.

### Get Your Signing Secret

Store the secret as `WEBHOOK_SECRET` and use it as UTF-8 bytes, as shown, without hex- or base64-decoding. Each webhook has its own secret, so programmes with several webhooks need one verifier per secret.

### Test a delivery

Start a local receiver and forward requests with the [Hookdeck CLI](/docs/cli):

```bash
npx hookdeck-cli listen 3000 partnero --path /webhooks/partnero

```

Add a webhook with the CLI's URL, then use the dashboard's test option to send a `partner.created` event, or create a test partner.

## Securing Partnero webhooks

Compute HMAC-SHA256 of the raw body with the signing secret, hex-encode it, and compare it with `Signature` in constant time. Partnero's Node sample is `createHmac('sha256', WEBHOOK_SECRET).update(req.body).digest('hex')`. Because the body comes from PHP's `json_encode`, slashes and non-ASCII characters are escaped (`/` becomes `\/`, `é` becomes `\u00e9`), and `JSON.stringify` of the parsed object won't reproduce them. See the [outgoing webhooks guide](https://docs.partnero.com/guides/webhooks/outgoing-webhooks).

```javascript
const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['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 builds its dedup key from the event and the resource identifier in `data`, and routes rewards to a payouts service.

```javascript
const express = require('express');

function createPartneroApp({ claim, crm, ledger, payouts }) {
  const app = express();
  app.post('/webhooks/partnero', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
    let parsed;
    try {
      parsed = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
    const { event, created_at, data } = parsed;
    if (typeof event !== 'string' || !data || typeof data !== 'object') return res.sendStatus(400);
    try {
      const resourceId = data.id || data.key;
      if (!(await claim(`${event}:${resourceId}:${created_at}`))) return res.sendStatus(200);
      const [resource] = event.split('.');
      if (resource === 'partner') await crm.syncPartner(event, data);
      else if (resource === 'transaction') await ledger.apply(event, data);
      else if (resource === 'reward') await payouts.update(event, data);
      else if (resource === 'customer') await crm.syncCustomer(event, data);
      else console.log('Unhandled Partnero event', event);
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

```

With a 5-second limit, the CRM and ledger calls should run in a background job in production; the handler shows the routing, not the timing.

Only the body is signed, and `created_at` isn't covered by a documented tolerance, so a captured request verifies whenever it's replayed. The dedup key stops it taking effect twice.

On a Partnero source, Hookdeck verifies the `Signature` header against the raw body using your signing secret. Partnero always receives `200`, so slow processing on your side never uses up its five attempts; a bad signature is logged as failed verification and not forwarded. No timestamp tolerance applies.

> **Make Partnero webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) replies to Partnero within its 5-second window, verifies every programme event, and lets you replay commission and reward updates after an outage.

## Partnero webhook limitations and pain points

### A 5-second response window

**The Problem:** Deliveries fail while your handler is still working, and the same event is retried.

**Why It Happens:** Partnero expects a `2xx` within 5 seconds, and CRM or accounting calls can take longer.

**Workarounds:**

* Persist the body and respond before calling other systems.

**How Hookdeck Can Help:** Hookdeck acknowledges immediately and gives your destination its own timeout.

### Retries end after about two and a half hours

**The Problem:** A longer outage leaves a webhook marked failed, with events missing downstream.

**Why It Happens:** Partnero tries immediately and then after 1, 5 and 30 minutes and 2 hours, then marks the webhook failed in its logs.

**Workarounds:**

* Watch the dashboard logs for failed webhooks.
* Reconcile partners, transactions and rewards through the Partnero API after an incident.

**How Hookdeck Can Help:** Hookdeck accepts each event on the first try and stores it, so your service can catch up whenever it's back.

### No event ID

**The Problem:** Retries can be processed twice.

**Why It Happens:** The envelope has `webhook_key` and `created_at` but no unique event identifier, despite the docs recommending deduplication by event ID.

**Workarounds:**

* Combine `event`, the resource ID or key in `data`, and `created_at`.
* Make updates set state rather than increment it.

**How Hookdeck Can Help:** Hookdeck deduplication can match on chosen body fields to drop identical retries.

### PHP-encoded JSON

**The Problem:** Signatures don't match in Node or Python even with the right secret.

**Why It Happens:** Partnero escapes slashes and non-ASCII characters, so any re-serialised body differs from the bytes that were signed.

**Workarounds:**

* Read the body as raw bytes and verify before parsing.

**How Hookdeck Can Help:** Hookdeck verifies the original bytes and forwards them unchanged.

## Best practices

### Treat `transaction.deleted` as a refund

Partnero describes a deleted transaction as a refund. Reverse any commission or revenue entries you created from `transaction.created`.

### Separate partner data from money

Route partner and customer events to your CRM and transaction and reward events to finance, ideally through separate queues.

### Don't depend on IP allowlisting

Partnero's troubleshooting mentions allowing its IPs but publishes no list. Rely on the signature.

### Respond first, then process

Store the event, return `2xx`, and do the work later, as in our [asynchronous processing guide](/webhooks/guides/why-implement-asynchronous-processing-webhooks). Record each dedup key with its effect, following the [idempotency guide](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Partnero's webhooks cover the full life of an affiliate programme, from partner sign-up to reward payment, with a simple HMAC on the raw body. The things to plan for are the 5-second window, a retry schedule that ends after a couple of hours, PHP-escaped JSON and the lack of an event ID.

[Hookdeck Event Gateway](https://hookdeck.com) answers Partnero instantly and keeps every programme event stored and replayable.

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