eva crm Public API

Conventions

Ids, dates, option values, errors, idempotency and retries — the rules every endpoint follows.

Requests and responses

  • JSON in, JSON out. Send Content-Type: application/json on POST and PUT, except the attachment upload which is multipart/form-data.
  • Every endpoint lives under /v1. Nothing under it will be removed or renamed; see the changelog for how changes are made.
  • GET /health is unauthenticated and returns { "status": "ok" }. It is there for monitoring; it does not check the database.

Every write endpoint's page lists its fields in a table with a Required column. Anything not marked required can be left out, and the notes say what happens when it is. PATCH bodies never require a particular field, only at least one.

Ids

Every record is identified by a UUID string, returned as id. Numeric ids are never exposed and never accepted. Ids are stable for the life of the record; names are not, since staff can rename things in the CRM. Store ids.

Dates and timezones

The API works in your organisation's timezone, the one set in the CRM's organisation settings (Europe/London for UK customers). Storage is always UTC; the zone decides how dates are read from you and shown to you. GET /v1/whoami says which zone is in effect. To use another one for a request, send X-Timezone with an IANA name, e.g. X-Timezone: Europe/Dublin; an unknown name is a 400.

Every timestamp you receive carries the zone's offset for that instant:

2026-07-10T09:00:00.000+01:00    a July morning, British Summer Time
2026-12-10T09:00:00.000+00:00    a December morning, GMT

Both are what a colleague sees in the CRM. Any ISO 8601 parser reads them correctly, and the UTC instant is unchanged.

Every date you send without an offset is read in the zone. "2026-07-10T09:00" means 9am BST and is stored as 08:00 UTC; "2026-12-10T09:00" means 9am GMT. The offset is worked out for the date you send, not for today, so a July time sent in December is still BST. A value with Z or an explicit offset is taken exactly as written.

A bare date is midnight in the zone. "2026-07-10" is read as 00:00 on 10 July in your timezone (23:00 UTC the evening before, in summer), which is exactly what the CRM's own date pickers store, so the CRM and the API agree which day it is. It comes back as 2026-07-10T00:00:00.000+01:00. Fields staff usually set by date — expectedCloseDate, and soldAt, lostAt, onHoldAt — are still full timestamps, because the CRM also sets them automatically with a real time when a stage changes. Read them by their date part.

Accepted input forms: YYYY-MM-DD, YYYY-MM-DDTHH:MM, YYYY-MM-DDTHH:MM:SS, optionally with milliseconds and Z or ±HH:MM. Anything else, including 10/07/2026, is a 422 saying so.

Option values

Fields that map to a dropdown in the CRM (leadType, marketing, country, assignedTo, and so on) accept either the option's id or its name. Names match case-insensitively and ignore surrounding whitespace. A name that matches more than one option is a 422 asking for the id. GET /v1/leads/fields lists every option with both.

An empty string means "not set", the same as leaving the field out, because HTML forms send "" for an unselected dropdown.

Booleans

true, "true", "yes", "y", "1", "on" and 1 are all true. Anything else is false.

Errors

Every error has the same shape:

{
  "error": "Invalid lead payload",
  "fields": {
    "email": "Must be a valid email address",
    "leadType": "No lead.type option named \"Windows & Doors\""
  }
}

error is a sentence for a human. fields, when present, is keyed by the offending property or query parameter and says what to change. Some responses add a hint.

StatusMeaningWhat to do
200Done. On a create with a reference already seen, created: false
201Created
400The body could not be read (malformed JSON or multipart)Fix the request
401Key missing, invalid or expiredSee Authentication
403The key is valid but this action is not allowed on this recordRead the error
404No such record in your organisation, or no such routeCheck the id
413A file or the request is over the size limitSend less
415Wrong Content-TypeRead the endpoint's page
422The request was understood but is invalidFix the field(s) named in fields
429Too many requestsWait for Retry-After seconds
500Our faultRetry later; we are alerted automatically
502The CRM behind the API did not answerSafe to retry — see below
503A feature is not enabled on this environment, or a query took too longRead the error

Idempotency and retries

Network calls fail. The API is designed so that retrying is always safe:

  • POST /v1/leads is idempotent on reference. Sending the same reference again returns the original lead with 200 and created: false. Nothing is duplicated.
  • POST /v1/leads/{id}/attachments accepts an optional Idempotency-Key header. A replay with the same key returns the original files without storing anything.
  • PUT /v1/leads/{id}/stage to a stage the lead is already in returns changed: false.
  • DELETE of something already deleted is a 404, which your code can treat as success.

A 502 means the API could not reach the CRM. The request was not applied; repeat it as sent. A 500 may or may not have been applied, which is why the operations above are built so that repeating them is harmless.

Audit trail

Everything the API changes is recorded in the CRM's audit log exactly as a staff change would be: lead and customer creation, every field a patch touches, stage moves, assignments, and files added or removed. Each entry is marked as made through the API, shows which API key made it by name, and is attributed to the user who created that key. Name keys after the integration they belong to, so staff can read the log.

The same goes for the CRM's search: every record the API creates or changes is re-indexed at once. Endpoint pages take both for granted and do not repeat it.

Caching

Reference data (/v1/leads/fields, /v1/users) is served with Cache-Control: private, max-age=300. Fetch it when your integration starts and after any 422, not on every request. Everything else is no-store.

Rate limits

None are enforced today. Design for 60 requests per minute per key; when limiting is switched on it will be announced in the changelog and enforced with 429 and Retry-After.

Test data

test: true on a lead marks it as test data in the CRM and excludes it from reporting. Lists exclude test leads unless you ask for them with test=include or test=only.

On this page