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 | /appointments | List appointments. Filters: start, end (ISO 8601), doctor_id, patient_id, location_id, status, q, page, limit. |
| POST | /appointments | Create 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}/audit | Change 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 | /patients | List patients. Filters: q, doctor_id, has_no_shows, include_inactive, page, limit. |
| POST | /patients | Create 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}/appointments | A patient's appointments, newest first. |
| GET | /patients/{id}/notes | Consultation notes of a patient. |
| POST | /patients/{id}/notes | Add a consultation note. |
| GET | /patients/{id}/files | Files attached to a patient. |
| POST | /patients/{id}/files | Attach a file (multipart/form-data, 10 MB max). |
| GET | /patients/export | CSV of all patients. |
| POST | /patients/import | Import 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 | /doctors | Practitioners of the clinic. |
| GET | /doctors/{id}/availability | Weekly working hours of a practitioner. |
| GET | /doctors/{id}/exceptions | Days off and one-off changes. |
| GET | /doctors/{id}/slots?date=YYYY-MM-DD&service_id=N | Bookable slots of a practitioner for one day. |
| GET | /doctors/{id}/services | Services a practitioner offers. |
| GET | /services | Service catalogue with duration and price. |
| GET | /locations | Active locations of the clinic, main one first. |
| GET | /clinics/me | Your clinic: name, timezone, plan. |
| GET | /dashboard/stats | Today'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.created | an appointment was created (app, booking page, or API) |
| appointment.updated | time, practitioner, service or notes changed |
| appointment.confirmed | status became confirmed |
| appointment.completed | status became completed |
| appointment.cancelled | cancelled (by staff, patient, or API) |
| appointment.no_show | marked as no-show |
| patient.created | a 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
| 401 | INVALID_API_KEY | Missing, unknown or revoked key. |
| 402 | TRIAL_EXPIRED · SUBSCRIPTION_INACTIVE · PLAN_UPGRADE_REQUIRED | The clinic's plan does not allow integrations right now. |
| 403 | API_KEY_SCOPE | The key cannot reach this route (see Authentication). |
| 404 | APPOINTMENT_NOT_FOUND · PATIENT_NOT_FOUND · DOCTOR_NOT_FOUND · SERVICE_NOT_FOUND | The resource does not exist in this clinic. |
| 409 | SLOT_TAKEN | Overlapping appointment, or likely duplicate patient (pass force=true). |
| 422 | VALIDATION_ERROR · OUTSIDE_WORKING_HOURS · DOCTOR_HAS_NO_SCHEDULE · INVALID_TRANSITION | Validation failed; details says which field. |
| 429 | RATE_LIMITED | Too many requests; wait and retry. |
Something missing, or building something that needs more than this? Write to us — we answer.