Signing requests
Every request must carry an RSA SHA256withRSA signature made with your private key. A request with a missing or invalid signature is rejected with HTTP 401 and code 10011304.
Request headers
| Header | Required | Value |
|---|---|---|
Content-Type | Yes | application/json |
Authorization | Yes | LINTASPAY-SHA256withRSA <authString>,sign=<signature> |
lintaspay-request-id | Yes | Unique per request, [A-Za-z0-9_-], max 64 characters. Log it on your side. |
Step 1: build the authString
app_id=<app_id>,nonce=<nonce>,timestamp=<timestamp>
| Field | Rule |
|---|---|
app_id | Your application id: 64 lowercase hex characters, derived from your upstream binding. Your account manager gives you this value. Not the upstream id itself. |
nonce | Random string, unique per request. A repeat within 5 minutes is rejected. |
timestamp | Current Unix time in milliseconds. Requests older than 10 minutes are rejected. |
Step 2: build the content to sign
The content is four lines, and each line ends with \n, including the last one:
<authString>\n
POST\n
<path and query, without the domain>\n
<raw request body>\n
Example:
app_id=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef,nonce=9c1d4a7e2b3f4c5d6e7f8091a2b3c4d5,timestamp=1792141805123
POST
/pay-in/create
{"productCode":11,"currency":"IDR","amount":"10000","mchOrderNo":"M202610140001","remark":"Order 1001","userName":"Budi Santoso"}
- Sign the exact bytes you send. If you serialize JSON once to sign it and again to send it, the two can differ, and the signature fails.
- The path has no domain and no version prefix, for example
/pay-in/create.
Step 3: sign and send
Sign the content with your private key using SHA256withRSA (PKCS#1 v1.5), then Base64-encode the result:
Authorization: LINTASPAY-SHA256withRSA app_id=…,nonce=…,timestamp=…,sign=<base64 signature>
There is a single space after LINTASPAY-SHA256withRSA, and a comma before sign=.
Complete helpers
Each helper signs the request, sends it, and verifies the response signature.
#!/usr/bin/env bash
# Usage: ./call.sh /data/bank-list '{"type":1}'
set -euo pipefail
BASE_URL=https://test.lintaspay.xyz
APP_ID=your_app_id
URL_PATH="$1"; BODY="$2"
AUTH="app_id=$APP_ID,nonce=$(openssl rand -hex 16),timestamp=$(date +%s)000"
SIGN=$(printf '%s\nPOST\n%s\n%s\n' "$AUTH" "$URL_PATH" "$BODY" \
| openssl dgst -sha256 -sign merchant_private_key.pem | base64 | tr -d '\n')
curl -sS -X POST "$BASE_URL$URL_PATH" \
-H 'Content-Type: application/json' \
-H "Authorization: LINTASPAY-SHA256withRSA $AUTH,sign=$SIGN" \
-H "lintaspay-request-id: $(openssl rand -hex 16)" \
-d "$BODY"// Node.js 18+ — no dependencies
import crypto from 'node:crypto';
import fs from 'node:fs';
const BASE_URL = 'https://test.lintaspay.xyz';
const APP_ID = 'your_app_id';
const PRIVATE_KEY = fs.readFileSync('merchant_private_key.pem', 'utf8');
const PLATFORM_PUBLIC_KEY = fs.readFileSync('lintaspay_public_key.pem', 'utf8');
export async function call(path, body) {
const raw = JSON.stringify(body);
const authString = `app_id=${APP_ID},nonce=${crypto.randomBytes(16).toString('hex')},timestamp=${Date.now()}`;
const content = `${authString}\nPOST\n${path}\n${raw}\n`;
const sign = crypto.sign('RSA-SHA256', Buffer.from(content), PRIVATE_KEY).toString('base64');
const res = await fetch(BASE_URL + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `LINTASPAY-SHA256withRSA ${authString},sign=${sign}`,
'lintaspay-request-id': crypto.randomUUID().replaceAll('-', ''),
},
body: raw,
});
const text = await res.text();
// Verify the response signature before trusting it.
const signed = `${res.headers.get('lintaspay-timestamp')}\n${res.headers.get('lintaspay-nonce')}\n${text}\n`;
const valid = crypto.verify('RSA-SHA256', Buffer.from(signed), PLATFORM_PUBLIC_KEY,
Buffer.from(res.headers.get('lintaspay-signature') ?? '', 'base64'));
if (!valid) throw new Error('invalid response signature');
return JSON.parse(text);
}# Python 3.9+ — pip install requests cryptography
import base64, json, time, uuid
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
BASE_URL = "https://test.lintaspay.xyz"
APP_ID = "your_app_id"
with open("merchant_private_key.pem", "rb") as f:
PRIVATE_KEY = serialization.load_pem_private_key(f.read(), password=None)
with open("lintaspay_public_key.pem", "rb") as f:
PLATFORM_PUBLIC_KEY = serialization.load_pem_public_key(f.read())
def call(path: str, body: dict) -> dict:
raw = json.dumps(body, separators=(",", ":"), ensure_ascii=False)
auth = f"app_id={APP_ID},nonce={uuid.uuid4().hex},timestamp={int(time.time() * 1000)}"
content = f"{auth}\nPOST\n{path}\n{raw}\n".encode()
sign = base64.b64encode(PRIVATE_KEY.sign(content, padding.PKCS1v15(), hashes.SHA256())).decode()
res = requests.post(BASE_URL + path, data=raw.encode(), timeout=30, headers={
"Content-Type": "application/json",
"Authorization": f"LINTASPAY-SHA256withRSA {auth},sign={sign}",
"lintaspay-request-id": uuid.uuid4().hex,
})
text = res.content.decode("utf-8")
# Verify the response signature before trusting it (raises InvalidSignature).
signed = f"{res.headers['lintaspay-timestamp']}\n{res.headers['lintaspay-nonce']}\n{text}\n".encode()
PLATFORM_PUBLIC_KEY.verify(base64.b64decode(res.headers["lintaspay-signature"]), signed,
padding.PKCS1v15(), hashes.SHA256())
return json.loads(text)<?php
// PHP 7.4+ with openssl and curl
const BASE_URL = 'https://test.lintaspay.xyz';
const APP_ID = 'your_app_id';
function api_call(string $path, $body): array
{
static $privateKey, $platformKey;
$privateKey ??= openssl_pkey_get_private(file_get_contents('merchant_private_key.pem'));
$platformKey ??= openssl_pkey_get_public(file_get_contents('lintaspay_public_key.pem'));
$raw = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$auth = sprintf('app_id=%s,nonce=%s,timestamp=%d', APP_ID, bin2hex(random_bytes(16)), (int) (microtime(true) * 1000));
openssl_sign("$auth\nPOST\n$path\n$raw\n", $signature, $privateKey, OPENSSL_ALGO_SHA256);
$headers = [];
$ch = curl_init(BASE_URL . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $raw,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: LINTASPAY-SHA256withRSA ' . $auth . ',sign=' . base64_encode($signature),
'lintaspay-request-id: ' . bin2hex(random_bytes(16)),
],
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return strlen($line);
},
]);
$text = curl_exec($ch);
curl_close($ch);
// Verify the response signature before trusting it.
$signed = $headers['lintaspay-timestamp'] . "\n" . $headers['lintaspay-nonce'] . "\n" . $text . "\n";
if (openssl_verify($signed, base64_decode($headers['lintaspay-signature'] ?? ''), $platformKey, OPENSSL_ALGO_SHA256) !== 1) {
throw new RuntimeException('invalid response signature');
}
return json_decode($text, true);
}// Java 11+ — no dependencies. Private key must be PKCS8 PEM.
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.*;
import java.security.spec.*;
import java.util.*;
public class ApiClient {
static final String BASE_URL = "https://test.lintaspay.xyz";
static final String APP_ID = "your_app_id";
private final PrivateKey privateKey;
private final PublicKey platformKey;
private final HttpClient http = HttpClient.newHttpClient();
public ApiClient() throws Exception {
KeyFactory kf = KeyFactory.getInstance("RSA");
privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(pem("merchant_private_key.pem")));
platformKey = kf.generatePublic(new X509EncodedKeySpec(pem("lintaspay_public_key.pem")));
}
static byte[] pem(String file) throws Exception {
String body = Files.readString(Path.of(file)).replaceAll("-----[A-Z ]+-----", "").replaceAll("\\s", "");
return Base64.getDecoder().decode(body);
}
/** POSTs rawJson to path; returns the verified response body. */
public String call(String path, String rawJson) throws Exception {
String auth = "app_id=" + APP_ID + ",nonce=" + UUID.randomUUID().toString().replace("-", "")
+ ",timestamp=" + System.currentTimeMillis();
Signature s = Signature.getInstance("SHA256withRSA");
s.initSign(privateKey);
s.update((auth + "\nPOST\n" + path + "\n" + rawJson + "\n").getBytes(StandardCharsets.UTF_8));
String sign = Base64.getEncoder().encodeToString(s.sign());
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE_URL + path))
.header("Content-Type", "application/json")
.header("Authorization", "LINTASPAY-SHA256withRSA " + auth + ",sign=" + sign)
.header("lintaspay-request-id", UUID.randomUUID().toString().replace("-", ""))
.POST(HttpRequest.BodyPublishers.ofString(rawJson, StandardCharsets.UTF_8))
.build();
HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
// Verify the response signature before trusting it.
String signed = res.headers().firstValue("lintaspay-timestamp").orElse("") + "\n"
+ res.headers().firstValue("lintaspay-nonce").orElse("") + "\n" + res.body() + "\n";
Signature v = Signature.getInstance("SHA256withRSA");
v.initVerify(platformKey);
v.update(signed.getBytes(StandardCharsets.UTF_8));
if (!v.verify(Base64.getDecoder().decode(res.headers().firstValue("lintaspay-signature").orElse("")))) {
throw new SecurityException("invalid response signature");
}
return res.body();
}
}Common mistakes
| Symptom | Cause |
|---|---|
10011304 on every call | The private key doesn't match the public key you registered, or the path or body you signed differs from what you sent. |
10011304 "timestamp expired" | Your server clock is off, or the timestamp is in seconds instead of milliseconds. |
10011304 "nonce already used" | The nonce was reused within 5 minutes. Generate a fresh one for every request. |
10011303 | Wrong app_id, or your IP isn't on your allowlist (HTTP 403). |
10011302 | Your merchant account is disabled. Contact LintasPay. |