# Guide to PayPro Global Webhooks: Features and Best Practices

PayPro Global webhooks notify your application about order and subscription activity: an order is charged, refunded, or charged back, a subscription renews or is terminated. PayPro Global calls them IPN (Instant Payment Notification). If you're building on PayPro Global, IPN is how you react to these events without polling.

This guide covers how PayPro Global IPN works, the events you'll handle, how to verify the `HASH` and `SIGNATURE` parameters, and the best practices for production.

> This covers PayPro Global (payproglobal.com). It is not the unrelated PayPro B.V. / paypro.nl, whose npm and pip "paypro" packages belong to a different company.

## What are PayPro Global webhooks?

PayPro Global IPN messages are HTTP POSTs sent as form-encoded data. Verification is bespoke (not an HMAC header, not Standard Webhooks) and has three layers: an IP allowlist of fixed PayPro Global addresses, a `HASH` parameter, and a `SIGNATURE` parameter. Crucially, the two use different keys: `HASH` uses your SecretKey, and `SIGNATURE` uses your VALIDATION_KEY, both found under Store Settings > General Settings > Integration.

## PayPro Global webhook features

| Feature | Details |
| --- | --- |
| Delivery | HTTP POST, form-encoded |
| Layer 1 | IP allowlist of fixed PayPro Global IPs |
| Layer 2 | `HASH` = `MD5(OrderId + SecretKey)` for real orders (test orders use `MD5("1")`) |
| Layer 3 | `SIGNATURE` = SHA256 of a fixed 7-field ordered concatenation |
| Keys | `SecretKey` (for HASH) and `VALIDATION_KEY` (for SIGNATURE) are different keys |
| Configuration | IPN URL per product, or Store Settings > Notifications |
| SDK | None (the npm/pip "paypro" packages are an unrelated company) |

## Common events

PayPro Global IPN types include (note the deliberately non-standard `SubscriptionChargeSucceed` spelling):

| Event | Fires when |
| --- | --- |
| `OrderCharged` | An order is charged |
| `OrderRefunded` | An order is refunded |
| `OrderPartiallyRefunded` | An order is partially refunded |
| `OrderChargedBack` | An order is charged back |
| `OrderDeclined` | An order is declined |
| `SubscriptionChargeSucceed` | A subscription charge succeeds (non-standard spelling) |
| `SubscriptionChargeFailed` | A subscription charge fails |
| `SubscriptionSuspended` | A subscription is suspended |
| `SubscriptionRenewed` | A subscription renews |
| `SubscriptionTerminated` | A subscription is terminated |

Match `SubscriptionChargeSucceed` exactly, it isn't spelled `Succeeded`.

## Setting up PayPro Global webhooks

Set your IPN URL per product, or globally under Store Settings > Notifications. Retrieve both keys from Store Settings > General Settings > Integration: the `SecretKey` for the `HASH` check and the `VALIDATION_KEY` for the `SIGNATURE` check. Keep them straight, they are not interchangeable.

## Securing PayPro Global webhooks

Verify all three layers. Confirm the source IP is on PayPro Global's allowlist. Check `HASH` against `MD5(OrderId + SecretKey)`. Then recompute `SIGNATURE` as the SHA256 of the fields concatenated in this exact order: `ORDER_ID + ORDER_STATUS + ORDER_TOTAL_AMOUNT + CUSTOMER_EMAIL + VALIDATION_KEY + TEST_MODE + IPN_TYPE_NAME`. The concatenation order and the inclusion of `TEST_MODE` and `IPN_TYPE_NAME` are the easy parts to get wrong.

```javascript
const crypto = require("crypto");

const SECRET_KEY = process.env.PAYPRO_SECRET_KEY; // for HASH
const VALIDATION_KEY = process.env.PAYPRO_VALIDATION_KEY; // for SIGNATURE

function md5(s) {
  return crypto.createHash("md5").update(s).digest("hex");
}
function sha256(s) {
  return crypto.createHash("sha256").update(s).digest("hex");
}

function verify(body) {
  // Layer 2: HASH = MD5(OrderId + SecretKey)
  const expectedHash = md5(`${body.ORDER_ID}${SECRET_KEY}`);

  // Layer 3: SIGNATURE = SHA256 of the fixed 7-field ordered concatenation
  const message =
    `${body.ORDER_ID}${body.ORDER_STATUS}${body.ORDER_TOTAL_AMOUNT}` +
    `${body.CUSTOMER_EMAIL}${VALIDATION_KEY}${body.TEST_MODE}${body.IPN_TYPE_NAME}`;
  const expectedSig = sha256(message);

  const hashOk = timingSafeEqual(expectedHash, body.HASH);
  const sigOk = timingSafeEqual(expectedSig, body.SIGNATURE);
  return hashOk && sigOk;
}

function timingSafeEqual(a, b) {
  const ba = Buffer.from(a || "");
  const bb = Buffer.from(b || "");
  return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
}

// Also confirm req.ip is on PayPro Global's fixed allowlist (Layer 1).
app.post("/webhook", express.urlencoded({ extended: false }), (req, res) => {
  if (!verify(req.body)) return res.sendStatus(401);
  res.sendStatus(200); // acknowledge fast
  processQueue.add(req.body); // branch on IPN_TYPE_NAME, async
});

```

