# Guide to Paymob Webhooks: Features and Best Practices

Paymob callbacks notify your systems about payment activity: a transaction is processed, refunded, or voided. If you're building payments on Paymob, callbacks are how you react to money movement without polling.

This guide covers how Paymob callbacks work, its unusual verification (an HMAC over specific ordered fields, compared against a query parameter), the two callback types, and the best practices for production.

## What are Paymob webhooks?

Paymob callbacks are HTTP requests Paymob sends to a URL you configure. Verification is unlike most webhooks in two ways: it's not over the raw body, and the signature isn't in a header. Paymob computes an HMAC-SHA512 over a specific ordered concatenation of selected payload fields, hex-encoded, and you compare it against the `hmac` value passed as a query parameter (`?hmac=<hex>`).

There are also two callback types per integration, which share the ordered field list but differ in key paths.

## Paymob webhook features

| Feature | Details |
| --- | --- |
| Configuration | Paymob dashboard (integration callbacks) |
| Verification | `hmac` query parameter (`?hmac=<hex>`) |
| Signature scheme | HMAC-SHA512 over an ordered concatenation of selected fields, hex |
| Key | HMAC secret from the Paymob dashboard |
| Callback types | Transaction Processed (server-to-server POST, nested keys) and Transaction Response (client GET, flattened keys) |
| Event names | None, `type` is `TRANSACTION`; read state from boolean fields |
| SDK | `paymob` (npm/pip) is minimal, effectively roll your own |

## Reading the callback

There are no discrete event names. The callback `type` is `TRANSACTION`, and you determine what happened from boolean fields:

| Field | Meaning |
| --- | --- |
| `success` | The transaction succeeded |
| `is_refunded` | The transaction was refunded |
| `is_voided` | The transaction was voided |
| `is_capture` | The transaction was captured |
| `is_auth` | The transaction was an authorization |
| `pending` | The transaction is pending |

Branch on these booleans rather than an event name.

## The two callback types

Both callback types share the same ordered field list for the HMAC but differ in key paths:

* Transaction Processed Callback (server-to-server POST, JSON): nested keys (`obj.id`, `order.id`, `source_data.*`).
* Transaction Response Callback (client redirect GET, flattened query params): flattened keys (`id`, `order_id`, ...).

If you implement the GET variant, verify against its flattened key names.

## Setting up Paymob webhooks

Configure the callback URLs in the Paymob dashboard per integration, and copy the HMAC secret. Both callback types use the same secret and ordered field list.

## Securing Paymob webhooks

Compute an HMAC-SHA512 over the concatenation (no separator) of these fields in this exact order, then compare the hex digest to the `hmac` query parameter:

`amount_cents`, `created_at`, `currency`, `error_occured`, `has_parent_transaction`, `id` (`obj.id`), `integration_id`, `is_3d_secure`, `is_auth`, `is_capture`, `is_refunded`, `is_standalone_payment`, `is_voided`, `order.id`, `owner`, `pending`, `source_data.pan`, `source_data.sub_type`, `source_data.type`, `success`.

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

const HMAC_SECRET = process.env.PAYMOB_HMAC_SECRET;

// For the POST (processed) callback, obj = req.body.obj
function verify(obj, hmacFromQuery) {
  const ordered = [
    obj.amount_cents,
    obj.created_at,
    obj.currency,
    obj.error_occured,
    obj.has_parent_transaction,
    obj.id,
    obj.integration_id,
    obj.is_3d_secure,
    obj.is_auth,
    obj.is_capture,
    obj.is_refunded,
    obj.is_standalone_payment,
    obj.is_voided,
    obj.order.id,
    obj.owner,
    obj.pending,
    obj.source_data.pan,
    obj.source_data.sub_type,
    obj.source_data.type,
    obj.success,
  ].join(""); // concatenate as strings, no separator

  const expected = crypto.createHmac("sha512", HMAC_SECRET).update(ordered).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(hmacFromQuery || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.json(), (req, res) => {
  if (!verify(req.body.obj, req.query.hmac)) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge fast
  processQueue.add(req.body.obj); // branch on success/is_refunded/..., async
});

```

The same field order in Python:

```python
import hashlib
import hmac
import os

HMAC_SECRET = os.environ["PAYMOB_HMAC_SECRET"].encode()

ORDER = [
    "amount_cents", "created_at", "currency", "error_occured",
    "has_parent_transaction", "id", "integration_id", "is_3d_secure",
    "is_auth", "is_capture", "is_refunded", "is_standalone_payment",
    "is_voided", "order.id", "owner", "pending", "source_data.pan",
    "source_data.sub_type", "source_data.type", "success",
]  # resolve nested paths (order.id, source_data.*) from the obj

```

Because the digest is over a fixed field list in a fixed order, the order and the exact field set matter more than anything, get either wrong and every callback fails.

## Paymob webhook limitations and pain points

### The HMAC is over ordered fields, not the body

The Problem: Verification isn't over the raw body and the signature isn't in a header. It's an HMAC-SHA512 over a specific ordered concatenation of selected fields, compared to a `hmac` query parameter. Standard "HMAC the raw body" logic finds nothing that matches.

Why It Happens: Paymob signs a canonical field concatenation rather than the payload bytes.

Workarounds:

* Concatenate the exact fields in the documented order, HMAC-SHA512, hex, and compare to `?hmac=`.

How Hookdeck Can Help: Hookdeck can verify provider signatures at the edge, so your app receives pre-verified callbacks without reconstructing the field concatenation.

### SHA-512, and POST vs GET key paths

The Problem: It's SHA-512 (not 256), and the two callback types use different key paths (`obj.id`/`order.id` for POST vs `id`/`order_id` for GET). Using the wrong hash or the wrong key paths fails.

Why It Happens: Paymob uses SHA-512 and flattens keys differently for the GET redirect.

Workarounds:

* Use SHA-512, and resolve the correct key paths for the callback type you're handling.

How Hookdeck Can Help: Hookdeck normalizes callbacks so downstream handlers aren't juggling POST-vs-GET key shapes.

### No event names

The Problem: There are no discrete event names; `type` is always `TRANSACTION`. Handlers keyed to event names have nothing to switch on.

Why It Happens: Paymob models outcome via boolean fields, not typed events.

Workarounds:

* Read `success`, `is_refunded`, `is_voided`, `is_capture`, and `pending` to determine the outcome.

How Hookdeck Can Help: Hookdeck's filters can route on those boolean fields, giving you event-like routing.

### Duplicates

The Problem: The same transaction outcome can arrive more than once, and doubles are costly for payments.

Why It Happens: At-least-once delivery favors eventual delivery.

Workarounds:

* Dedupe on the transaction `id` and make side effects idempotent.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Best practices

### Verify the ordered-field HMAC-SHA512 against `?hmac=`

Concatenate the documented fields in the exact order, HMAC-SHA512 with the dashboard secret, hex, and compare to the `hmac` query parameter in constant time.

### Handle the right callback type

Use the correct key paths for the POST (processed) or GET (response) callback you're implementing.

### Read outcome from booleans

Branch on `success`/`is_refunded`/`is_voided`/`is_capture`/`pending`, not an event name.

### Acknowledge fast, dedupe, confirm

Return 2xx quickly, dedupe on the transaction id, and confirm state via the API before fulfilling. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Paymob callbacks verify with an HMAC-SHA512 over a fixed, ordered concatenation of selected fields, compared to the `hmac` query parameter, not the raw body, and not a header. Get the field set and order exactly right, use the correct key paths for the POST vs GET callback, read outcome from boolean fields, and dedupe on the transaction id.

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

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