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.
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:
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.
Make Praxis webhooks production-ready
Hookdeck verifies gt-authentication, deduplicates, and durably queues every notification
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 verifies the signature, deduplicates, and durably queues every notification at the edge, so your app only ever processes verified events.
Get started with Hookdeck for free and handle Praxis webhooks reliably in minutes.