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.
https://arqamed.com/api/v1Overview
- 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/v1only, never an internal app route. - Read access needs a
*:readscope; writes need an explicit*:writescope. - 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:
Authorization: Bearer ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxEach key can be revoked at any time (effective immediately) and may have an optional expiry. Example request:
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.
| Scope | Grants |
|---|---|
patients:read | GET /patients |
patients:write | POST /patients, PATCH /patients/{id} |
appointments:read | GET /appointments |
appointments:write | PATCH /appointments/{id} |
deals:read | GET /deals |
doctors:read | GET /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:
{
"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.
// 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 CRMErrors
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.
| Status | Meaning |
|---|---|
| 400 | Validation failed / unrecognized phone number |
| 401 | Missing, invalid, revoked, or expired key |
| 403 | Key lacks the required scope |
| 404 | Target not found in this clinic |
| 409 | Duplicate patient (IIN/phone), or a reschedule slot clash |
| 429 | Per-key rate limit exceeded |
| 500 | Unexpected server error |
Patients
/api/v1/patientspatients:readList the clinic's patients, newest first. Supports limit / offset.
// 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 }
}/api/v1/patientspatients:writeIdempotent 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 }.
// 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"
}// 201 Created (or 200 on update)
{ "data": { "id": "6f0c…", "externalId": "ZH-10432", … }, "skipped": false }/api/v1/patients/{id}patients:writeUpdate an existing patient by its ArqaMed id. Full representation — fullName 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
/api/v1/appointmentsappointments:readList the clinic's appointments, most recent first. Supports limit / offset.
/api/v1/appointments/{id}appointments:writeChange 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.
// request body — status change
{ "status": "arrived" }
// or a reschedule
{ "startsAt": "2026-07-02T09:00:00Z", "endsAt": "2026-07-02T09:30:00Z" }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
/api/v1/dealsdeals:readList active deals (trashed excluded), newest first.
/api/v1/doctorsdoctors:readList 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 /patientswith the sameexternalIdupdate the same record — safe to retry and to use for the initial bulk import. - Correlation. Store your own ids in
externalId; every write is stampedexternalSourceso both systems can reference the same record. - Conflict resolution. Send
updatedAton 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.jsonpatients:read, patients:write, appointments:read, appointments:write, deals:read, doctors:read