# Authentication & Environments

> For the complete documentation index, see [llms.txt](https://docs.banxa.com/llms.txt). Append `.md` to any page URL for its markdown version.


## Environments

| Environment | Base URL |
|  --- | --- |
| Sandbox | `https://api.banxa-sandbox.com` |
| Production | `https://api.banxa.com` |


Use sandbox for all development and testing. Production credentials are issued separately by Banxa after your integration has been approved.

Your partner reference (`{partnerRef}`) is a unique identifier assigned to your account. It is included in every API request path:

```
https://api.banxa-sandbox.com/{partnerRef}/v2/...
```

## Rate limiting

The production API is rate-limited to **500 requests per minute** across all endpoints combined, per IP address. The sandbox API is rate-limited to **120 requests per minute** per merchant account: sandbox is a shared testing environment, and the lower limit keeps it responsive for all merchants. Requests exceeding either limit will receive an HTTP `429 Too Many Requests` response.

**Best practices:**

- Use webhooks for order status updates instead of polling the orders endpoint.
- Call the quotes endpoint only when a customer actively requests a quote — do not poll for price feeds.
- Implement exponential backoff when retrying after a `429` response.
- If you consistently hit rate limits in normal operation, review your call patterns — it typically indicates unnecessary polling.


## Environment switching

Switch between sandbox and production via the environment toggle at the top of the Partner Dashboard. Configuration changes (webhooks, supported assets, UI settings) are made independently per environment.

## API key authentication

All v2 endpoints use API key authentication. Include your API key in the request header:

```
x-api-key: YOUR_API_KEY
```

Your sandbox and production API keys are different. Retrieve them from the Partner Dashboard under your account settings.

## HMAC authentication

The identity token sharing endpoint (`POST /v2/identities/token/share`) requires HMAC-signed requests instead of the standard API key header.

HMAC is also used in the opposite direction for webhooks: Banxa signs every outbound webhook notification it sends to you, and you verify that signature to confirm it genuinely came from Banxa. The signing algorithm is the same, but the path in the canonical string is different — see [Webhooks](/products/hosted-checkout/docs/transaction-lifecycle/webhooks) for the verification flow.

HMAC credentials must be stored server-side. Never embed your API secret in frontend or mobile code.

### Authorization header

```
Authorization: Bearer API_KEY:SIGNATURE:NONCE
```

| Component | Description |
|  --- | --- |
| `API_KEY` | Your Banxa API key |
| `SIGNATURE` | Hex-encoded HMAC-SHA256 of the canonical string |
| `NONCE` | Unix timestamp in microseconds (16 digits), unique per request |


### Building the signature

Construct a newline-separated canonical string, then sign it with HMAC-SHA256 using your API secret.

**GET request:**

```
METHOD\nPATH_WITH_QUERY_STRING\nNONCE
```

**POST request:**

```
METHOD\nPATH\nNONCE\nCOMPACT_JSON_BODY
```

Rules:

- Use the request **path only** — never the full URL with domain
- Include the query string in the path for GET requests
- JSON body must be compact — no whitespace between elements
- Generate a new nonce for every request


Example, using a 16-digit microsecond nonce. Sign the path exactly as you call it — `POST /{partnerRef}/v2/identities/token/share` is routed to the enterprise API through the Banxa gateway, but the canonical string uses the v2 path you requested:

```
POST\n/mypartner/v2/identities/token/share\n1785804345837761\n{"externalCustomerId":"example_01"}
```

Nonce precision
Use **microseconds (16 digits)**. Seconds (10 digits) and milliseconds (13 digits) are also accepted, but millisecond precision causes nonce collisions under concurrent load: two requests generated in the same millisecond produce the same nonce, and the second is rejected as reused (`40003`).

Generate the nonce from a clock with genuine sub-millisecond resolution. Multiplying a millisecond timestamp by 1000 pads it to 16 digits without adding precision and does **not** prevent collisions.

### Code examples

```python Python
import hmac
import time
import json

key = '[YOUR_API_KEY]'
secret = '[YOUR_API_SECRET]'

def generate_hmac(method, path, payload=None):
    nonce = str(int(time.time() * 1_000_000))
    parts = [method, path, nonce]
    if payload:
        parts.append(json.dumps(payload, separators=(',', ':')))
    data = '\n'.join(parts)
    signature = hmac.new(secret.encode('utf-8'), data.encode('utf-8'), 'sha256').hexdigest()
    return f'{key}:{signature}:{nonce}', nonce
```

```javascript Node.js
const crypto = require('crypto');

const key = '[YOUR_API_KEY]';
const secret = '[YOUR_API_SECRET]';

function generateHmac(method, path, payload = null) {
    const nonce = Math.round((performance.timeOrigin + performance.now()) * 1000).toString();
    const parts = [method, path, nonce];
    if (payload) parts.push(JSON.stringify(payload));
    const data = parts.join('\n');
    const signature = crypto.createHmac('sha256', secret).update(data).digest('hex');
    return `${key}:${signature}:${nonce}`;
}
```

```php PHP
<?php
$key = '[YOUR_API_KEY]';
$secret = '[YOUR_API_SECRET]';

function generateHmac($method, $path, $payload, $key, $secret) {
    $nonce = (string)(int)(microtime(true) * 1000000);
    $parts = [$method, $path, $nonce];
    if ($payload) $parts[] = $payload;
    $data = implode("\n", $parts);
    $signature = hash_hmac('sha256', $data, $secret);
    return "{$key}:{$signature}:{$nonce}";
}
```

```java Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.time.Instant;
import java.util.Formatter;

public class BanxaAuth {
    private static final String KEY = "[YOUR_API_KEY]";
    private static final String SECRET = "[YOUR_API_SECRET]";

    public String generateHmac(String method, String path, String payload) throws Exception {
        Instant now = Instant.now();
        String nonce = String.valueOf(now.getEpochSecond() * 1_000_000L + now.getNano() / 1_000L);
        String data = method + "\n" + path + "\n" + nonce;
        if (payload != null) data += "\n" + payload;

        SecretKeySpec signingKey = new SecretKeySpec(SECRET.getBytes(), "HmacSHA256");
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);
        Formatter formatter = new Formatter();
        for (byte b : mac.doFinal(data.getBytes())) {
            formatter.format("%02x", b);
        }
        return KEY + ":" + formatter.toString() + ":" + nonce;
    }
}
```

```swift Swift
import CryptoKit

let key = "[YOUR_API_KEY]"
let secret = "[YOUR_API_SECRET]"

func generateHmac(method: String, path: String, payload: String? = nil) -> String {
    let nonce = String(Int(Date().timeIntervalSince1970 * 1_000_000))
    var parts = [method, path, nonce]
    if let payload = payload { parts.append(payload) }
    let data = parts.joined(separator: "\n")
    let secretKey = SymmetricKey(data: secret.data(using: .utf8)!)
    let signature = HMAC<SHA256>.authenticationCode(for: data.data(using: .utf8)!, using: secretKey)
        .map { String(format: "%02hhx", $0) }.joined()
    return "\(key):\(signature):\(nonce)"
}
```

```ruby Ruby
require 'openssl'

KEY = '[YOUR_API_KEY]'
SECRET = '[YOUR_API_SECRET]'

def generate_hmac(method, path, payload = nil)
    now = Time.now
    nonce = (now.to_i * 1_000_000 + now.usec).to_s
    parts = [method, path, nonce]
    parts << payload if payload
    data = parts.join("\n")
    signature = OpenSSL::HMAC.hexdigest('sha256', SECRET, data)
    "#{KEY}:#{signature}:#{nonce}"
end
```

### HMAC error codes

| Code | Cause |
|  --- | --- |
| `40001` | Nonce is not a valid Unix timestamp — must be 10, 13, or 16 digits (seconds, milliseconds, or microseconds) |
| `40002` | Nonce is too old — check your system clock is in sync |
| `40003` | Nonce already used — generate a new nonce per request |
| `40100` | API key not recognised — check you are using the correct environment key |
| `40101` | Authorization header is malformed — format must be `Bearer API_KEY:SIGNATURE:NONCE` |
| `40102` | Authorization header is missing |
| `40103` | Signature mismatch — check path, newline separators, compact JSON, and correct secret |


### Best practices

- Generate a **new nonce for every request** — reuse will be rejected
- Use **microsecond precision** for the nonce — millisecond timestamps collide under concurrent load
- Keep your system clock in sync (NTP) to avoid nonce expiry errors
- Serialize request bodies with **no whitespace** before signing
- Sign the **request path only** — never the full URL including domain
- Store your API secret in a secure secret store, never in source code or client-side storage
- Rotate your secret immediately if you suspect it has been exposed