Gareth Wilson Gareth Wilson

Guide to Praxis Webhooks: Features and Best Practices

Published


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

FeatureDetails
Signature headergt-authentication (lowercase), 96-char lowercase hex
Signature schemesha384(ordered_field_values + merchant_secret), a plain hash, not HMAC
Field orderFixed per notification type (documented); do not alphabetize
AcknowledgementReply HTTP 200 with {status:0,...} and sign the response
ACK signatureResponse header external-request-signature over status + timestamp
Notification typesPayment Notification (uses transaction_status), Subscription Notification (has an event field)
SDKNone for the server (browser Cashier JS SDK only)

Common events

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

NotificationHow to identify
Payment NotificationNo event-name field; read transaction_status (pending, approved, rejected, error)
Subscription NotificationHas 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.