sandbox

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.

OrderCallback sent when
Pay-inorderStatus becomes 2 (success) or 3 (failed). See Pay-in notification.
PayoutorderStatus becomes 4 (success) or 5 (failed). See Payout notification.
Verify bank accountNo 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 accepts http, 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\"}");
}