Developers

The CarelyCloud API.

The same API our own app uses, with an API key instead of a login. Appointments, patients, availability, and webhooks for anything that happens in the clinic. Included in the Group and Enterprise plans; the free trial can try it.

Authentication

An admin creates keys in Settings → Integrations. The key is shown once. Send it on every request in the X-API-Key header.

curl https://carelycloud-production.up.railway.app/clinics/me \
  -H "X-API-Key: cc_live_xxxxxxxxxxxxxxxxxxxx"

A key acts for the clinic, not for a person, and it can only touch part of the API:

  • Read: appointments, patients (with notes and files), practitioners, availability, services, the clinic profile and dashboard stats.
  • Write: appointments and patients, including their notes and files.
  • Everything else (team, settings, billing, integrations, AI notes) answers 403 API_KEY_SCOPE.

Access is checked on every request against the clinic's plan. When a trial ends or a subscription is suspended, every key answers 402 immediately — nothing to revoke by hand. Revoking a key in Settings takes effect on the next request.

Conventions

  • Base URL: https://carelycloud-production.up.railway.app
  • JSON in and out. Send Content-Type: application/json on writes.
  • Dates are ISO 8601 with timezone. We store UTC; send whatever offset you like and we convert. Slots come back in the clinic's timezone.
  • Lists that paginate take page (1-based) and limit. GET /appointments without page returns the whole range.
  • Errors are JSON with a stable code, a message and optional details:
{ "code": "SLOT_TAKEN", "message": "SLOT_TAKEN", "details": null }

Appointments

GET/appointmentsList appointments. Filters: start, end (ISO 8601), doctor_id, patient_id, location_id, status, q, page, limit.
POST/appointmentsCreate an appointment. Validates working hours and overlaps; the location (location_id) is set by the server from the practitioner's schedule.
GET/appointments/{id}One appointment.
PATCH/appointments/{id}Reschedule or change status (see transitions).
DELETE/appointments/{id}Cancel. Completed or no-show appointments cannot be cancelled.
GET/appointments/{id}/auditChange history of one appointment.

Filter by range (start, end), practitioner, patient, status, or free text (q). Without a range you get everything, so pass one.

curl "https://carelycloud-production.up.railway.app/appointments?start=2026-09-15T00:00:00Z&end=2026-09-15T23:59:59Z&doctor_id=3" \
  -H "X-API-Key: cc_live_..."

Creating an appointment goes through the same rules as the app. The practitioner is required, and the slot must be inside their working hours and free:

curl -X POST https://carelycloud-production.up.railway.app/appointments \
  -H "X-API-Key: cc_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "doctor_id": 3,
    "service_id": 1,
    "patient_id": 42,
    "start_at": "2026-09-15T15:00:00Z",
    "end_at":   "2026-09-15T15:30:00Z",
    "status":   "confirmed",
    "channel":  "manual",
    "notes":    "Booked from our CRM"
  }'

What a create or reschedule can answer, and why

  • DOCTOR_REQUIRED — no doctor_id was sent.
  • DOCTOR_HAS_NO_SCHEDULE — the practitioner has no working hours set yet.
  • OUTSIDE_WORKING_HOURS — the time is outside their hours, in a break, or on a blocked day.
  • SLOT_TAKEN (409) — another active appointment overlaps.

To avoid guessing, ask GET /doctors/{id}/slots for a date first and pick one marked available.

Status changes go through PATCH. Allowed transitions: scheduled → confirmed | cancelled | no_show; confirmed → completed | cancelled | no_show. Completed, cancelled and no_show are final. DELETE is a shortcut for cancelling.

curl -X PATCH https://carelycloud-production.up.railway.app/appointments/981 \
  -H "X-API-Key: cc_live_..." -H "Content-Type: application/json" \
  -d '{ "status": "cancelled", "cancel_reason": "Patient called" }'

Patients

GET/patientsList patients. Filters: q, doctor_id, has_no_shows, include_inactive, page, limit.
POST/patientsCreate a patient. 409 if a possible duplicate exists, unless force=true.
GET/patients/{id}One patient.
PATCH/patients/{id}Update contact data or notes.
DELETE/patients/{id}Archive a patient. Their appointments stay.
GET/patients/{id}/appointmentsA patient's appointments, newest first.
GET/patients/{id}/notesConsultation notes of a patient.
POST/patients/{id}/notesAdd a consultation note.
GET/patients/{id}/filesFiles attached to a patient.
POST/patients/{id}/filesAttach a file (multipart/form-data, 10 MB max).
GET/patients/exportCSV of all patients.
POST/patients/importImport patients from CSV (multipart/form-data).

Creating a patient checks for likely duplicates by phone or email and answers 409 with the matches; pass force=true to create anyway.

