Callbacks
When an order reaches a final state, LintasPay sends one signed POST to the order's notifyUrl, or to your merchant default if the order has none.
| Order | Callback sent when |
|---|---|
| Pay-in | orderStatus becomes 2 (success) or 3 (failed). See Pay-in notification. |
| Payout | orderStatus becomes 4 (success) or 5 (failed). See Payout notification. |
| Verify bank account | No callback; the result is in the response. |
Your reply
Reply HTTP 200 with exactly this body, within 5 seconds:
{"status":"SUCCESS"}
Anything else counts as a failed delivery: another status code, another body, or a timeout. Acknowledge first, then process asynchronously.
Retries
If delivery fails, LintasPay retries after 15 s, 1 min, 5 min, 15 min, 30 min, 1 h, 2 h and 6 h, then gives up. Every order can also be queried at any time, so build reconciliation on queries, with callbacks as the fast path.
Your handler must be idempotent. A notification can arrive more than once, for example after a timeout on your side.
Notify URL rules
- Production URLs must use
https. The sandbox also acceptshttp, for testing tunnels. - The host must resolve to a public IP address. Private, loopback and link-local addresses are rejected with code
10011431. - 255 characters maximum.
Verifying a callback
// Express — keep the raw body for signature verification
import crypto from 'node:crypto';
import express from 'express';
import fs from 'node:fs';
const PLATFORM_PUBLIC_KEY = fs.readFileSync('lintaspay_public_key.pem', 'utf8');
const app = express();
app.post('/notify/payout', express.raw({ type: '*/*' }), (req, res) => {
const raw = req.body.toString('utf8');
const signed = `${req.get('lintaspay-timestamp')}\n${req.get('lintaspay-nonce')}\n${raw}\n`;
const valid = crypto.verify('RSA-SHA256', Buffer.from(signed), PLATFORM_PUBLIC_KEY,
Buffer.from(req.get('lintaspay-signature') ?? '', 'base64'));
if (!valid) return res.status(401).end();
const notice = JSON.parse(raw);
queueForProcessing(notice); // process asynchronously, idempotently (by orderNo)
res.json({ status: 'SUCCESS' }); // reply within 5 seconds
});# Flask
import base64, json
from flask import Flask, request, jsonify
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
with open("lintaspay_public_key.pem", "rb") as f:
PLATFORM_PUBLIC_KEY = serialization.load_pem_public_key(f.read())
app = Flask(__name__)
@app.post("/notify/payout")
def notify_payout():
raw = request.get_data(as_text=True)
signed = f"{request.headers['lintaspay-timestamp']}\n{request.headers['lintaspay-nonce']}\n{raw}\n".encode()
try:
PLATFORM_PUBLIC_KEY.verify(base64.b64decode(request.headers["lintaspay-signature"]), signed,
padding.PKCS1v15(), hashes.SHA256())
except (InvalidSignature, KeyError):
return "", 401
queue_for_processing(json.loads(raw)) # process asynchronously, idempotently (by orderNo)
return jsonify(status="SUCCESS") # reply within 5 seconds<?php
$raw = file_get_contents('php://input');
$signed = $_SERVER['HTTP_LINTASPAY_TIMESTAMP'] . "\n" . $_SERVER['HTTP_LINTASPAY_NONCE'] . "\n" . $raw . "\n";
$platformKey = openssl_pkey_get_public(file_get_contents('lintaspay_public_key.pem'));
$ok = openssl_verify($signed, base64_decode($_SERVER['HTTP_LINTASPAY_SIGNATURE'] ?? ''), $platformKey, OPENSSL_ALGO_SHA256);
if ($ok !== 1) {
http_response_code(401);
exit;
}
$notice = json_decode($raw, true);
queue_for_processing($notice); // process asynchronously, idempotently (by orderNo)
header('Content-Type: application/json');
echo '{"status":"SUCCESS"}'; // reply within 5 seconds// Spring Boot
@PostMapping(value = "/notify/payout", consumes = "*/*")
public ResponseEntity<String> notifyPayout(@RequestBody String raw,
@RequestHeader("lintaspay-signature") String signature,
@RequestHeader("lintaspay-timestamp") String timestamp,
@RequestHeader("lintaspay-nonce") String nonce) throws Exception {
Signature v = Signature.getInstance("SHA256withRSA");
v.initVerify(platformKey); // PublicKey loaded from lintaspay_public_key.pem
v.update((timestamp + "\n" + nonce + "\n" + raw + "\n").getBytes(StandardCharsets.UTF_8));
if (!v.verify(Base64.getDecoder().decode(signature))) {
return ResponseEntity.status(401).build();
}
queueForProcessing(raw); // process asynchronously, idempotently (by orderNo)
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body("{\"status\":\"SUCCESS\"}");
}