Skip to main content

Authentication Guide

The EVX Software API uses OAuth 2.0 Client Credentials for server-to-server integrations. Every request carries a short-lived access token in the Authorization header. The credentials only identify which API client is calling; your EVX Software installation decides what that client may do, by having it act as one of its users.

Prerequisites​

1. Request an API client​

API clients are created by EVX Software. Ask support for one for your installation, saying which integration it is for. You receive:

CredentialDescription
client_idThe API client's identifier
client_secretIts secret
Token endpointThe URL your integration requests access tokens from
caution

Treat the client_secret like a password: never expose it in client-side code, public repositories or logs. Store it in environment variables or a secrets manager. If it leaks, ask support for a new one; the old one stops working.

2. Grant it access in your installation​

An administrator of your installation grants that client_id access in Administration → API access, and picks the user it acts as. The client can do everything that user can do and nothing more:

  • Use a dedicated, least-privilege service user — never a real person.
  • Avoid administrators: they bypass permission checks, so the client would have full access.

Revoking the client there stops its calls at once.

Base URL​

The API is served by your installation:

https://<your-installation>/api/v3

Step 1: Obtain an Access Token​

Send the credentials to the token endpoint as a form (application/x-www-form-urlencoded) with grant_type=client_credentials.

Using PHP cURL​

<?php
$tokenUrl = getenv('EVX_TOKEN_URL');
$clientId = getenv('EVX_CLIENT_ID');
$clientSecret = getenv('EVX_CLIENT_SECRET');

$ch = curl_init($tokenUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'client_credentials',
'client_id' => $clientId,
'client_secret' => $clientSecret,
]),
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode !== 200) {
throw new RuntimeException("Token request failed with HTTP {$httpCode}: {$response}");
}

$tokenData = json_decode($response, true);
$accessToken = $tokenData['access_token'];
$expiresIn = $tokenData['expires_in'];

echo "Access token obtained, expires in {$expiresIn} seconds.\n";

Using PHP Guzzle​

<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;

$http = new Client(['timeout' => 10]);

try {
$response = $http->post(getenv('EVX_TOKEN_URL'), [
'form_params' => [
'grant_type' => 'client_credentials',
'client_id' => getenv('EVX_CLIENT_ID'),
'client_secret' => getenv('EVX_CLIENT_SECRET'),
],
]);

$tokenData = json_decode($response->getBody(), true);
$accessToken = $tokenData['access_token'];
$expiresIn = $tokenData['expires_in'];

echo "Access token obtained, expires in {$expiresIn} seconds.\n";
} catch (ClientException $e) {
$errorBody = $e->getResponse()->getBody()->getContents();
throw new RuntimeException("Authentication failed: {$errorBody}");
}

Step 2: Use the Token in API Requests​

Include the access token as a Bearer token in the Authorization header of every request:

<?php
// cURL example
$base = 'https://' . getenv('EVX_INSTALLATION') . '/api/v3';

$ch = curl_init($base . '/projects?limit=50');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
'Accept: application/json',
],
]);
$answer = json_decode(curl_exec($ch), true);
curl_close($ch);

$projects = $answer['data']['projects'];
<?php
// Guzzle example
$api = new Client([
'base_uri' => 'https://' . getenv('EVX_INSTALLATION') . '/api/v3/',
'timeout' => 10,
]);

$response = $api->get('projects', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
'Accept' => 'application/json',
],
'query' => ['limit' => 50],
]);
$projects = json_decode($response->getBody(), true)['data']['projects'];

Every answer is wrapped: { "ok": true, "data": … } on success, and { "ok": false, "error": { "code", "message", "details"? } } on failure.


Step 3: Request a New Token When It Expires​

Access tokens are short-lived (expires_in, in seconds) and client credentials come with no refresh token: when one expires, repeat Step 1. Cache the token and reuse it until shortly before it expires, instead of requesting one per call:

<?php
function evxAccessToken(bool $renew = false): string
{
static $token = null;
static $expiresAt = 0;

// Renew 30 seconds before expiry, or when asked to.
if (!$renew && $token !== null && time() < $expiresAt - 30) {
return $token;
}

$http = new GuzzleHttp\Client(['timeout' => 10]);
$data = json_decode($http->post(getenv('EVX_TOKEN_URL'), [
'form_params' => [
'grant_type' => 'client_credentials',
'client_id' => getenv('EVX_CLIENT_ID'),
'client_secret' => getenv('EVX_CLIENT_SECRET'),
],
])->getBody(), true);

$token = $data['access_token'];
$expiresAt = time() + (int) $data['expires_in'];
return $token;
}
tip

In a long-running worker, keep the token in a shared cache (Redis, APCu, a file) so every process reuses it.


Error Handling​

HTTP CodeWhereMeaningAction
400/401Token endpointWrong grant_type, client_id or client_secret (invalid_client)Check the credentials; ask support if they were rotated
401APIMissing, invalid or expired token, or a client this installation has not granted access toRequest a new token; check Administration → API access
403APIThe user the client acts as may not do thisGive that user the permission, or use another endpoint

Sending the token and retrying once on 401​

<?php
use GuzzleHttp\Client;

$api = new Client([
'base_uri' => 'https://' . getenv('EVX_INSTALLATION') . '/api/v3/',
'timeout' => 10,
'http_errors' => false, // read the error envelope instead of throwing
]);

function evxRequest(Client $api, string $method, string $path, array $options = []): array
{
// The token may expire between two calls: on a 401, get a new one and retry once.
foreach ([false, true] as $renew) {
$options['headers']['Authorization'] = 'Bearer ' . evxAccessToken($renew);
$options['headers']['Accept'] = 'application/json';
$response = $api->request($method, $path, $options);
if ($response->getStatusCode() !== 401) {
break;
}
}

$answer = json_decode($response->getBody(), true);
if (!$answer['ok']) {
throw new RuntimeException($answer['error']['code'] . ': ' . $answer['error']['message']);
}
return $answer['data'];
}

$projects = evxRequest($api, 'GET', 'projects', ['query' => ['limit' => 50]])['projects'];

Optional Modules​

Some resources belong to modules that not every installation runs. Read the module's bootstrap first and hide what is not there:

<?php
$config = evxRequest($api, 'GET', 'expenses/config');

if ($config['available']) {
$actualExpenses = evxRequest($api, 'GET', 'expenses/actual')['actual_expenses'];
}
info

GET /expenses/config always answers 200; with available: false the module is not active, and every other /expenses/… route answers 404 with message: "expenses".


Security Best Practices​

  1. Store credentials securely — environment variables (getenv()) or a secrets manager. Never hardcode them.
  2. Use HTTPS only — never send tokens over unencrypted connections.
  3. Least privilege — the client acts as one user of your installation: give that user only the permissions the integration needs.
  4. Cache tokens — reuse the access token until shortly before it expires.
  5. Retry safely — send an Idempotency-Key header on every POST, PUT, PATCH and DELETE, so a retry never runs a write twice.
  6. Log responsibly — never log access tokens or client secrets; log only non-sensitive metadata (HTTP status, request path).

Token Response Reference​

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 300
}
FieldTypeDescription
access_tokenstringBearer token for API requests
token_typestringAlways Bearer
expires_inintegerToken lifetime in seconds

The answer may carry other standard OAuth 2.0 fields; read only these three.