curl -X POST "https://carelycloud-production.up.railway.app/patients?force=false" \
  -H "X-API-Key: cc_live_..." -H "Content-Type: application/json" \
  -d '{
    "first_name": "Marta",
    "last_name":  "Ríos",
    "phone":      "+50499990000",
    "email":      "marta@example.com",
    "birth_date": "1992-04-12",
    "gender":     "female",
    "preferred_channel": "whatsapp"
  }'

Search matches name, phone and email. Pagination is 20 per page by default, 500 maximum.

curl "https://carelycloud-production.up.railway.app/patients?q=marta&page=1&limit=20" -H "X-API-Key: cc_live_..."

Practitioners, availability and services

Read-only. Practitioners and services are managed from the app.

GET/doctorsPractitioners of the clinic.
GET/doctors/{id}/availabilityWeekly working hours of a practitioner.
GET/doctors/{id}/exceptionsDays off and one-off changes.
GET/doctors/{id}/slots?date=YYYY-MM-DD&service_id=NBookable slots of a practitioner for one day.
GET/doctors/{id}/servicesServices a practitioner offers.
GET/servicesService catalogue with duration and price.
GET/locationsActive locations of the clinic, main one first.
GET/clinics/meYour clinic: name, timezone, plan.
GET/dashboard/statsToday's numbers as the dashboard shows them.
curl "https://carelycloud-production.up.railway.app/doctors/3/slots?date=2026-09-15&service_id=1" -H "X-API-Key: cc_live_..."

[
  { "start_at": "2026-09-15T15:00:00-06:00", "end_at": "2026-09-15T15:30:00-06:00", "available": true },
  { "start_at": "2026-09-15T15:15:00-06:00", "end_at": "2026-09-15T15:45:00-06:00", "available": false }
]

Webhooks

An admin registers an https URL in Settings → Integrations and picks the events. We POST each one as it happens, once, with an 8-second timeout. No retries: if your endpoint is down, the delivery shows as failed in the log and you can replay from the API.

appointment.createdan appointment was created (app, booking page, or API)
appointment.updatedtime, practitioner, service or notes changed
appointment.confirmedstatus became confirmed
appointment.completedstatus became completed
appointment.cancelledcancelled (by staff, patient, or API)
appointment.no_showmarked as no-show
patient.createda patient was created (app, booking page, import, or API)

Every delivery carries the event name and an HMAC-SHA256 signature of the raw body, using the secret shown once when the endpoint was created:

POST https://your-server.example.com/carelycloud
Content-Type: application/json
X-CarelyCloud-Event: appointment.confirmed
X-CarelyCloud-Signature: sha256=3f1a...

{
  "event": "appointment.confirmed",
  "created_at": "2026-09-15T14:02:11.480Z",
  "data": {
    "id": 981, "clinic_id": 6, "doctor_id": 3, "service_id": 1, "patient_id": 42,
    "start_at": "2026-09-15T15:00:00Z", "end_at": "2026-09-15T15:30:00Z",
    "status": "confirmed", "channel": "online"
  }
}

Verify the signature before trusting a payload. Compare with a constant-time function:

# Python
import hmac, hashlib
def verify(secret: str, raw_body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)

// Node
const crypto = require("crypto")
function verify(secret, rawBody, header) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header))
}
  • Only https URLs on public hosts. Redirects are not followed.
  • Respond 2xx within 8 seconds; do the work afterwards.
  • The log keeps the last 100 deliveries per endpoint, with status and duration.
  • The Test button in Settings sends a sample event so you can check your endpoint before going live.

Zapier, n8n and Make

They do not need a native app: a webhook trigger plus an HTTP action covers the whole API.

  • Zapier: a "Webhooks by Zapier · Catch Hook" trigger receives events; a "Webhooks by Zapier · Custom Request" action calls the API with the X-API-Key header.
  • n8n: the Webhook node receives events; the HTTP Request node calls the API. Use a Header Auth credential for the key.
  • Make: the Custom Webhook module receives events; the HTTP module calls the API.

Typical first automation: patient.created → add the patient to your CRM; appointment.confirmed → post to the clinic's Slack or WhatsApp group.

Errors

401INVALID_API_KEYMissing, unknown or revoked key.
402TRIAL_EXPIRED · SUBSCRIPTION_INACTIVE · PLAN_UPGRADE_REQUIREDThe clinic's plan does not allow integrations right now.
403API_KEY_SCOPEThe key cannot reach this route (see Authentication).
404APPOINTMENT_NOT_FOUND · PATIENT_NOT_FOUND · DOCTOR_NOT_FOUND · SERVICE_NOT_FOUNDThe resource does not exist in this clinic.
409SLOT_TAKENOverlapping appointment, or likely duplicate patient (pass force=true).
422VALIDATION_ERROR · OUTSIDE_WORKING_HOURS · DOCTOR_HAS_NO_SCHEDULE · INVALID_TRANSITIONValidation failed; details says which field.
429RATE_LIMITEDToo many requests; wait and retry.

Something missing, or building something that needs more than this? Write to us — we answer.