Overview#
The Artha Cards External API uses HMAC-SHA256 request signing for authentication. Every request must include 5 headers that prove the client's identity, ensure request integrity, and prevent replay attacks.This is the same approach used by Stripe, Adyen, Razorpay, and other fintech providers.
Credentials#
When onboarded, each client receives:| Credential | Description | Example |
|---|
| API Key | Public identifier sent with every request | ak_live_8f3a9b2c1d4e5f6a |
| Secret | Private key used to compute HMAC signatures. Never sent over the wire. | mJ8v3aQpT5y2rX6nK9cD4eH7sB1uF0gLzN2wV8tYqP= |
Important: The Secret is displayed only once at creation time. Store it securely (e.g., in a vault or environment variable). If lost, a new API key must be generated.
Every request to /ext/api/v1/* must include these 5 headers:| Header | Type | Description |
|---|
X-API-Key | string | Your API key (e.g., ak_live_8f3a9b2c1d4e5f6a) |
X-Timestamp | string | Current UTC time as Unix epoch seconds (e.g., 1707753600) |
X-Nonce | string | A unique random string per request (e.g., UUID or random hex). Must not be reused within 5 minutes. |
X-Body-Hash | string | base64(SHA-256(request_body)). For requests with no body (GET, DELETE), hash an empty string. |
X-Signature | string | base64(HMAC-SHA256(secret, string_to_sign)) |
How to Compute the Signature#
Step 1: Compute the Body Hash#
body_hash = base64(SHA-256(request_body_bytes))
Use the raw request body bytes (UTF-8 encoded)
For GET/DELETE requests with no body, hash an empty byte array: base64(SHA-256(b""))
The body hash for an empty body is always: 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
Step 2: Build the String to Sign#
Concatenate 5 components separated by newline (\n):string_to_sign = "{METHOD}\n{PATH_AND_QUERY}\n{TIMESTAMP}\n{NONCE}\n{BODY_HASH}"
| Component | Description | Example |
|---|
METHOD | HTTP method in uppercase | POST |
PATH_AND_QUERY | Full path including query string | /ext/api/v1/cards?limit=10 |
TIMESTAMP | Same value as X-Timestamp header | 1707753600 |
NONCE | Same value as X-Nonce header | a1b2c3d4e5f6 |
BODY_HASH | Same value as X-Body-Hash header | base64(SHA-256(body)) |
Step 3: Compute the HMAC Signature#
signature = base64(HMAC-SHA256(secret_bytes, string_to_sign_bytes))
secret_bytes: your Secret encoded as UTF-8 bytes
string_to_sign_bytes: the string from Step 2 encoded as UTF-8 bytes
Full Example#
Request#
POST /ext/api/v1/cards HTTP/1.1
Host: api.arthapay.com
Content-Type: application/json
X-API-Key: ak_test_abc123def456
X-Timestamp: 1707753600
X-Nonce: f47ac10b-58cc-4372-a567
X-Body-Hash: oWVxV2ghrGQFIMUEm0bRNNMCHTCnxNFKbqA3CMY9VVE=
X-Signature: K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=
{"product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "customer_id": "...", "currency": "USD"}
Step-by-step Computation#
1. Body Hash:
body = '{"product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", ...}'
X-Body-Hash = base64(SHA-256(body_bytes))
2. String to Sign:
"POST\n/ext/api/v1/cards\n1707753600\nf47ac10b-58cc-4372-a567\noWVxV2ghrGQFIMUEm0bRNNMCHTCnxNFKbqA3CMY9VVE="
3. Signature:
X-Signature = base64(HMAC-SHA256(secret, string_to_sign))
Code Examples#
C# (.NET)#
Python#
JavaScript / Node.js#
cURL#
Error Responses#
When authentication fails, the API returns a 401 Unauthorized response:{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Missing required authentication headers (X-API-Key, X-Timestamp, X-Nonce, X-Body-Hash, X-Signature)."
}
}
Common Authentication Errors#
| Error Message | Cause | Fix |
|---|
Missing required authentication headers | One or more of the 5 headers is missing | Include all 5 headers |
Invalid API key | API key not found in database | Check your API key is correct |
API key is disabled | Key was manually disabled or auto-locked | Contact admin |
API key has expired | Key passed its ExpiresAt date | Request a new API key |
API key is locked due to excessive failures | 50+ consecutive auth failures | Contact admin to reset |
Request from unauthorized IP address | Client IP not in the AllowedIPs whitelist | Request IP whitelist update |
Request timestamp is outside the allowed window | X-Timestamp is more than 5 minutes from server time | Sync your system clock (NTP) |
Replay detected (duplicate nonce) | Same nonce reused within 5 minutes | Generate a new nonce per request |
Body hash mismatch | X-Body-Hash doesn't match the actual request body | Recompute SHA-256 of the exact body bytes sent |
Signature mismatch | HMAC signature is incorrect | Verify string-to-sign format and secret |
Security Properties#
| Property | Mechanism |
|---|
| Identity | API Key identifies the client and tenant |
| Authentication | HMAC-SHA256 proves the client has the secret |
| Integrity | Body hash ensures the request body wasn't tampered with |
| Replay Protection | Timestamp window (5 min) + unique nonce per request |
| Timing Attack Protection | Constant-time comparison for all hash/signature checks |
| Auto-lockout | Key auto-disables after 50 consecutive auth failures |
| IP Restriction | Optional IP whitelist per API key |
| Expiration | Optional key expiration date |
| Scoping | Per-key permission scopes |
Rate Limits#
Requests are rate-limited per API key
Default: varies by endpoint
HTTP 429 is returned when limits are exceeded
Use X-RateLimit-Remaining and X-RateLimit-Reset response headers to manage your request rate
Modified at 2026-02-18 17:21:48