# Guide to Praxis Webhooks: Features and Best Practices

Praxis webhooks notify your application about payment and subscription activity from Praxis Tech's Cashier. If you're building on Praxis for payment orchestration, webhooks are how you react to transaction and subscription changes without polling.

This guide covers how Praxis webhooks work, the notifications you'll handle, how to verify the `gt-authentication` signature (it's SHA-384, not HMAC), and how to sign your acknowledgement.

## What are Praxis webhooks?

Praxis webhooks are HTTP POSTs that carry a signature in the lowercase `gt-authentication` header. The value is a 96-character lowercase hex string, which is a SHA-384 hash, not an HMAC and not SHA-256. You compute it by taking a fixed, per-webhook-type list of field values in the documented order, concatenating them into one string, appending the Merchant Secret, then running `sha384` over the result. This is not Standard Webhooks.

## Praxis webhook features

| Feature | Details |
| --- | --- |
| Signature header | `gt-authentication` (lowercase), 96-char lowercase hex |
| Signature scheme | `sha384(ordered_field_values + merchant_secret)`, a plain hash, not HMAC |
| Field order | Fixed per notification type (documented); do not alphabetize |
| Acknowledgement | Reply HTTP 200 with `{status:0,...}` and sign the response |
| ACK signature | Response header `external-request-signature` over `status` + `timestamp` |
| Notification types | Payment Notification (uses `transaction_status`), Subscription Notification (has an `event` field) |
| SDK | None for the server (browser Cashier JS SDK only) |

## Common events

Praxis has two notification types, and they identify themselves differently:

| Notification | How to identify |
| --- | --- |
| Payment Notification | No event-name field; read `transaction_status` (`pending`, `approved`, `rejected`, `error`) |
| Subscription Notification | Has an explicit `event` field (for example `SubscriptionActivated`) |

For payments, branch on `transaction_status`; for subscriptions, branch on the `event` field.

## Setting up Praxis webhooks

Configure your notification URL with Praxis and store your Merchant Secret. Because the signed field set and order differ per notification type, keep the documented field lists for Payment and Subscription notifications close at hand.

## Securing Praxis webhooks

To verify, take the field values for that notification type in the documented order, concatenate them into one string, append the Merchant Secret, run `sha384`, and compare to `gt-authentication`. For a Payment Notification the fields are `merchant_id`, `application_key`, `timestamp`, `customer.customer_token`, `session.order_id`, `transaction.tid`, `transaction.currency`, `transaction.amount`, `transaction.conversion_rate`, `transaction.processed_currency`, `transaction.processed_amount`, then the secret. Do not alphabetize the fields, that alphabetical `ksort` rule is for the separate API-request signature, not webhooks.

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

const MERCHANT_SECRET = process.env.PRAXIS_MERCHANT_SECRET;

// Field order is fixed per notification type (documented). Example: Payment Notification.
const PAYMENT_FIELDS = [
  "merchant_id",
  "application_key",
  "timestamp",
  "customer.customer_token",
  "session.order_id",
  "transaction.tid",
  "transaction.currency",
  "transaction.amount",
  "transaction.conversion_rate",
  "transaction.processed_currency",
  "transaction.processed_amount",
];

function get(obj, path) {
  return path.split(".").reduce((o, k) => (o == null ? o : o[k]), obj);
}

function verifyPayment(body, signature) {
  const message = PAYMENT_FIELDS.map((f) => get(body, f)).join("") + MERCHANT_SECRET;
  const expected = crypto.createHash("sha384").update(message).digest("hex"); // plain SHA-384, not HMAC
  const a = Buffer.from(expected);
  const b = Buffer.from((signature || "").toLowerCase());
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.json(), (req, res) => {
  if (!verifyPayment(req.body, req.headers["gt-authentication"])) {
    return res.sendStatus(401);
  }

  // Praxis expects a signed ACK: {status:0,...} + external-request-signature over status+timestamp
  const timestamp = req.body.timestamp;
  const ackSig = crypto
    .createHash("sha384")
    .update(`0${timestamp}${MERCHANT_SECRET}`)
    .digest("hex");
  res.set("external-request-signature", ackSig);
  res.json({ status: 0, timestamp });
});

