Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Webhooks

Cashela notifies your server with a signed POST when a transaction changes status. Pay-In and Pay-Out use the same headers and the same signature scheme; the body is specific to each product.

ProductWhere the URL comes fromBody and events
Pay-Innotification_url of each depositPay-In webhooks
Pay-Outcallback_url of each transactionCreate a transaction

Headers

HeaderDescription
X-Cashela-EventThe event, for example partner.payin.transaction.updated or partner.payout.transaction.updated. The body does not carry it.
X-Cashela-TimestampUnix time (seconds) of the signature.
X-Cashela-NonceUnique value per delivery (UUID).
X-Cashela-SignatureHMAC-SHA256 of {timestamp}.{nonce}.{body} with your api_secret, lower-case hex.
X-Cashela-Trace-IdCorrelation id; quote it to support.

Verify the signature

  1. Read the three signature headers; reject the request if one is missing.
  2. Compute HMAC-SHA256 over {timestamp}.{nonce}.{raw body} with the api_secret of the key that created the transaction while it is active (or your account’s most recent active key if it was removed). Use the raw body exactly as received.
  3. Compare with X-Cashela-Signature in constant time.
  4. Reject timestamps more than 300 seconds old and nonces you have already seen.
// Express: mount this route with express.raw({ type: "application/json" })
// so req.body is the raw Buffer.
const crypto = require("crypto");
function isValidCashelaNotification(req, apiSecret) {
const timestamp = req.get("X-Cashela-Timestamp");
const nonce = req.get("X-Cashela-Nonce");
const signature = req.get("X-Cashela-Signature");
if (!timestamp || !nonce || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac("sha256", apiSecret)
.update(`${timestamp}.${nonce}.${req.body.toString("utf8")}`)
.digest("hex");
return expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Test your implementation with the signature test vectors.

Delivery

  • Answer any 2xx quickly. Any other status, a timeout or a connection error is a failed delivery. Pay-In retries failed deliveries; for Pay-Out, do not rely on automatic retries: read the transaction or ask for a re-send.
  • The same notification can arrive more than once: make your handler idempotent.
  • Notifications are never sent unsigned.
  • Lost one? Ask for it again: POST /api/v1/pay-in/transactions/{reference}/callback (Pay-In) or POST /api/v1/pay-out/transactions/{reference}/callback (Pay-Out), or read the transaction.
  • The return of your customer to your site is never a payment confirmation; only the notification or a read of the transaction is.