Skip to main content
Version: 4.3.x

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.

  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 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.

  2. 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.

  3. 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.

  4. 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:

  1. Send it with its stored Idempotency-Key (and, for a create, its stored uuid).
  2. 2xx: done; keep the answer (a create's id is what later mutations on the same record need).
  3. 409 request_in_progress: wait, retry the same request.
  4. 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.
  5. 422, 403, 404: the server refused the content; surface it, do not retry blindly.
  6. 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​

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

Contact

EVX Software:

URL: https://www.evxsoftware.com

License