API Authentication
The ADA API uses JWT-based authentication for the /authenticates/api-code endpoint.
Quick Start
- Generate a key pair using one of the supported algorithms (ES256, RS256, RS384, RS512, ES384, or ES512).
- Register your public key with Visma Amili AB and obtain your API code.
- Create a JWT signed with your private key.
- Authenticate and receive an access token.
- Use the access token in the X-API-Key header for all subsequent API requests.
- Refresh token as needed.
GET /authenticates/api-code (JWT-based Authentication)
Purpose: Authenticate using a JWT (JSON Web Token) signed with a private key.
Method: GET
Headers:
X-API-Key: JWT token containing the API code and expiration
Prerequisites:
- Generate a key pair using one of the supported algorithms
- Register your public key with Visma Amili AB
- Obtain your API code from Visma Amili AB
Supported Algorithms:
The API supports the following JWT signing algorithms:
- ES256 - ECDSA using P-256 curve and SHA-256 hash
- ES384 - ECDSA using P-384 curve and SHA-384 hash
- ES512 - ECDSA using P-521 curve and SHA-512 hash
- RS256 - RSA signature with SHA-256 hash
- RS384 - RSA signature with SHA-384 hash
- RS512 - RSA signature with SHA-512 hash
Key Generation Examples:
These commands generate an ES256 key pair — the algorithm used throughout this guide. The private key signs your JWTs; the public key is the one you register with Visma Amili AB. To use a different algorithm, see the variations below.
# Generate the private key (ES256 / P-256 curve)
openssl ecparam -name prime256v1 -genkey -noout -out jwt.private.ec.key
# Derive the public key to register with Visma Amili AB
openssl ec -in jwt.private.ec.key -pubout -out jwt.public.ec.key# ssh-keygen ships with Windows 10 (1809+) and Windows 11 by default.
# (Missing it? Enable "OpenSSH Client" under Settings > Optional Features.)
# Generate the private key (ES256 / P-256 curve).
# At the passphrase prompts, press Enter twice to leave it empty: the auth code reads
# this key directly to sign JWTs, so an encrypting passphrase would just block signing.
ssh-keygen -t ecdsa -b 256 -m PEM -f jwt.private.ec.key
# Derive the public key to register with Visma Amili AB.
# Pipe to Set-Content (not `>`) so the file is plain ASCII, not UTF-16/BOM.
ssh-keygen -f jwt.private.ec.key -e -m PKCS8 | Set-Content jwt.public.ec.key -Encoding asciiOther algorithms:
- ES384 / ES512 — change the curve only. OpenSSL:
-name secp384r1or-name secp521r1; ssh-keygen:-b 384or-b 521. Everything else stays the same. - RS256 / RS384 / RS512 — all three share a single RSA key (the number is the SHA hash size, not the key size), generated as follows:
openssl genrsa -out jwt.private.rsa.key 2048
openssl rsa -in jwt.private.rsa.key -pubout -out jwt.public.rsa.key# Press Enter twice at the passphrase prompts to leave it empty (see the ES256 note above).
ssh-keygen -t rsa -b 2048 -m PEM -f jwt.private.rsa.key
ssh-keygen -f jwt.private.rsa.key -e -m PKCS8 | Set-Content jwt.public.rsa.key -Encoding asciiTIP
This documentation uses ES256 in the code examples, but you can use any of the supported algorithms. Make sure to use the same algorithm when signing your JWT as the one you registered with your public key.
JWT Token Structure:
{
"api_code": "your-api-code",
"exp": "expiration-timestamp"
}Implementation Details:
- JWT should expire after 10 minutes (set exp claim to UNIX timestamp)
- Requires a private key matching one of the supported algorithms
- The JWT is signed with the private key and sent in the
X-API-Keyheader - The algorithm used must match the public key you registered with Visma Amili AB
Example from code:
from datetime import datetime, timedelta
import jwt
exp = int((datetime.utcnow() + timedelta(minutes=10)).timestamp())
# Use the algorithm matching your registered public key (ES256, RS256, etc.)
token = jwt.encode({"api_code": api_code, "exp": exp}, private_key, algorithm='ES256')
auth = requests.get(
url=f"{self.api_url}/authenticates/api-code",
headers={"X-API-Key": token}
)Response Format
The endpoint returns the following response structure upon successful authentication:
{
"token": "access-token-for-subsequent-requests"
}Usage in the API
The returned access token is used for all subsequent API requests in the X-API-Key header.
Example use on POST /case-registrations:
auth = requests.post(
url=f"{api_url}/case--registrations",
headers={"X-API-Key": token},
json=case_data
)Token Expiry and Refresh
- JWT tokens (sent by client): Should expire after 10 minutes for security
- Access tokens (returned by API): Have a limited lifetime. The
expclaim in the returned JWT contains the exact expiration as a UNIX timestamp. Clients should decode the token to read this claim and re-authenticate before expiry. - When the access token expires, you must re-authenticate to obtain a new token
Handling Token Expiry
Here's an example of how to handle token expiry and refresh in your code:
import axios, { AxiosError } from 'axios'
import jwt from 'jsonwebtoken'
import { readFileSync } from 'fs'
interface TokenInfo {
token: string
expiryTime: number // in milliseconds
}
class AuthTokenProvider {
private tokenInfo: TokenInfo | null = null
private readonly apiCode: string
private readonly privateKey: Buffer
constructor(apiCode: string, privateKeyPath: string) {
this.apiCode = apiCode
this.privateKey = readFileSync(privateKeyPath)
}
private async getNewAccessToken(): Promise<TokenInfo> {
// Create JWT with 10-minute expiry
const payload = {
api_code: this.apiCode,
exp: Math.floor(Date.now() / 1000) + 10 * 60,
}
// Use the algorithm matching your registered public key
const jwt_token = jwt.sign(payload, this.privateKey, { algorithm: 'ES256' })
const response = await axios.get(
'https://api-sandbox.amili.se/authenticates/api-code',
{
headers: { 'X-API-Key': jwt_token },
}
)
const token = response.data.token
// Decode token to get expiry time
const decodedToken = jwt.decode(token)
if (!decodedToken || typeof decodedToken === 'string') {
throw new Error('Invalid token format received from server')
}
return {
token,
expiryTime: (decodedToken.exp || 0) * 1000, // Convert to milliseconds
}
}
async getValidToken(): Promise<string> {
// If we don't have a token or it's expiring soon, get a new one
if (
!this.tokenInfo ||
Date.now() + 5 * 60 * 1000 >= this.tokenInfo.expiryTime
) {
this.tokenInfo = await this.getNewAccessToken()
}
return this.tokenInfo.token
}
}
// Example usage:
async function makeApiCall() {
const auth = new AuthTokenProvider('your-api-code', 'path/to/private.key')
try {
// Get a valid token
const token = await auth.getValidToken()
// Use the token for your API call
const response = await axios.get(
'https://api-sandbox.amili.se/invoice/123',
{
headers: { 'X-API-Key': token },
}
)
return response.data
} catch (error) {
// Handle errors appropriately
console.error('API call failed:', error)
throw error
}
}import jwt
import requests
from datetime import datetime, timedelta
from dataclasses import dataclass
@dataclass
class TokenInfo:
token: str
expiry_time: int # in milliseconds
class AuthTokenProvider:
def __init__(self, api_code: str, private_key_path: str):
self.api_code = api_code
with open(private_key_path, 'r') as f:
self.private_key = f.read()
self.token_info: TokenInfo | None = None
def _get_new_access_token(self) -> TokenInfo:
# Create JWT with 10-minute expiry
exp = int((datetime.utcnow() + timedelta(minutes=10)).timestamp())
payload = {"api_code": self.api_code, "exp": exp}
# Use the algorithm matching your registered public key
jwt_token = jwt.encode(payload, self.private_key, algorithm='ES256')
response = requests.get(
'https://api-sandbox.amili.se/authenticates/api-code',
headers={'X-API-Key': jwt_token}
)
response.raise_for_status()
token = response.json()['token']
# Decode token to get expiry time
decoded_token = jwt.decode(token, options={"verify_signature": False})
if not isinstance(decoded_token, dict):
raise ValueError('Invalid token format received from server')
return TokenInfo(
token=token,
expiry_time=int(decoded_token.get('exp', 0)) * 1000 # Convert to milliseconds
)
def get_valid_token(self) -> str:
# If we don't have a token or it's expiring soon, get a new one
if (not self.token_info or
int(datetime.now().timestamp() * 1000) + 5 * 60 * 1000 >= self.token_info.expiry_time):
self.token_info = self._get_new_access_token()
return self.token_info.token
# Example usage:
def make_api_call():
auth = AuthTokenProvider('your-api-code', 'path/to/private.key')
try:
# Get a valid token
token = auth.get_valid_token()
# Use the token for your API call
response = requests.get(
'https://api-sandbox.amili.se/invoice/123',
headers={'X-API-Key': token}
)
response.raise_for_status()
return response.json()
except Exception as e:
# Handle errors appropriately
print('API call failed:', e)
raiseusing System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Jose;
using Jose.Jws;
public class TokenInfo
{
public string Token { get; set; }
public long ExpiryTime { get; set; } // in milliseconds
}
public class AuthTokenProvider
{
private TokenInfo _tokenInfo;
private readonly string _apiCode;
private readonly string _privateKey;
public AuthTokenProvider(string apiCode, string privateKeyPath)
{
_apiCode = apiCode;
_privateKey = File.ReadAllText(privateKeyPath);
}
private async Task<TokenInfo> GetNewAccessTokenAsync()
{
// Create JWT with 10-minute expiry
var payload = new
{
api_code = _apiCode,
exp = DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds()
};
// Use the algorithm matching your registered public key
var jwtToken = JWT.Encode(payload, _privateKey, JwsAlgorithm.ES256);
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("X-API-Key", jwtToken);
var response = await httpClient.GetAsync("https://api-sandbox.amili.se/authenticates/api-code");
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
var responseData = JsonSerializer.Deserialize<JsonElement>(responseContent);
var token = responseData.GetProperty("token").GetString();
// Decode token to get expiry time
var decodedToken = JWT.Decode(token);
var tokenData = JsonSerializer.Deserialize<JsonElement>(decodedToken);
if (!tokenData.TryGetProperty("exp", out var expProperty))
{
throw new InvalidOperationException("Invalid token format received from server");
}
return new TokenInfo
{
Token = token,
ExpiryTime = expProperty.GetInt64() * 1000 // Convert to milliseconds
};
}
public async Task<string> GetValidTokenAsync()
{
// If we don't have a token or it's expiring soon, get a new one
if (_tokenInfo == null ||
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + 5 * 60 * 1000 >= _tokenInfo.ExpiryTime)
{
_tokenInfo = await GetNewAccessTokenAsync();
}
return _tokenInfo.Token;
}
}
// Example usage:
public static async Task<object> MakeApiCallAsync()
{
var auth = new AuthTokenProvider("your-api-code", "path/to/private.key");
try
{
// Get a valid token
var token = await auth.GetValidTokenAsync();
// Use the token for your API call
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("X-API-Key", token);
var response = await httpClient.GetAsync("https://api-sandbox.amili.se/invoice/123");
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<object>(responseContent);
}
catch (Exception ex)
{
// Handle errors appropriately
Console.WriteLine($"API call failed: {ex.Message}");
throw;
}
}Security Considerations
- Always use HTTPS for all API communications
- Keep your private key secure and never expose it in client-side code
- Use strong key sizes (minimum 2048 bits for RSA, P-256 or higher for ECDSA)
- Store private keys securely using key management systems or encrypted storage
