Skip to main content

Authentication

Avenia accepts two ways to authenticate a request:

MethodUse it forHow
API key (recommended)Servers and integrationsEach request is signed with your private key. Keys don't expire.
JWTLogging in, managing accesses and API keysLog 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​

  1. Turn on TOTP MFA for your access — creating a key asks for a TOTP code. See the MFA guide.
  2. Generate a key pair. RSA, ECDSA and Ed25519 are supported:
    openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
    openssl rsa -in private_key.pem -pubout -out public_key.pem
    Keep private_key.pem on your server and never share it.
  3. 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:

HeaderValue
X-API-KeyYour API key ID.
X-API-TimestampThe current time in milliseconds since the Unix epoch.
X-API-SignatureThe 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 of X-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:

  1. 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.
  2. 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.
  3. Body. The signed body must be byte-for-byte the body sent — same spacing, same key order. Sign the serialized string, not the object.
  4. IP allow-list. If the key has whitelistedIPs, the request must come from one of those ranges (CIDR).
  5. Environment. A sandbox key doesn't exist in production, and the other way round.
  6. Route. /v2/auth/... routes refuse API keys; use a JWT there.
  7. Key type. RSA and ECDSA sign the SHA-256 of the string; Ed25519 signs the raw string.
  8. Extra headers. If you send the optional X-API-IdempotencyKey header, 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).