Agent skill
Praxis Webhooks Skill
Receive and verify Praxis (Praxis Tech / Cashier payment orchestration) webhooks. Use when setting up a Praxis webhook endpoint, verifying the gt-authentication SHA-384 signature, signing the acknowledgement with the external-request-signature header, or handling Payment Notification (transaction_status pending, approved, rejected, error) and Subscription Notification events.
Install this skill
npx skills add hookdeck/webhook-skills --skill praxis-webhooks
Praxis (Praxis Tech, the "Cashier" payment orchestration platform) signs every outbound webhook with a SHA-384 hex digest in the lowercase gt-authentication header. This is not an HMAC and not Standard Webhooks: Praxis takes a fixed, per-webhook-type list of field values in the documented order, concatenates them into one string, appends your Merchant Secret, and hashes the result with sha384. Your endpoint must reply 200 with a { "status": 0, ... } body and sign that acknowledgement with the external-request-signature header.
When to Use This Skill
- How do I receive Praxis / Praxis Tech / Cashier webhooks?
- How do I verify the
gt-authenticationsignature on a Praxis webhook? - Why is my Praxis SHA-384 signature verification failing?
- How do I sign the Praxis acknowledgement (
external-request-signature)? - How do I handle Payment Notification
transaction_status(pending, approved, rejected, error)? - How do I handle a Praxis Subscription Notification
event?
Verification (core)
Concatenate the documented field values in order (do not alphabetize — that is only for the general API-request signature), append the Merchant Secret, then sha384 (hex). Compare to the gt-authentication header.
const crypto = require('crypto');
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'];
const SUBSCRIPTION_FIELDS = ['event', 'merchant_id', 'application_key', 'cid', 'plan_id',
'subscription_id', 'subscription_status', 'timestamp'];
const at = (o, p) => p.split('.').reduce((x, k) => (x == null ? undefined : x[k]), o);
// Subscription Notifications carry an `event` field; Payment Notifications do not.
function verifyPraxis(body, headerSig, merchantSecret) {
const fields = body.event ? SUBSCRIPTION_FIELDS : PAYMENT_FIELDS;
const data = fields.map((p) => String(at(body, p) ?? '')).join('') + merchantSecret;
const expected = crypto.createHash('sha384').update(data, 'utf8').digest('hex');
const a = Buffer.from(String(headerSig || ''), 'utf8');
const b = Buffer.from(expected, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b); // timing-safe
}
Sign the acknowledgement you return (sha384 of status + timestamp + secret, sent in the external-request-signature header):
const status = 0;
const timestamp = Math.floor(Date.now() / 1000);
const ackSig = crypto.createHash('sha384')
.update(`${status}${timestamp}${merchantSecret}`, 'utf8').digest('hex');
// res.set('external-request-signature', ackSig).status(200).json({ status, timestamp });
Parse before verify (deliberate exception): the signature covers field values, not the raw body, so you must parse the JSON to rebuild the signed string. This is the opposite of HMAC-over-raw-body providers. See references/verification.md for the number-vs-string gotcha.
For complete handlers with signature verification, event dispatch, the signed acknowledgement, and tests, see:
Common Event Types
Payment Notification — no event-name field; identified by the transaction.transaction_status value:
transaction_status | Meaning |
|---|---|
initialized | Transaction created |
pending | Awaiting completion |
approved | Transaction approved / funds captured |
rejected | Transaction declined |
error | Processing error |
Subscription Notification — identified by the explicit event field, carrying subscription_status, subscription_id, plan_id, and cid:
event | Fires When |
|---|---|
SubscriptionCreated | A subscription is created |
SubscriptionActivated | A subscription becomes active |
SubscriptionDeactivated | A subscription is deactivated |
SubscriptionExpired | A subscription expires |
SubscriptionCanceled | A subscription is canceled |
PaymentAttemptApproved | A recurring charge attempt is approved |
PaymentAttemptFailed | A recurring charge attempt fails |
PaymentSucceeded | A subscription payment succeeds |
PaymentFailed | A subscription payment fails |
PaymentManuallyPaid | A payment is marked manually paid |
PaymentRefundSucceeded | A refund succeeds |
PaymentRefundFailed | A refund fails |
subscription_status is one of active, inactive, expired, canceled. Confirm the enabled values for your program in the Praxis webhook docs.
Environment Variables
# The Merchant Secret from your Praxis merchant configuration. Used both to
# verify inbound gt-authentication signatures and to sign your acknowledgement.
PRAXIS_MERCHANT_SECRET=your_merchant_secret
Signature Header Reference
| Direction | Header | Content |
|---|---|---|
| Inbound (Praxis → you) | gt-authentication | sha384(field_values + merchant_secret), 96-char lowercase hex |
| Outbound (your ACK → Praxis) | external-request-signature | sha384(status + timestamp + merchant_secret) |
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 praxis --path /webhooks/praxis
Reference Materials
- references/overview.md - What Praxis webhooks are, notification types, payload shape
- references/setup.md - Merchant configuration, notification URLs, the Merchant Secret
- references/verification.md - SHA-384 field-value signing, ACK signing, and gotchas