Integration API

The official, versioned REST API for connecting external systems to ArqaMed (for example, the МИС «Жетысу» sync). It is authenticated with API keys and lives entirely under /api/v1. Every request is scoped to a single clinic.

base url
https://arqamed.com/api/v1

Overview

  • All endpoints require an API key sent as a Bearer token (except the public OpenAPI spec).
  • A key is bound to one clinic (tenant) and only ever returns or writes that clinic's data.
  • The API is isolated — a key can reach /api/v1 only, never an internal app route.
  • Read access needs a *:read scope; writes need an explicit *:write scope.
  • All request and response bodies are JSON. Timestamps are ISO 8601 (UTC).

Authentication

Create a key as a clinic director under Settings → API keys. The full key is shown once at creation and stored only as a hash — it cannot be retrieved again. Send it in the Authorization header:

http
Authorization: Bearer ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Each key can be revoked at any time (effective immediately) and may have an optional expiry. Example request:

bash
curl -s \
  -H "Authorization: Bearer $ARQAMED_KEY" \
  "https://arqamed.com/api/v1/patients?limit=20"

Scopes

Each key carries a least-privilege set of scopes. A request whose key lacks the required scope returns 403.

ScopeGrants
patients:readGET /patients
patients:writePOST /patients, PATCH /patients/{id}
appointments:readGET /appointments
appointments:writePATCH /appointments/{id}
deals:readGET /deals
doctors:readGET /doctors

Rate limits

Each key is limited to 120 requests per minute by default. Exceeding it returns 429 with a Retry-After header (seconds until the window resets). Limiting is enforced per key in the database, so it holds across all servers.

Pagination

List endpoints accept ?limit (default 50, max 100) and ?offset (default 0), and return a stable envelope:

json
{
  "data": [ /* … items … */ ],
  "timezone": "Asia/Almaty",
  "pagination": { "limit": 50, "offset": 0, "total": 137 }
}

Timestamps & timezone

Every timestamp — startsAt, endsAt, createdAt, etc. — is a UTC instant in ISO 8601, always ending in Z. These are the exact same instants the CRM calendar runs on; the CRM simply renders them in the clinic's timezone. To display a time that matches what staff see in the CRM, convert the UTC instant into the clinic's zone.

That zone is returned on every response as timezone (an IANA name, e.g. Asia/Almaty) — resolved from the clinic's own settings, so it always agrees with the CRM. Do not assume the server's zone.

js
// A response gives you both the instant and the zone to read it in:
//   "startsAt": "2026-06-18T05:00:00.000Z",  "timezone": "Asia/Almaty"

const localTime = new Intl.DateTimeFormat('ru-RU', {
  timeZone: appt.timezone,          // "Asia/Almaty" (UTC+5)
  hour: '2-digit', minute: '2-digit',
}).format(new Date(appt.startsAt));
// → "10:00" — matches the appointment as shown in the CRM

Errors

Errors are JSON. Every error carries a canonical error code (switch on this) and a human message. Validation errors add an issues array; duplicate 409s add existingPatient. A same-valued code alias may also be present.

StatusMeaning
400Validation failed / unrecognized phone number
401Missing, invalid, revoked, or expired key
403Key lacks the required scope
404Target not found in this clinic
409Duplicate patient (IIN/phone), or a reschedule slot clash
429Per-key rate limit exceeded
500Unexpected server error

Patients

GET/api/v1/patientspatients:read

List the clinic's patients, newest first. Supports limit / offset.

json
// 200 OK
{
  "data": [
    {
      "id": "6f0c…",
      "externalId": "ZH-10432",
      "externalSource": "zhetysu",
      "fullName": "Айгүл Нұрланова",
      "phone": "+77071234567",
      "iin": "900723400005",
      "gender": "female",
      "birthDate": "1990-07-23T00:00:00.000Z",
      "balance": 0,
      "tags": [],
      "updatedAt": "2026-07-01T09:00:00.000Z",
      "createdAt": "2026-06-20T11:12:00.000Z"
    }
  ],
  "timezone": "Asia/Almaty",
  "pagination": { "limit": 50, "offset": 0, "total": 137 }
}
POST/api/v1/patientspatients:write

Idempotent upsert keyed on externalId. A patient already present — matched by externalId, then phone, then iin — is updated in place; otherwise created. Returns 201 on create, 200 on update. Phone accepts 8XXX/7XXX/+7XXX; a missing or malformed IIN is tolerated (the field is dropped, the sync is not rejected). comment maps to the patient's notes; updatedAt drives last-write-wins — an update older than the stored copy is skipped and returns { "skipped": true }.

json
// request body
{
  "externalId": "ZH-10432",
  "fullName": "Айгүл Нұрланова",
  "phone": "87071234567",
  "iin": "900723400005",
  "gender": "female",
  "birthDate": "1990-07-23",
  "email": "aigul@example.kz",
  "comment": "Пришла по рекомендации",
  "updatedAt": "2026-07-01T09:00:00Z"
}
json
// 201 Created  (or 200 on update)
{ "data": { "id": "6f0c…", "externalId": "ZH-10432", … }, "skipped": false }
PATCH/api/v1/patients/{id}patients:write

Update an existing patient by its ArqaMed id. Full representationfullName and phone are required every call (externalId is optional here). Returns 404 if the patient is not in this clinic. Prefer POST /patients when you only know your own external id.

Appointments

GET/api/v1/appointmentsappointments:read

List the clinic's appointments, most recent first. Supports limit / offset.

PATCH/api/v1/appointments/{id}appointments:write

Change an appointment's status (arrived / no_show / cancelled / …) and/or reschedule it (startsAt / endsAt). Pass ?externalId=<your id> to resolve the appointment by its external id instead of the ArqaMed id. A reschedule onto an occupied slot returns 409 with the booking-conflict payload. At least one of status, startsAt, endsAt is required.

json
// request body — status change
{ "status": "arrived" }

// or a reschedule
{ "startsAt": "2026-07-02T09:00:00Z", "endsAt": "2026-07-02T09:30:00Z" }
bash
curl -X PATCH \
  -H "Authorization: Bearer $ARQAMED_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"no_show"}' \
  "https://arqamed.com/api/v1/appointments/6f0c…"

Deals & Doctors

GET/api/v1/dealsdeals:read

List active deals (trashed excluded), newest first.

GET/api/v1/doctorsdoctors:read

List the clinic's doctors — use this to map an external system's providers to ArqaMed doctor ids.

Two-way sync notes

  • Idempotency & de-duplication. Repeated POST /patients with the same externalId update the same record — safe to retry and to use for the initial bulk import.
  • Correlation. Store your own ids in externalId; every write is stamped externalSource so both systems can reference the same record.
  • Conflict resolution. Send updatedAt on patient updates; an update older than the stored copy is skipped (last-write-wins).
  • Booking legality. A reschedule is validated against the doctor's working hours and existing bookings — expect a 409 to be possible and handle it.

OpenAPI spec

A machine-readable OpenAPI 3.0 contract is served publicly — import it into Postman, Insomnia, or a code generator.

GET https://arqamed.com/api/v1/openapi.json
Available scopes in this deployment: patients:read, patients:write, appointments:read, appointments:write, deals:read, doctors:read