API Reference
Authentication
All API requests must be signed with the request signing key and include an expiration window. AinePay signs webhook callbacks with the webhook verification key. This page covers the API request signing rules, webhook verification rules, and their corresponding examples.
Required headers
x-api-key- Merchant request signing keyx-api-signature- HMAC-SHA256 signature in lowercase hexx-api-timestamp- client side current time in millisecondsx-api-recv-window- allowed client time offset, usually60000milliseconds, maximum180000milliseconds
All requests only support form submission.
Signing rules
- Collect query string parameters and form parameters.
- Flatten all parameters into key-value pairs, including repeated keys.
- Sort by key, then by value, both in ascending order.
- URL-encode them using UTF-8.
- Append
timestampandrecvWindow. - Use the secret key and compute HMAC-SHA256.
Request Signing Example
A request can include query parameters, form parameters, indexed keys, and URL-encoded values together.
- Query string:
?orderIds[1]=222&orderIds[0]=111 - Form body:
coin=USDT_ERC20&userId=1000000001¬e=hello world - timestamp:
1761611071000 - recvWindow:
60000 - secretKey:
sv_5n61GLATPVKKejONbmwQPg2LuXZwRlibgDuoDLUQzV4
Payload:
coin=USDT_ERC20¬e=hello+world&orderIds%5B0%5D=111&orderIds%5B1%5D=222&userId=1000000001×tamp=1761611071000&recvWindow=60000
Signature:
f10590f4f0f8fb2c63382276edf128061eb51b36ae5522d09d28376f03ee2d92
Java request signing example
package util;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class AinePaySigner {
public static final class Params {
private final Map<String, String[]> data = new LinkedHashMap<>();
private Params() {}
public static Params create() {
return new Params();
}
public Params put(String key, String... values) {
data.put(key, values == null ? new String[]{""} : values);
return this;
}
}
public static String sign(Params params, String timestamp, String recvWindow, String secretKey) throws Exception {
List<String[]> pairs = new ArrayList<>();
for (Map.Entry<String, String[]> e : params.data.entrySet()) {
if (e.getValue() != null) {
for (String v : e.getValue()) {
pairs.add(new String[]{e.getKey(), v == null ? "" : v});
}
}
}
pairs.sort(Comparator.<String[], String>comparing(p -> p[0]).thenComparing(p -> p[1]));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < pairs.size(); i++) {
if (i > 0) sb.append("&");
sb.append(URLEncoder.encode(pairs.get(i)[0], StandardCharsets.UTF_8.name()))
.append("=")
.append(URLEncoder.encode(pairs.get(i)[1], StandardCharsets.UTF_8.name()));
}
String payload = sb.length() == 0
? "timestamp=" + timestamp + "&recvWindow=" + recvWindow
: sb + "×tamp=" + timestamp + "&recvWindow=" + recvWindow;
String b64url = secretKey.startsWith("sv_") ? secretKey.substring(3) : secretKey;
byte[] secret = Base64.getUrlDecoder().decode(b64url);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret, "HmacSHA256"));
byte[] digest = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : digest) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
}Webhook Verification Example
Callback fields are verified after parsing, sorting, and rebuilding the canonical string.
- Raw callback body:
userId=user_001&orderId=order123&status=PAID&merchantId=1001&coin=USDT&qty=1.11&expired=1767793122000&created=1767793122000&updated=1767793122000 - notifySecret:
sv_5n61GLATPVKKejONbmwQPg2LuXZwRlibgDuoDLUQzV4
Canonical string:
coin=USDT&created=1767793122000&expired=1767793122000&merchantId=1001&orderId=order123&qty=1.11&status=PAID&updated=1767793122000&userId=user_001
Signature:
08fbca6b485a773c764ee0b5933e7936d536cda67bb44c19667a41ae401d7036
Verification result: true
Java webhook verification example
package util;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
public class AinePayVerifier {
public static boolean verify(String rawBody, String signature, String notifySecret) throws Exception {
Map<String, String> fields = parseBody(rawBody);
String canonical = toCanonical(fields);
String b64 = notifySecret.startsWith("sv_") ? notifySecret.substring(3) : notifySecret;
byte[] secret = Base64.getUrlDecoder().decode(b64);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret, "HmacSHA256"));
byte[] digest = mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8));
String expected = toHex(digest);
return constantTimeEquals(expected, signature == null ? "" : signature.toLowerCase());
}
public static Map<String, String> parseBody(String rawBody) throws Exception {
Map<String, String> map = new LinkedHashMap<>();
if (rawBody == null || rawBody.isEmpty()) return map;
for (String pair : rawBody.split("&")) {
int idx = pair.indexOf('=');
if (idx < 0) continue;
String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name());
String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name());
map.put(key, value);
}
return map;
}
public static String toCanonical(Map<String, String> fields) throws Exception {
Map<String, String> sorted = new TreeMap<>(fields);
StringBuilder sb = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> e : sorted.entrySet()) {
if (!first) sb.append("&");
first = false;
sb.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8.name()))
.append("=")
.append(URLEncoder.encode(e.getValue() == null ? "" : e.getValue(), StandardCharsets.UTF_8.name()));
}
return sb.toString();
}
private static String toHex(byte[] bytes) {
char[] hex = "0123456789abcdef".toCharArray();
char[] out = new char[bytes.length * 2];
for (int i = 0, j = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
out[j++] = hex[v >>> 4];
out[j++] = hex[v & 0x0F];
}
return new String(out);
}
private static boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) return false;
int r = 0;
for (int i = 0; i < a.length(); i++) r |= (a.charAt(i) ^ b.charAt(i));
return r == 0;
}
}Common mistakes
- Signing parameters in the wrong order
- Server clock drift causing timestamp rejection
- Only form submission is supported