EVX Software API v3
The v3 API is served by the EVX Software installation itself.
Authentication
External systems consume an installation's API with an API client that authenticates with OAuth2 client credentials. The credentials only say which client is calling; the installation decides what that client may do, by having it act as one of its users.
-
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 its Client ID, its secret and your installation's token endpoint. Keep the secret like a password; if it leaks, ask for a new one, which invalidates the old.
-
Access in the installation. An administrator 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, and not an administrator, who bypasses permission checks. Revoking it there stops the client's calls at once.
-
A token. Ask the token endpoint you received for one:
curl -s -X POST "<token endpoint>" \
-d grant_type=client_credentials \
-d client_id=<client id> \
-d client_secret=<client secret>
# \{ "access_token": "eyJ…", "expires_in": 300, "token_type": "Bearer", … \}The token is short-lived (
expires_in, in seconds) and comes with no refresh token: request a new one when it expires. -
Every call carries it:
Authorization: Bearer <access_token>.
A missing, invalid or expired token, or a client this installation has not granted access
to, is 401 unauthorized. A call the acting user may not make is 403 forbidden.
Addressing
Pretty URLs (/api/v3/...) need the installation's /api/v3 rewrite. Where it is not
deployed, every operation is also reachable without Apache configuration as
index.php?c=api_v3&a=index&_path=<path> — same method, body and headers.
Envelope
Every JSON answer is wrapped: { "ok": true, "data": … } on success,
{ "ok": false, "error": { "code", "message", "details"? } } on failure. Two operations
answer bytes instead (GET /me/picture, GET /documents/{id}/download).
details is present on validation errors only: a map of field → reason, where the
reason is a short code (required, invalid, not_found, conflict, …) the client can
switch on; message is a readable sentence.
Paging
Lists take start (offset) and limit, and answer total: the size of the whole
filter, not of the page. A client that needs everything walks start up to total.
Dates and money
Civil dates (date, due_date, …) are YYYY-MM-DD with no timezone. Timestamps
(created_on, createdOn, start_time, …) are unix seconds. Amounts are numbers
rounded to the installation's decimals (GET /expenses/config → decimals); an amount
the caller may not see is null, never 0.
Casing — a known inconsistency
Tasks, notes and timeslots use camelCase for some keys (createdOn, percentCompleted,
parentId); projects, documents and expenses are snake_case throughout. Clients should
read each resource's schema as it is; nothing is going to be renamed in v3.
Idempotency and offline sync
A client that works offline queues its writes and replays them when it is back online. Two mechanisms make a replay safe, and they are meant to be used together.
Idempotency-Key: one execution per request
Send a unique key (a UUID per queued mutation) on every POST, PUT, PATCH and DELETE:
Idempotency-Key: 8f3c0a3e-7b2d-4d61-9c4f-2a9e1d0f5b77.
- A retry with the same key and the same body gets the first answer back, status and body, without executing again.
- The same key with a different body is
409 idempotency_key_reuse. - A request still running when the retry arrives is
409 request_in_progress: wait and retry. - Keys are scoped per user and expire after 7 days.
The key protects one request. It does not know that two requests with different keys describe
the same record — that is what uuid is for.
uuid: one record per create
Creates that take a uuid (notes, timeslots, tasks, forms, actual expenses) store it on the
record. A create sent again with a uuid the server already holds answers the existing
record with 200 instead of creating another one with 201. A client that generated the key
and the uuid before going offline can replay a create any number of times, with any key, and
end up with one record. On actual expenses, a uuid naming a record in the trash, one of
another kind, or one the caller cannot see is 422 uuid: conflict.
The replay loop
For each queued mutation, in order:
- Send it with its stored
Idempotency-Key(and, for a create, its storeduuid). 2xx: done; keep the answer (a create'sidis what later mutations on the same record need).409 request_in_progress: wait, retry the same request.409 idempotency_key_reuse: the body changed since the key was issued. Treat it as a bug in the queue; do not re-issue the key.422,403,404: the server refused the content; surface it, do not retry blindly.- Network error: retry later, same key, same body.
A create replayed after the client lost the first answer is the case uuid exists for: the
client has no id yet, replays, gets 200 with the record, and continues. GETs need neither
mechanism. Deleting twice with the same key replays the first { "deleted": true }; with a new
key, the second delete is 404.
Authentication
- HTTP: Bearer Auth
An access token obtained with OAuth2 client credentials by an API client this installation grants access to (Administration → API access); see Authentication in the introduction. The call acts as the user that client is mapped to.
Security Scheme Type: | http |
|---|---|
HTTP Authorization Scheme: | bearer |
Bearer format: | JWT |
License