Skip to content

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

Webhooks

When a deposit reaches a final status, Cashela sends a signed POST to the deposit’s notification_url with the outcome. Verify the signature, update your order, and answer 2xx.

When notifications are sent

Event (X-Cashela-Event header)When
partner.payin.transaction.updatedThe deposit reaches its final status: COMPLETED, DECLINED or CANCELLED (including an expired deposit).
partner.payin.transaction.voidedA card deposit was voided. Its status becomes REVERSED.
partner.payin.transaction.refundedA card deposit was refunded: REVERSED when refunded in full, COMPLETED after a partial refund.
partner.payin.dispute.opened, partner.payin.dispute.won, partner.payin.dispute.lostA card dispute on the deposit was opened or resolved.

The event travels only in the X-Cashela-Event header, never in the body. Ignore event names you do not know: new ones may be added.

Delivery and retries

  • Cashela sends POST with Content-Type: application/json to the deposit’s notification_url (HTTPS only).
  • Answer any 2xx to confirm receipt. Any other status, a timeout or a connection error counts as a failed delivery, and Cashela retries.
  • Answer quickly and do the heavy work asynchronously.
  • The same notification may arrive more than once. Make your handler idempotent: process each ref + status (+ event) only once.
  • Notifications are never sent unsigned.
  • If you missed one, ask for it again with Re-send a notification, or read the deposit with Get a deposit.

Headers

HeaderDescription
X-Cashela-EventThe event that fired this notification (table above).
X-Cashela-TimestampUnix time (seconds) the signature was computed at.
X-Cashela-NonceUnique value per delivery (UUID).
X-Cashela-SignatureHMAC-SHA256 of {timestamp}.{nonce}.{body} with your api_secret, lower-case hex (64 characters).
X-Cashela-Trace-IdCorrelation id of the deposit. Quote it to support.

Verify the signature

The secret is the api_secret of the API key that created the deposit while that key is still active; if it no longer exists, it is the api_secret of your account’s most recent active key. It is never sent in a notification.

  1. Read X-Cashela-Timestamp, X-Cashela-Nonce and X-Cashela-Signature. Reject the request if any is missing.
  2. Compute HMAC-SHA256 over the string {timestamp}.{nonce}.{body} with your api_secret, where {body} is the raw request body exactly as received. Parsing and re-serialising the JSON changes the bytes, and the signature will not match.
  3. Compare your result with X-Cashela-Signature using a constant-time comparison.
  4. Reject timestamps more than 300 seconds (5 minutes) away from your clock, 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));
}

These examples check the timestamp; also store each nonce for at least the tolerance window and reject repeats.

Test vectors

Check your implementation against these cases before going live. Download them as JSON: webhook-signatures.json.

CaseSecretTimestampExpected result
validwhsec_test_0123456789abcdef1759838400Valid. Signature b2e80494a3da3d33cfb2c38b1c6c334efb46ed098b4f29f24760da6831de0f6c
tampered-bodysamesameInvalid: the body changed (status is CANCELLED) but the signature is the one of valid.
reserialized-bodysamesameInvalid: the same JSON pretty-printed. Hash the raw bytes.
wrong-secretwhsec_test_wrongsameInvalid.
stale-timestampwhsec_test_0123456789abcdef100Invalid: the signature matches, but the timestamp is outside the 300-second tolerance.

All cases use the nonce 8f14e45f-ceea-467d-9a0b-000000000001 and, except where noted, this exact body (no spaces, no trailing newline):

{"ref":"PI-TEST-0001","external_identifier":"partner-order-0001","status":"COMPLETED","date":"2025-10-07T12:00:00.000000Z","amount":4750.5,"currency":"CLP","settlement_currency":"USD","net_amount":5,"processing_fee_amount":0.25,"fx_spread_amount":0,"tax_amount":0,"card":null}

Notification body

The body has the same keys, in the same order, on every status and event.

{
"ref": "210000000001",
"external_identifier": "order-2026-0002",
"status": "COMPLETED",
"date": "2026-10-20T15:04:05.000000Z",
"amount": 5,
"currency": "USD",
"settlement_currency": "USD",
"net_amount": 4.85,
"processing_fee_amount": 0.15,
"fx_spread_amount": 0,
"tax_amount": 0,
"card": { "network": "visa", "last4": "4242" }
}
FieldTypeDescription
refstringThe deposit’s reference.
external_identifierstringYour id, as sent when you created the deposit.
statusstringThe deposit status after the event: COMPLETED, DECLINED, CANCELLED or REVERSED.
datestringISO-8601 date and time of the event.
amountnumberWhat your customer was charged, in currency.
currencystringCurrency of amount.
settlement_currencystringCurrency of the four amounts below.
net_amountnumberWhat is credited to you, in settlement_currency.
processing_fee_amountnumberProcessing fee, in settlement_currency.
fx_spread_amountnumberExchange-rate spread, in settlement_currency. 0 when there was no currency conversion.
tax_amountnumberTaxes, in settlement_currency.
cardobject | nullnetwork (visa, mastercard, amex, discover, or null if unknown) and last4 of the card that paid. null for non-card methods and before the charge. Never a full card number, CVV, expiry or token.

How to read the amounts:

  • amount / currency is what your customer was charged. For card options charged in USD, it is the USD charged (for example 5.00 USD), not the local-currency invoice. The invoice stays available as details.invoice_* in Get a deposit, and details.charged_amount / details.charged_currency match amount / currency here.
  • When currency and settlement_currency differ (for example a local-currency method credited in USD), net_amount + processing_fee_amount + fx_spread_amount + tax_amount does not add up to amount: they are in different currencies.
  • On DECLINED and CANCELLED, the amounts are the quote the deposit was created with, not money that moved. Nothing was charged and nothing is credited. Reconcile money only on COMPLETED and REVERSED.
  • On REVERSED, the body keeps the original deposit amounts. The detail of the void or refund is in reversals.refunds of Get a deposit.

Re-send a notification

Asks Cashela to send a deposit’s notification again, now, to its notification_url.

POST/api/v1/pay-in/transactions/{reference}/callback
  • {reference}: the deposit’s reference, your external_identifier, or the deposit id.
  • Only for a deposit in a final status. Otherwise 409 (PARTNER_PAYIN_CALLBACK_NOT_FINAL).
  • Limited to 6 requests per minute.
  • The answer is 200 even when your endpoint did not accept the delivery: check data.callback.
Terminal window
curl -X POST https://sandbox-api.cashela.com/api/v1/pay-in/transactions/210000000001/callback \
-u "$CASHELA_API_KEY:$CASHELA_API_SECRET" \
-H "Accept: application/json"
{
"success": true,
"data": {
"transaction": { "reference": "210000000001", "status": "COMPLETED", "…": "…" },
"callback": {
"status": "…",
"success": true,
"reference": "210000000001",
"http_status": 200
}
},
"message": "…"
}
data.callback fieldDescription
successtrue if your endpoint answered 2xx.
http_statusWhat your endpoint answered, when it answered.
reasonWhen it failed without an answer, a stable code such as callback_url_not_configured or callback_url_blocked.

Checklist

  • Your notification_url is HTTPS, public, and answers 2xx within a few seconds.
  • You verify the signature over the raw body, the timestamp and the nonce.
  • Your handler is idempotent.
  • You read the event from X-Cashela-Event, not from the body.
  • You reconcile money only on COMPLETED and REVERSED.
  • You can recover a missed notification with the re-send endpoint or by reading the deposit.