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-authentication signature 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_statusMeaning
initializedTransaction created
pendingAwaiting completion
approvedTransaction approved / funds captured
rejectedTransaction declined
errorProcessing error

Subscription Notification — identified by the explicit event field, carrying subscription_status, subscription_id, plan_id, and cid:

eventFires When
SubscriptionCreatedA subscription is created
SubscriptionActivatedA subscription becomes active
SubscriptionDeactivatedA subscription is deactivated
SubscriptionExpiredA subscription expires
SubscriptionCanceledA subscription is canceled
PaymentAttemptApprovedA recurring charge attempt is approved
PaymentAttemptFailedA recurring charge attempt fails
PaymentSucceededA subscription payment succeeds
PaymentFailedA subscription payment fails
PaymentManuallyPaidA payment is marked manually paid
PaymentRefundSucceededA refund succeeds
PaymentRefundFailedA 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

DirectionHeaderContent
Inbound (Praxis → you)gt-authenticationsha384(field_values + merchant_secret), 96-char lowercase hex
Outbound (your ACK → Praxis)external-request-signaturesha384(status + timestamp + merchant_secret)

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 praxis --path /webhooks/praxis

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Aug 6, 2026

View on GitHub →