Authentication
Avenia accepts two ways to authenticate a request:
| Method | Use it for | How |
|---|---|---|
| API key (recommended) | Servers and integrations | Each request is signed with your private key. Keys don't expire. |
| JWT | Logging in, managing accesses and API keys | Log in with email, password and the email token, then send Authorization: Bearer <accessToken>. |
Routes under /v2/auth/ that need authentication (accesses, API keys, MFA) accept only a JWT; the login, token refresh and password-reset routes take no credential. Every other authenticated route accepts either.
After the one-time setup below, your server authenticates every request with the API key alone: no login, no email token, no password.
Set up an API key
- Turn on TOTP MFA for your access — creating a key asks for a TOTP code. See the MFA guide.
- Generate a key pair. RSA, ECDSA and Ed25519 are supported:
Keep
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in private_key.pem -pubout -out public_key.pemprivate_key.pemon your server and never share it. - Register the public key with
POST /v2/auth/api-keys/, authenticated with a JWT. The response is your API key ID (a UUID). Each access can hold up to 5 keys. See API Keys Management for the request body and how to restrict the key to your server IPs.
Sandbox and production are separate: register a key in each environment you use.
Sign a request
Send these three headers on every request:
| Header | Value |
|---|---|
X-API-Key | Your API key ID. |
X-API-Timestamp | The current time in milliseconds since the Unix epoch. |
X-API-Signature | The base64-encoded signature of the string below. |
The string you sign is the concatenation, with no separators, of:
timestamp + METHOD + requestURI + body
timestamp— the exact value ofX-API-Timestamp.METHOD— the HTTP method in upper case:GET,POST,PATCH,PUT,DELETE.requestURI— the path and query string exactly as you send them, starting at/v2/.... For example/v2/account/quote/fixed-rate?inputCurrency=BRL&inputAmount=100.body— the exact bytes of the request body. Empty for requests without a body.
With an RSA key, sign the SHA-256 of that string with PKCS#1 v1.5. With ECDSA, sign its SHA-256 (ASN.1 signature). With Ed25519, sign the string itself.
Example in Python
This helper signs and sends any request. It needs pip install cryptography requests.
import base64
import json
import time
from urllib.parse import urlencode
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
BASE_URL = "https://api.sandbox.avenia.io:10952"
API_KEY = "00000000-0000-0000-0000-000000000000" # your API key ID
with open("private_key.pem", "rb") as key_file:
PRIVATE_KEY = serialization.load_pem_private_key(key_file.read(), password=None)
def avenia(method, path, params=None, body=None):
method = method.upper()
uri = path + ("?" + urlencode(params) if params else "")
payload = json.dumps(body) if body is not None else ""
timestamp = str(int(time.time() * 1000))
string_to_sign = timestamp + method + uri + payload
# RSA key. ECDSA: PRIVATE_KEY.sign(data, ec.ECDSA(hashes.SHA256())). Ed25519: PRIVATE_KEY.sign(data).
signature = PRIVATE_KEY.sign(string_to_sign.encode(), padding.PKCS1v15(), hashes.SHA256())
headers = {
"X-API-Key": API_KEY,
"X-API-Timestamp": timestamp,
"X-API-Signature": base64.b64encode(signature).decode(),
}
if payload:
headers["Content-Type"] = "application/json"
response = requests.request(method, BASE_URL + uri, headers=headers, data=payload.encode() or None, timeout=30)
return response.status_code, response.json() if response.content else None
print(avenia("GET", "/v2/account/account-info"))
The body is serialized once and the same string is both signed and sent. Building the JSON twice — or letting your HTTP client re-serialize it — changes the bytes and breaks the signature.
Fixing a 401
A failed authentication always returns 401 with {"error":"UnauthorizedError"}, without saying why. Check, in this order:
- Timestamp. It must be in milliseconds and within 1 minute of the server's clock. A server clock that drifts, or a timestamp in seconds, fails every request.
- Request URI. The signed URI must include the query string and match the sent one character for character, parameter order included. Build the URI once and use it for both.
- Body. The signed body must be byte-for-byte the body sent — same spacing, same key order. Sign the serialized string, not the object.
- IP allow-list. If the key has
whitelistedIPs, the request must come from one of those ranges (CIDR). - Environment. A sandbox key doesn't exist in production, and the other way round.
- Route.
/v2/auth/...routes refuse API keys; use a JWT there. - Key type. RSA and ECDSA sign the SHA-256 of the string; Ed25519 signs the raw string.
- Extra headers. If you send the optional
X-API-IdempotencyKeyheader, its value must be prepended to the string to sign (idempotencyKey + timestamp + METHOD + requestURI + body). If you don't need it, don't send it.
If all of these hold and you still get 401, the key may have been deleted: list your keys with GET /v2/auth/api-keys/ (JWT).