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:
- 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.
X-Callback-URLheader — per-transaction override (same-host restriction, see section 4).- 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
hashcodeis computed over the exactamountstring 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:
The signature scheme can be customized per account: algorithm
hmac_sha256(default) orsha256, and formatreference,amount,status(default) orsecret,reference,amount,status.sha256is only available with thesecret,reference,amount,statusformat. 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:
The field name
successis a contract, not our internal naming. Our decoder silently ignores unknown fields. If you return the old shape ({"status": true, "msg": "OK"}), nosuccessfield is found, it stays at its zero valuefalse, 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
pendinguntil 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:
- HTTP status is 2xx.
- 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
referencemay be re-delivered until you acknowledge it successfully (see 6.3) — always with the samestatus. Deduplicate onreference; never apply the same result twice.