The same verification in Python:

```python
import hashlib
import hmac
import os

SECRET_KEY = os.environ["PAYPRO_SECRET_KEY"]  # for HASH
VALIDATION_KEY = os.environ["PAYPRO_VALIDATION_KEY"]  # for SIGNATURE

def verify(body: dict) -> bool:
    expected_hash = hashlib.md5(f"{body['ORDER_ID']}{SECRET_KEY}".encode()).hexdigest()

    message = (
        f"{body['ORDER_ID']}{body['ORDER_STATUS']}{body['ORDER_TOTAL_AMOUNT']}"
        f"{body['CUSTOMER_EMAIL']}{VALIDATION_KEY}{body['TEST_MODE']}{body['IPN_TYPE_NAME']}"
    )
    expected_sig = hashlib.sha256(message.encode()).hexdigest()

    return hmac.compare_digest(expected_hash, body.get("HASH", "")) and hmac.compare_digest(
        expected_sig, body.get("SIGNATURE", "")
    )

```

## PayPro Global webhook limitations and pain points

### Two different keys for the two checks

The Problem: `HASH` uses `SecretKey` and `SIGNATURE` uses `VALIDATION_KEY`. Swapping them makes one check pass and the other fail, or both fail confusingly.

Why It Happens: PayPro Global uses separate keys for the two layers.

Workarounds:

* Keep `SecretKey` for the `HASH` and `VALIDATION_KEY` for the `SIGNATURE`, both from Store Settings > General Settings > Integration.

How Hookdeck Can Help: Hookdeck verifies provider signatures at the edge, so your app doesn't juggle two keys across two checks.

### The SIGNATURE field order is exact

The Problem: `SIGNATURE` is the SHA256 of seven fields in a fixed order ending in `TEST_MODE` and `IPN_TYPE_NAME`. A wrong order, or omitting those trailing fields, never matches.

Why It Happens: The signed string is a positional concatenation.

Workarounds:

* Concatenate exactly `ORDER_ID + ORDER_STATUS + ORDER_TOTAL_AMOUNT + CUSTOMER_EMAIL + VALIDATION_KEY + TEST_MODE + IPN_TYPE_NAME`.

How Hookdeck Can Help: Hookdeck reconstructs and verifies the signed string at the edge.

### It's MD5 and SHA256, not an HMAC header

The Problem: The checks are plain `MD5` and `SHA256` over concatenations, not an HMAC in a header. Standard HMAC-header logic finds nothing to verify.

Why It Happens: PayPro Global uses hash concatenations as form parameters.

Workarounds:

* Compute the `MD5` and `SHA256` values over the documented concatenations and compare the `HASH` and `SIGNATURE` fields.

How Hookdeck Can Help: Hookdeck handles the bespoke scheme at the edge, so your app receives pre-verified events.

### The non-standard event spelling

The Problem: `SubscriptionChargeSucceed` is spelled deliberately without the `-ed`. A handler keyed to `SubscriptionChargeSucceeded` never fires.

Why It Happens: PayPro Global's event catalog uses that spelling.

Workarounds:

* Use the exact `IPN_TYPE_NAME` values, and handle unknown types defensively.

How Hookdeck Can Help: Hookdeck routes on the exact `IPN_TYPE_NAME` you receive.

## Best practices

### Verify all three layers

Confirm the source IP, check `HASH` against `MD5(OrderId + SecretKey)`, and recompute `SIGNATURE` as the SHA256 of the ordered 7-field concatenation.

### Keep the two keys straight

`SecretKey` is for the `HASH`; `VALIDATION_KEY` is for the `SIGNATURE`.

### Match event spellings exactly

Use `SubscriptionChargeSucceed` and the other exact `IPN_TYPE_NAME` values.

### Return 200 and process asynchronously

Acknowledge quickly and defer work to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

PayPro Global IPN is verified in three layers: an IP allowlist, a `HASH` of `MD5(OrderId + SecretKey)`, and a `SIGNATURE` that's the SHA256 of a fixed 7-field concatenation ending in `TEST_MODE + IPN_TYPE_NAME` using a separate `VALIDATION_KEY`. Keep the two keys straight, get the concatenation order right, match the non-standard event spellings, and acknowledge fast.

[Hookdeck](https://hookdeck.com) verifies the checks, deduplicates, and durably queues every IPN at the edge, so your app only ever processes verified events.

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