1. Home
  2. Developers

EVOR REST API v1

The same API the EVOR mobile app uses. Versioned, tenant-scoped and permission-checked on every call.

Authentication

Every call sends Authorization: Bearer <token>. There are two kinds of token, and both belong to exactly one workspace. A tenant id sent in a header or body is ignored — the workspace always comes from the token.

Mobile and app sign-in

Exchange an email and password for a token that lasts 30 days. If the person works in more than one workspace, the first call returns the list and you sign in again with membership_id.

POST /api/v1/auth/login
{ "email": "tech@yourcompany.ae", "password": "••••••••", "device_name": "Pixel 8" }

201 Created
{ "data": { "token": "12|Vb3…", "token_type": "Bearer", "expires_at": "2026-11-02T10:00:00+00:00",
            "membership_id": "01j…", "workspace": "Gulf Breeze Technical Services" } }

409 Conflict   → { "error": { "code": "choose_workspace" }, "workspaces": [ { "membership_id": "01j…", "tenant": "…" } ] }

POST /api/v1/auth/logout revokes the token. Five wrong passwords lock the account for 15 minutes.

Workspace API keys

For server-to-server work, an owner or admin creates a key in Connect → API keys. Keys start with evk_, are shown once, and act with the permissions of the person who created them. A key with only the read scope can call GET endpoints and nothing else. API access depends on your plan.

Conventions

  • Base URL https://<your-evor-domain>/api/v1. JSON in and out; send Accept: application/json.
  • IDs are 26-character ULIDs. Times are ISO 8601 in UTC; dates are YYYY-MM-DD. Datetime fields you send without an offset are read in the workspace timezone.
  • Permissions are the same as on screen. A technician's token sees only assigned jobs; a sales token sees only the records its role allows. Records outside your scope return 404, never 403, so their existence is not revealed.
  • Idempotency. Any write can send Idempotency-Key: <uuid>. Repeating the call with the same key returns the first response with Idempotent-Replayed: true instead of running again. The mobile app uses this for its offline queue.
  • Client time. Field actions accept client_time (when it happened on the device) as well as the server's own time.
  • Optimistic concurrency. Records carry a version. Send the version you read on PATCH; if someone saved in between you get 409 version_conflict with the current record.
  • Lists are paginated: ?page=2&per_page=50 (max 100). The response includes meta.total and meta.last_page. Use updated_since for incremental sync.
  • Rate limit: 240 requests per minute per token. Each response carries X-Correlation-Id — quote it when you contact support.

Errors

Errors always have the same shape:

422 Unprocessable Content
{ "error": { "code": "validation_failed", "message": "The legal name field is required.",
             "fields": { "legal_name": ["The legal name field is required."] } } }
Status and codeMeaning
401 session_invalidToken missing, expired or revoked, or the member was deactivated. Sign in again.
402 plan_limitThe workspace reached a plan limit (users, records, storage).
403 forbiddenThe role cannot do this, the module is not enabled, or the status does not allow the change.
404 not_foundThe record does not exist or is outside your scope.
409 conflict, version_conflict, possible_duplicateThe record changed, the action no longer applies, or a similar record exists (send ignore_duplicates=true to continue).
422 validation_failedField errors are in error.fields.
429 rate_limitedSlow down; retry after the Retry-After seconds.

Records

Every record type in EVOR uses the same endpoints. {resource} is the type key — for example customers, contacts, leads, opportunities, quotations, work_orders, service_requests, assets, amc_contracts, invoices, payments, products, purchase_orders, tasks. GET /schema lists the types your token can open, with every field, its type and whether it is required.

EndpointWhat it does
GET/{resource}List. Filters: q (search), view (saved view such as open or overdue), status and other listed filters, updated_since, sort, dir, page, per_page.
POST/{resource}Create. Fields you leave out take the workspace defaults.
GET/{resource}/{id}Read one record, including the status changes you are allowed to make (transitions).
PATCH/{resource}/{id}Update only the fields you send. Send version to avoid overwriting someone else.
POST/{resource}/{id}/transitionChange status: { "to": "qualified" }. Some changes need { "reason": "…" }. Approvals and lifecycle rules apply.
DELETE/{resource}/{id}Delete where your role allows it. Linked records cannot be deleted; mark them inactive.
GET/{resource}/{id}/timelineAudit trail, activities and comments, newest first.
POST/{resource}/{id}/commentsAdd an internal comment: { "body": "…" }.
POST/{resource}/{id}/activitiesLog a call, email, WhatsApp, meeting, visit or note; add due_at to schedule a follow-up.
POST/{resource}/{id}/filesUpload a file (multipart, field "file", up to 20 MB). Files are private to the workspace.
curl https://app.example.ae/api/v1/customers?q=noor&per_page=10 \
  -H "Authorization: Bearer evk_…" -H "Accept: application/json"

