Skip to content

Callbacks

6. Callbacks

When a transaction is approved or rejected, Cuzdan360 sends an asynchronous POST to the callback URL. The destination is resolved in this priority order:

  1. Platform central callback URL — if your payment platform (turnkey/aggregator provider) has one registered with Cuzdan360, all results go there, signed with the platform's secret and signature scheme.
  2. X-Callback-URL header — per-transaction override (same-host restriction, see section 4).
  3. Account webhook URL — the default registered on your account.

In cases 2 and 3 the callback is signed with your account's secret key as described below. In case 1 the platform verifies and relays the result to you through its own channels — the rest of this section then applies to the platform, not to your site.

6.1 Payload

{
  "reference": "TX-DEP-001",
  "amount": "1000.00",
  "status": "approve",
  "hashcode": "a1b2c3d4e5..."
}
Field Description
reference The reference you submitted
amount Amount as a string, always 2 decimals (e.g. 1000.00)
status approve or reject
hashcode Signature — verify as below

The hashcode is computed over the exact amount string sent (always 2 decimals). Build the verification message from the values received verbatim — do not reformat them.

Extra fields may be added to this payload if your account is configured with them; they never replace the four core fields above.

6.2 Verifying the hashcode

Default scheme:

message  = "{reference},{amount},{status}"
hashcode = HMAC-SHA256(secret_key, message)  →  hex

The signature scheme can be customized per account: algorithm hmac_sha256 (default) or sha256, and format reference,amount,status (default) or secret,reference,amount,status. sha256 is only available with the secret,reference,amount,status format. The active scheme is confirmed at onboarding. The examples below use the default.

PHP:

$message  = $_POST['reference'] . ',' . $_POST['amount'] . ',' . $_POST['status'];
$expected = hash_hmac('sha256', $message, $secretKey);
if (!hash_equals($expected, $_POST['hashcode'])) {
    http_response_code(400);
    exit('invalid signature');
}
// safe: apply the transaction, then return 200 with {"success": true}

Node.js:

const crypto = require('crypto')
const message = `${req.body.reference},${req.body.amount},${req.body.status}`
const expected = crypto.createHmac('sha256', SECRET_KEY).update(message).digest('hex')
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.body.hashcode))
if (!ok) return res.status(400).send('invalid signature')
// apply the transaction, then:
res.status(200).json({ success: true, message: 'OK' })

Python:

import hmac, hashlib
message = f"{reference},{amount},{status}".encode()
expected = hmac.new(secret_key.encode(), message, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, hashcode):
    return ("invalid signature", 400)
# apply the transaction, then return 200 with {"success": true}

6.3 Your response — the acknowledgement body

Return HTTP 200 with:

{ "success": true, "message": "OK" }

The field name success is a contract, not our internal naming. Our decoder silently ignores unknown fields. If you return the old shape ({"status": true, "msg": "OK"}), no success field is found, it stays at its zero value false, and the delivery is counted as failed and retried — nothing in the exchange looks wrong, since your body is valid JSON and the HTTP status is 200.

An empty body is safe: with no body to decode, the response is treated as "not decoded" and the delivery counts as successful. A body that is not valid JSON at all is likewise treated as successful. The only dangerous case is returning valid JSON that does not contain success: true.

The consequence is not cosmetic. The transaction stays pending until the callback is delivered; after 3 failed attempts a still-pending transaction is automatically rejected. A deposit your operator approved can end up rejected purely because of the shape of the acknowledgement.

Two conditions must both hold for a delivery to count as successful:

  1. HTTP status is 2xx.
  2. The body is either empty / not valid JSON, or it parses and contains success: true.
You return Result
{"success": true, "message": "OK"} Success
(empty body) Success — nothing to decode
OK (not JSON) Success — decode fails, logged as a warning
{"success": false, "message": "..."} Failed, retried
{"status": true, "msg": "OK"} (old shape) Failed, retried — no success field, defaults to false
Any non-2xx (3xx/4xx/5xx) Failed, retried

6.4 Retry policy

Property Value
Max attempts 3
Backoff (exponential) 1s, 2s, 4s
HTTP timeout Configured per account: 1–60 seconds, 15 seconds by default
Redirects Not followed
Durability Stored in an outbox; pending callbacks are re-processed even after a restart
After the last failed attempt A still-pending transaction is automatically rejected

6.5 Status mapping

Transition Callback status
pending → approved approve
pending → rejected reject

A callback is sent only once, on the transaction's first and only result (pending → approved/rejected). Later back-office status corrections on the panel (e.g. approved → rejected) do not emit a callback — treat the first callback you receive as final and irreversible.

Idempotency: the callback for a given reference may be re-delivered until you acknowledge it successfully (see 6.3) — always with the same status. Deduplicate on reference; never apply the same result twice.