```

The same verification in Python:

```python
import hashlib
import os

MERCHANT_SECRET = os.environ["PRAXIS_MERCHANT_SECRET"]

PAYMENT_FIELDS = [
    "merchant_id",
    "application_key",
    "timestamp",
    "customer.customer_token",
    "session.order_id",
    "transaction.tid",
    "transaction.currency",
    "transaction.amount",
    "transaction.conversion_rate",
    "transaction.processed_currency",
    "transaction.processed_amount",
]

def get(obj, path):
    for key in path.split("."):
        if obj is None:
            return None
        obj = obj.get(key)
    return obj

def verify_payment(body: dict, signature: str) -> bool:
    message = "".join(str(get(body, f)) for f in PAYMENT_FIELDS) + MERCHANT_SECRET
    expected = hashlib.sha384(message.encode()).hexdigest()  # plain SHA-384, not HMAC
    return expected == (signature or "").lower()

```

## Praxis webhook limitations and pain points

### It's SHA-384, not HMAC and not SHA-256

The Problem: The 96-char `gt-authentication` value is a plain SHA-384 hash of the fields plus the secret, not an HMAC and not SHA-256. Reaching for `hmac` or SHA-256 produces a value that never matches.

Why It Happens: Praxis concatenates the secret into the hashed string and uses SHA-384.

Workarounds:

* Use a plain SHA-384 hash over the ordered field values with the secret appended.

How Hookdeck Can Help: Hookdeck verifies provider signatures at the edge, so your app doesn't hand-roll the SHA-384 construction.

### The field order differs per notification type

The Problem: Payment and Subscription notifications sign different field sets in different documented orders. Using the wrong list, or reusing one across both, fails.

Why It Happens: Each notification type has its own signed field list.

Workarounds:

* Keep the documented field order per type, and pick the right list based on the notification.

How Hookdeck Can Help: Hookdeck can verify each source type with its configured scheme, so field order isn't your app's problem.

### Don't alphabetize webhook fields

The Problem: Praxis's general API-request signature sorts keys alphabetically (`ksort`) before hashing, but webhooks use the explicit documented field order. Applying the API rule to webhooks breaks verification.

Why It Happens: The two signing rules differ, and it's easy to conflate them.

Workarounds:

* Use the documented per-type order for webhooks; reserve alphabetical sorting for outbound API requests.

How Hookdeck Can Help: Hookdeck verifies inbound webhooks with the correct rule, independent of your outbound API signing.

### You must sign the ACK

The Problem: Praxis expects a `{status:0,...}` response with an `external-request-signature` header over the response `status` and `timestamp`. An unsigned or missing ACK isn't accepted as a successful delivery.

Why It Happens: Praxis verifies your acknowledgement in both directions.

Workarounds:

* Reply HTTP 200 with `{status:0,...}` and sign the response fields with SHA-384.

How Hookdeck Can Help: Hookdeck can manage the acknowledgement handshake at the edge, so your app focuses on processing.

## Best practices

### Verify with SHA-384 over the ordered fields plus the secret

Concatenate the documented field values for the notification type, append the Merchant Secret, `sha384`, and compare against `gt-authentication` in constant time.

### Use the right field list per type

Payment Notifications sign one field set, Subscription Notifications another, in their documented orders.

### Sign your acknowledgement

Reply `{status:0,...}` with the `external-request-signature` header over `status` and `timestamp`.

### Return the ACK fast 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

Praxis webhooks carry a `gt-authentication` signature that's a plain SHA-384 hash over per-type ordered field values with the Merchant Secret appended, not an HMAC and not SHA-256. Use the documented field order per notification type, don't alphabetize webhook fields, identify payments by `transaction_status` and subscriptions by their `event` field, and sign your `{status:0}` acknowledgement.

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

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