Skip to main content
Hermes tells your system that something durable happened — a payout reached its terminal state, a trade settled, your balance dropped below its floor — by POSTing an event to your webhook endpoint.

Registering an endpoint

An org admin registers the endpoint in the Jenzy portal (Integration settings): one HTTPS URL, one active endpoint per org. Registration mints a signing secret (whsec_…), shown exactly once — store it beside your API key. Replacing the endpoint mints a new secret; deliveries already in flight keep using the old one until they finish, so verify against both secrets while a rotation is in progress.

The event envelope

Every delivery is a JSON POST with the same envelope:
{
  "id": "6a1f0d0e-3f0a-4c6f-9b1e-8d2c5a7e9f31",
  "event_type": "payout.succeeded",
  "created_at": "2026-07-08T09:14:31.000Z",
  "balance": { "available": 1245000, "held": 45000, "currency": "MWK" },
  "data": { "...": "the resource — see below" }
}
  • id — your dedupe handle. Delivery is at-least-once: the same event can arrive more than once, and events can arrive out of order. Process each id once and ignore repeats.
  • balance — a convenience snapshot of your balance as of when the event was prepared. Never do balance math off webhooks — events can arrive out of order; GET /balance is the authoritative read.
  • data — the resource, rendered byte-identically to the corresponding /v1 read endpoint.

Event catalog

EventWhendata
payout.succeededA payout reached its terminal success state.The payout, as GET /payouts/{id} renders it.
payout.failedA payout failed; its hold is already released.The payout, with failure: { code, message } set — see failure codes.
trade.settledAn OTC funding trade landed; your balance is credited.The trade, as GET /trades renders it.
balance.lowYour available balance is below your configured floor. At most one per cooldown window while the condition holds; no recovery event.{ "balance": …, "floor_mwk": … }

Verifying signatures

Every delivery carries a Signature header: HMAC-SHA256 of the exact raw request body, keyed with your signing secret, hex-encoded. There are no timestamp headers and nothing else to canonicalize — sign the bytes you received, compare in constant time.
Compute the HMAC over the raw body bytes, before any JSON parsing. Parsing and re-serializing will reorder or reformat and the signature will never match.
Node.js (Express)
import { createHmac, timingSafeEqual } from 'node:crypto'
import express from 'express'

const app = express()

// express.raw so req.body is the untouched Buffer the signature covers
app.post('/webhooks/jenzy', express.raw({ type: 'application/json' }), (req, res) => {
  const secret = process.env.JENZY_WEBHOOK_SECRET // whsec_…
  const expected = createHmac('sha256', secret).update(req.body).digest('hex')
  const received = req.get('Signature') ?? ''

  const a = Buffer.from(expected, 'utf8')
  const b = Buffer.from(received, 'utf8')
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return res.status(401).end()
  }

  const event = JSON.parse(req.body)

  // At-least-once delivery: drop events you have already processed.
  // if (await alreadyProcessed(event.id)) return res.status(200).end()

  switch (event.event_type) {
    case 'payout.succeeded':
    case 'payout.failed':
      // event.data is the payout — same shape as GET /payouts/{id}
      break
    case 'trade.settled':
      // event.data is the trade — same shape as GET /trades items
      break
    case 'balance.low':
      // event.data is { balance, floor_mwk }
      break
  }

  res.status(200).end() // respond 2xx fast; do heavy work async
})
During a secret rotation, verify against both the old and new secret and accept if either matches.

Delivery semantics

  • Only a 2xx response counts as delivered. Redirects are failures and are never followed. Respond quickly (within 10 seconds) and do real work asynchronously.
  • A failed delivery is re-attempted 5 times over ~6 hours, front-loaded. Every attempt sends the identical bytes — the payload is snapshotted when the event occurs, so the signature stays valid across attempts.
  • After the last failed attempt the delivery is dead-lettered and Jenzy ops are alerted; it is never auto-revived. If you missed events during an outage, contact Jenzy to arrange re-delivery — and reconcile against GET /payouts in the meantime (polling and webhooks carry the same data, so the list endpoint is always a valid catch-up path).