production

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

HeaderRequiredValue
Content-TypeYesapplication/json
AuthorizationYesLINTASPAY-SHA256withRSA <authString>,sign=<signature>
lintaspay-request-idYesUnique 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>
FieldRule
app_idYour application id: 64 lowercase hex characters, derived from your upstream binding. Your account manager gives you this value. Not the upstream id itself.
nonceRandom string, unique per request. A repeat within 5 minutes is rejected.
timestampCurrent 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

SymptomCause
10011304 on every callThe 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.
10011303Wrong app_id, or your IP isn't on your allowlist (HTTP 403).
10011302Your merchant account is disabled. Contact LintasPay.