{ "data": [ { "id": "01j…", "number": "CUS-2026-0001", "label": "CUS-2026-0001 Al Noor Facilities Management LLC",
              "status": "active", "version": 3, "legal_name": "Al Noor Facilities Management LLC", "credit_days": 30,
              "owner_membership_id": "01j…", "owner_membership_id_label": "Omar Haddad", … } ],
  "meta": { "page": 1, "per_page": 10, "last_page": 1, "total": 1 } }

Field app

These endpoints power the EVOR mobile app for technicians and supervisors. A person must accept a job before working on it.

EndpointWhat it does
GET/field/jobsMy jobs for today plus anything still open. ?day=2026-10-05 for a specific day.
GET/field/jobs/{id}Job detail: site and directions, contact, team and responses, checklist, materials, completion.
POST/field/jobs/{id}/respond{ "response": "accepted" } or { "response": "declined", "reason": "…" }.
POST/field/jobs/{id}/attendanceCheck in or out: event, lat, lng, accuracy, client_time. Without location, send permission_state and exception_reason; the supervisor is told.
POST/field/jobs/{id}/checklist{ "answers": [ { "id": "…", "response": "1" } ] }.
GET/field/stockStock I can use: my van first, then the main store. ?q= to search.
POST/field/jobs/{id}/materialsRecord material used: product_id, quantity, optional warehouse_id. Stock moves out of the warehouse.
POST/field/jobs/{id}/photosUpload a site photo (multipart "file", captured_at).
POST/field/jobs/{id}/completeresult (completed or partial), work_done, findings, recommendations, customer_signed_name, signature (PNG data URL), client_time. Required checklist items must be answered.

Workspace

EndpointWhat it does
GET/meThe signed-in person, workspace, enabled modules, permissions and workspace terms.
GET/homeThe figures on this person's command centre, using the same definitions as the web app.
GET/search?q=Search customers, contacts, leads, quotations, jobs, invoices, requests and assets.
GET/options/{resource}Picker values, scoped by permission. ?parent=customer_id&value=… for dependent lists such as sites.
GET/notificationsNotifications, newest first. ?unread=1. POST /notifications/read with ids, or no ids to mark all.
GET/approvalsApprovals waiting for me. POST /approvals/{id}/decide with decision and comment.
POST/devicesRegister a push token: platform (android, ios), push_token, app_version.

Webhooks

Add an endpoint in Connect → Webhooks and choose events such as quotation.accepted, work_order.completed, invoice.overdue or payment.received. EVOR sends a POST with this body:

{ "event_id": "01j…", "schema_version": 1, "event": "work_order.completed",
  "tenant": "gulf-breeze", "actor": "tech@yourcompany.ae",
  "subject": { "type": "work_order", "id": "01j…", "label": "WO-2026-0042 Barrier arm service", "version": 7 },
  "occurred_at": "2026-10-03T13:05:11+00:00", "correlation_id": "01j…" }

Fetch the full record with GET /api/v1/{resource}/{id} when you need more than the summary.

Each request carries X-Evor-Event, X-Evor-Delivery (use it to ignore repeats), X-Evor-Timestamp and X-Evor-Signature. Verify the signature before trusting the body:

expected = "sha256=" + hex(hmac_sha256(secret, timestamp + "." + raw_body))
accept only if constant_time_equals(expected, X-Evor-Signature) and timestamp is within 5 minutes

Reply with any 2xx within 10 seconds. Failed deliveries are retried after 1, 5 and 30 minutes, then 2, 6 and 24 hours. An endpoint that fails 20 times in a row is paused and shown as unhealthy in Connect.