# PayPro Global Webhooks (IPN)

PayPro Global calls its webhooks IPN — Instant Payment Notification. When
an order or subscription event occurs, PayPro Global sends an HTTP POST with
a `application/x-www-form-urlencoded` body (not JSON) to the IPN URL you
configure. Verification is bespoke: it is not HMAC-in-a-header and not
Standard Webhooks.

## When to Use This Skill

* How do I receive PayPro Global IPN webhooks?
* How do I verify the PayPro Global `SIGNATURE` (SHA256) parameter?
* How do I verify the PayPro Global `HASH` (MD5) parameter?
* Why is my PayPro Global signature verification failing?
* How do I handle `OrderCharged`, `OrderRefunded`, or `SubscriptionChargeSucceed` events?
* How do I restrict IPN requests to PayPro Global's IP addresses?

## Verification (core)

PayPro Global has three independent layers — verify all that you can:

1. IP allowlist — requests come only from fixed PayPro Global IPs
  (IPv4 `198.199.123.239`, `157.230.8.40`; IPv6 `2604:a880:400:d0::1843:7001`,
  `2604:a880:400:d1::b6c:c001`).
2. `SIGNATURE` — `SHA256` (hex) over seven field values concatenated in
  this exact order: `ORDER_ID` + `ORDER_STATUS` + `ORDER_TOTAL_AMOUNT` +
  `CUSTOMER_EMAIL` + `VALIDATION_KEY` + `TEST_MODE` + `IPN_TYPE_NAME`.
3. `HASH` — `MD5` of `ORDER_ID` + `SecretKey` for real orders, or
  `MD5("1")` for test orders.

> `VALIDATION_KEY` (for SIGNATURE) and `SecretKey` (for HASH) are two
> different keys. Both live under Store Settings → General Settings →
> Integration. Mixing them up is the most common verification bug.

The signature covers specific field values, not the raw request body — so
parsing the form first is correct here (unlike HMAC-over-raw-body providers).
Recompute server-side and compare timing-safely (Node):

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

// SIGNATURE = SHA256(ORDER_ID + ORDER_STATUS + ORDER_TOTAL_AMOUNT +
//   CUSTOMER_EMAIL + VALIDATION_KEY + TEST_MODE + IPN_TYPE_NAME). Order and the
// inclusion of TEST_MODE + IPN_TYPE_NAME are easy to get wrong — keep them exact.
function verifySignature(f, validationKey) {
  const base = `${f.ORDER_ID ?? ''}${f.ORDER_STATUS ?? ''}${f.ORDER_TOTAL_AMOUNT ?? ''}` +
    `${f.CUSTOMER_EMAIL ?? ''}${validationKey}${f.TEST_MODE ?? ''}${f.IPN_TYPE_NAME ?? ''}`;
  const expected = crypto.createHash('sha256').update(base, 'utf8').digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(String(f.SIGNATURE ?? '').toLowerCase());
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

```

> For complete handlers with HASH verification, IP allowlisting, event
> dispatch, and tests, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/paypro-global-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/paypro-global-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/paypro-global-webhooks/examples/fastapi/)

## Common Event Types

The event name arrives in the `IPN_TYPE_NAME` field. Note the non-standard
spelling `SubscriptionChargeSucceed` (not "Succeeded").

| `IPN_TYPE_NAME` | Triggered When | Common Use Cases |
| --- | --- | --- |
| `OrderCharged` | A one-time order (or first subscription charge) is paid | Fulfil order, grant access, send license |
| `OrderRefunded` | An order is fully refunded | Revoke access, update accounting |
| `OrderPartiallyRefunded` | An order is partially refunded | Adjust balance, partial revoke |
| `OrderChargedBack` | A chargeback is opened | Suspend account, gather evidence |
| `OrderChargedBackWon` | A chargeback dispute is won | Restore access |
| `OrderDeclined` | A payment attempt is declined | Notify customer, retry flow |
| `SubscriptionChargeSucceed` | A recurring subscription charge succeeds | Extend subscription period |
| `SubscriptionChargeFailed` | A recurring charge fails | Dunning, notify customer |
| `SubscriptionRenewed` | A subscription renews | Extend access |
| `SubscriptionSuspended` | A subscription is suspended | Pause access |
| `SubscriptionTerminated` | A subscription is terminated | Revoke access |
| `SubscriptionFinished` | A subscription reaches its natural end | Offer renewal |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/paypro-global-webhooks/references/overview.md) for the full event list.

## Environment Variables

```bash
PAYPRO_VALIDATION_KEY=your_validation_key   # For SIGNATURE (SHA256). Store Settings → General Settings → Integration
PAYPRO_SECRET_KEY=your_secret_key           # For HASH (MD5). Same tab, DIFFERENT key. Optional but recommended.

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 paypro-global --path /webhooks/paypro-global

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/paypro-global-webhooks/references/overview.md) - IPN concepts, full event list, payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/paypro-global-webhooks/references/setup.md) - Configure the IPN URL and find your keys in the dashboard
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/paypro-global-webhooks/references/verification.md) - SIGNATURE, HASH, IP allowlist, and gotchas