Authentication & Signing
Accessing OristaPay Open APIs requires dual authentication:
- OAuth2 Access Token — proves the caller’s identity
- HMAC Request Signature — proves the request has not been tampered with
Every request must pass both layers of verification. Failure of either results in 401 Unauthorized.
1. Obtain Access Token
Standard OAuth2 client_credentials flow.
Request
POST /realms/digitalasset/protocol/openid-connect/token HTTP/1.1
Host: auth.uat.rdezlink.tech
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id={api_key}
&client_secret={api_secret}
Response
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 300
}
Usage Constraints
access_token can be reused within expires_in seconds. Cache it and refresh proactively 30 seconds before expiry.
access_token must be issued by the OAuth client corresponding to X-Api-Key.
Using merchant A’s api_key with merchant B’s access_token will be rejected.
2. Request Signature Algorithm
String to Sign
string_to_sign = METHOD + PATH + TIMESTAMP + NONCE + SHA256_HEX(BODY)
Signature Calculation
signature = HEX( HMAC_SHA256( sign_secret, string_to_sign ) )
Field Definitions
| Element | Definition |
|---|
METHOD | HTTP method, uppercase, e.g. POST |
PATH | API path without domain and query, e.g. /api/v1/wallet/list |
TIMESTAMP | 13-digit UTC millisecond timestamp string, must match X-Timestamp exactly |
NONCE | Unique random string for this request, must match X-Nonce exactly. Recommended: 32 hex characters |
BODY | Raw request body bytes; empty string when no body (SHA256_HEX("") = e3b0c442...b855) |
Reference Implementations
import hashlib
import hmac
def sign(method: str, path: str, ts: str, nonce: str,
body: str, sign_secret: str) -> str:
body_hash = hashlib.sha256(body.encode()).hexdigest()
to_sign = method + path + ts + nonce + body_hash
return hmac.new(sign_secret.encode(),
to_sign.encode(),
hashlib.sha256).hexdigest()
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.HexFormat;
public static String sign(String method, String path, String ts,
String nonce, byte[] body, String signSecret)
throws Exception {
String bodyHash = HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-256").digest(body));
String toSign = method + path + ts + nonce + bodyHash;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(signSecret.getBytes(), "HmacSHA256"));
return HexFormat.of().formatHex(mac.doFinal(toSign.getBytes()));
}
const crypto = require('crypto');
function sign(method, path, ts, nonce, body, signSecret) {
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const toSign = method + path + ts + nonce + bodyHash;
return crypto.createHmac('sha256', signSecret)
.update(toSign).digest('hex');
}
All business requests must carry the following headers:
| Header | Required | Description |
|---|
Authorization | ✓ | Bearer {access_token} |
X-Api-Key | ✓ | Your api_key |
X-Timestamp | ✓ | UTC millisecond timestamp, ±5 minutes tolerance |
X-Nonce | ✓ | Unique random string, must not be reused within 5 minutes under the same api_key |
X-Signature | ✓ | Signature computed per §2 |
Content-Type | ✓ | Always application/json; charset=utf-8 |
Request & Response Conventions
Protocol Specification
| Item | Specification |
|---|
| Transport | HTTPS (TLS 1.2+) |
| Method | POST |
| Charset | UTF-8 |
| Format | JSON |
| Field naming | camelCase |
| Timestamp | Milliseconds; string or number accepted in request, number in response |
Response Envelope
All responses use a single-layer JSON { code, message, data? }, returned by the downstream business service without additional gateway wrapping.
Success (Business OK)
Business code code = 1, business data in data:
{
"code": 1,
"message": "Success",
"data": { /* business data; array for list endpoints */ }
}
Business / Downstream Error
Downstream business errors still return HTTP 200, with error details expressed by code / message:
{
"code": <non-1 integer>,
"message": "<failure reason>"
}
When downstream gRPC is unreachable or times out, HTTP status remains 200, code is a negative integer (negated gRPC StatusCode, e.g. -14 for UNAVAILABLE), with connection error details in message.
Gateway-Level Error
Route not found, internal gateway errors, missing descriptors, etc. HTTP status uses the corresponding error code (e.g. 400 / 404 / 500 / 502), with response body still using {code, message}:
{
"code": 404,
"message": "route not found"
}
- HTTP status reflects transport layer results:
2xx means the request was successfully delivered and converted; non-2xx indicates a gateway-side error
- Business code
code reflects business layer results: 1 means business success with result in data; other values are business error codes with reason in message
- How to determine success: HTTP
2xx + business code == 1. Both conditions must be met
- Authentication errors: Any failure in
Authorization / X-Signature / X-Timestamp / X-Nonce returns HTTP 401 with {"code":401,"message":"Unauthorized"}
Error Handling
HTTP Status Codes & Business Codes
All responses use the single-layer envelope {code, message, data?}. HTTP status reflects gateway/transport results; business code code reflects business processing results.
| HTTP Status | Meaning | Response Body | Action |
|---|
200 + code == 1 | Business success | {code:1, message, data} | Read business fields from data |
200 + code != 1 | Downstream business error | {code, message} | Handle based on message |
200 + code < 0 | gRPC error after delivery | {code, message} (code is negated gRPC status) | Check downstream, retry if needed |
400 | Invalid request body or parameters | {code, message} | Fix and retry |
401 | Authentication failed (OAuth or signature) | {code:401, message} | Follow the troubleshooting checklist below |
404 | Endpoint not found | {code, message} | Verify the path |
429 | Rate limit triggered | {code, message, ...} | Back off and retry |
5xx | Gateway or downstream service error | {code, message} | Exponential backoff and retry |
Authentication Failure Checklist
All authentication errors return:
{ "code": 401, "message": "Unauthorized" }
Troubleshoot in this order:
- Is the token valid? — Has it expired? Was it issued by the current
api_key?
- Does the signature match? — Do
METHOD / PATH / BODY match the actual request?
- Is the timestamp within the window? — Is the local clock synchronized with NTP?
- Is the nonce unique? — Must not be reused within 5 minutes under the same
api_key.
Rate Limiting
| Item | Value |
|---|
| Dimension | Per api_key |
| Default quota | 600 requests / minute |
| Exceeded response | HTTP 429 |
Rate Limit Exceeded Response Example
{
"code": 429,
"message": "rate limit exceeded",
"limit": 600,
"window_ms": 60000
}
For higher quotas, contact your account manager. Adjustments take effect the following minute.
Security Recommendations
| Topic | Best Practice |
|---|
| Key management | Store api_secret and sign_secret server-side only. Never expose them to frontend or mobile clients. |
| Credential rotation | Rotate regularly; contact your account manager immediately if credentials are leaked |
| Transport security | Enforce HTTPS, reject any client not using TLS 1.2+ |
| Clock synchronization | Use NTP to maintain accuracy within ±1 minute to avoid false rejections |
| Log sanitization | Never log full api_secret / sign_secret / access_token in application logs |