Every model you define gets an API. Free.
Not a feature you turn on. A consequence of how the engine works. The moment a ModelDefinition exists, auto-CRUD answers for it at /api/v1/data/:modelKey — with the same auth, validation and audit log as the UI, because it is the same code.
curl -X POST https://api.caresewa.com/api/v1/data/patient \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "full_name": "A. Rahman", "phone": "+00 000 000 0000", "blood_group": "O+", "primary_doctor": "665f1a2b9c4d3e0012a7b8c1" }'{ "success": true, "message": "Patient created", "data": { "id": "665f2c7e9c4d3e0012a7b910", "modelKey": "patient", "tenantId": "665e0011aa4d3e0012a7b001", "values": { "full_name": "A. Rahman", "phone": "+00 000 000 0000", "blood_group": "O+", "primary_doctor": "665f1a2b9c4d3e0012a7b8c1" }, "createdAt": "2026-07-16T09:41:22.118Z", "createdBy": "665e00f2aa4d3e0012a7b0a4" }}Nobody wrote the endpoint you are calling
There is no generated controller, no scaffolded route file, no registry of models that someone has to remember to update. One route resolves your definition at request time and behaves accordingly.
Live on save
Create a model in Studio and its endpoints answer immediately. The gap between authoring and integrating is zero, because there is nothing in between them.
One shape, every model
Patient, Vehicle, Antenatal visit, whatever you invented last Tuesday — identical route shape, identical envelope, identical semantics. Learn it once.
Same rules as the UI
Tenant filter, field validation, permission grants and the audit entry all apply. Calling the API is not a way around the engine. It is a way into it.
Four keys. Every response. No exceptions.
Consistency here is worth more than cleverness. You write the unwrapping once and it holds for every endpoint on the platform, including the ones for models that did not exist when you wrote it.
{ "success": true, "message": "Patients fetched", "data": [ /* ... */ ], "meta": { "page": 1, "limit": 20, "total": 1284, "totalPages": 65 }}The same four keys carry a 422 as carry a 200. success flips, data goes null, and the field errors ride in meta. Your error path is not a different parser.
Two token families that never meet
JWT access and refresh, as you would expect. The part worth reading twice is that a patient token and a tenant token are not the same object with a different claim.
curl -X POST https://api.caresewa.com/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "admin@yourhospital.example", "password": "..." }'{ "success": true, "message": "Signed in", "data": { "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs...", "user": { "id": "665e00f2aa4d3e0012a7b0a4", "role": "PORTAL_ADMIN", "tenantId": "665e0011aa4d3e0012a7b001" } }}Access + refresh
A short-lived access token on every request, a refresh token to rotate it. Standard, on purpose — there is nothing to learn here that you do not already know.
Patient tokens are rejected on tenant routes
Not scoped down to an empty result. Rejected. A patient token presented to /api/v1/data/patient does not return zero rows — it does not get to the handler. The two identity systems are separate by construction, because a patient account is platform-level and a staff account is tenant-level.
Grants are not in the token
Roles and per-model grants are read from the database on every request. That costs a lookup and buys instant revocation — pull someone’s access and their very next call fails, with no waiting for a token to expire.
Every model you define answers on the same shape
Click through the surface. Same envelope, same auth, same pagination — whether you are reading a model you invented this morning or a patient's aggregated records.
{"success": true,"data": [{"key": "patient","label": "Patient","labelPlural": "Patients","portal": "hospital-owner","isSystem": true,"allowExtraFields": false,"fields": [{ "key": "full_name", "label": "Full name","type": "text", "required": true },{ "key": "blood_group", "label": "Blood group","type": "select","options": [{ "label": "O+", "value": "O+" }] }]}],"meta": { "page": 1, "limit": 20, "total": 7 }}
This is the whole trick: your schema is data you can read back, not code you have to ask us to change.
Illustrative responses, not a live console — this page talks to nothing, so it needs no tenant and no key to be honest with you. Shapes, paths, envelope and status codes mirror the real API. Ids are made up.
Everything under /api/v1
Twelve groups. One of them — /data/:modelKey — expands to however many models you ever define, which is why the surface stays this small no matter how large your system gets.
Sign in, refresh, sign out. Tenant and patient tokens are issued from separate families.
- POST/auth/loginaccess + refresh token
- POST/auth/refreshrotate the access token
- POST/auth/logoutinvalidate the refresh token
The definitions themselves. Builder roles only — this is Studio’s API.
- GET/modelslist definitions for the portal
- GET/models/:modelKeyone definition, with fields
- POST/modelscreate a model
- PATCH/models/:modelKeyedit fields and rules
Auto-CRUD for every model you ever define. The route you will actually live in.
- GET/data/:modelKeylist, paginated and filtered
- POST/data/:modelKeycreate, validated
- GET/data/:modelKey/:idread one
- PATCH/data/:modelKey/:idpartial update
- DELETE/data/:modelKey/:idsoft delete on medical models
The ERPs your account is entitled to, and the one you are currently acting in.
- GET/portalsentitled portals
- GET/portals/:keyone portal and its models
The navigation and page config the DynamicPage renderer reads.
- GET/pagespages for the active portal
- GET/pages/:keyone page definition
The tenant itself — profile, public code, and the settings that scope everything else.
- GET/accountyour account
- PATCH/accountupdate the profile
Staff and their grants. Changes here are read on the very next request, not the next login.
- GET/account/stafflist staff
- POST/account/staffcreate staff with grants
- PATCH/account/staff/:idchange portals and CRUD grants
- DELETE/account/staff/:idrevoke access
Service requests from patients, arriving in a queue you confirm or complete.
- GET/bookingsinbound bookings
- PATCH/bookings/:idconfirm, reschedule, complete
Discussions. Clinician answers carry a professional flag and mark a thread answered.
- GET/community/threadslist threads
- POST/community/threads/:id/answerspost an answer
The authenticated identity behind the token, whichever family it came from.
- GET/user/meidentity, role and grants
- PATCH/user/meupdate your own profile
The append-only audit log, read-only by construction. There is no POST here, ever.
- GET/activityaudit entries, paginated
The one unauthenticated surface — how a patient finds you by your public code.
- GET/public/tenants/:tenantUniqueIdpublic profile and services
Paths are shown relative to /api/v1. Everything except /public/tenants/:tenantUniqueId requires a bearer token, and every list route is paginated.
Every list is paginated, whether you asked or not
There is no unbounded list endpoint on this platform. Not for records, not for staff, not for the audit log. A default of 20 and a hard cap of 100 apply everywhere.
This is not a limitation we apologise for. An endpoint that will happily return four hundred thousand rows is an endpoint that will one day be asked to, usually at the worst moment. Ask for more than 100 and you get 100.
- page — 1-indexed, defaults to 1
- limit — defaults to 20, capped at 100
- sort — a field key, prefix with - to reverse
- Filter on any field you defined, by its key
- meta carries page, limit, total and totalPages
# Page 2, 50 per page, filtered on a field you definedcurl -G https://api.caresewa.com/api/v1/data/patient \ -H "Authorization: Bearer $ACCESS_TOKEN" \ --data-urlencode "page=2" \ --data-urlencode "limit=50" \ --data-urlencode "blood_group=O+" \ --data-urlencode "sort=-createdAt"{ "success": true, "message": "Patients fetched", "data": [ { "id": "665f2c7e9c4d3e0012a7b910", "values": { "full_name": "A. Rahman" } }, { "id": "665f2c7e9c4d3e0012a7b911", "values": { "full_name": "S. Iyer" } } ], "meta": { "page": 2, "limit": 50, "total": 1284, "totalPages": 26 }}422, with the field that is wrong
Errors name the field. Always. A validation failure you have to reverse-engineer from a message string is a validation failure that becomes a support ticket.
Every rule in this response came from a ModelDefinition somebody set in Studio — required, unique, select options, relation target. Nobody wrote a validator for the patient model, and nobody will have to write one for whatever you build next.
{ "success": false, "message": "Validation failed", "data": null, "meta": { "errors": [ { "field": "full_name", "message": "full_name is required" }, { "field": "phone", "message": "phone is already in use" }, { "field": "blood_group", "message": "blood_group must be one of: A+, B+, O+, AB+" }, { "field": "primary_doctor", "message": "primary_doctor does not resolve to a doctor in this tenant" } ] }}Note the last one. A relation that points outside your tenant is a validation error, not an empty result — the engine will not let a record reference something it should not be able to see, and it tells you exactly why.
An analyser is just a client with opinions
The bridge you write for a haematology analyser, a PACS or an existing HIS uses the same endpoints your staff’s browser does. There is no ingest pipeline, no separate schema and no import format to negotiate.
# An analyser bridge writing a verified result.# Same URL shape, same envelope, same audit trail as the technician's form.curl -X POST https://api.caresewa.com/api/v1/data/result \ -H "Authorization: Bearer $SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "sample": "665f3a119c4d3e0012a7c221", "analyte": "haemoglobin", "value": 13.4, "unit": "g/dL", "verified_at": "2026-07-16T09:41:22.118Z" }'You define the target model
Whatever your machine emits, you model it in Studio first — the fields, the units, the verification timestamp. The bridge then writes into a shape you chose rather than one a vendor imposed.
The bridge is a first-class actor
Its writes land in the audit log with its identity attached. Six months later, "where did this result come from" is a query, not an investigation.
Reads work the same way
Poll a list endpoint with a filter and a sort. Outbound webhooks are not shipped yet — when they are, they will carry delivery guarantees rather than best-effort fire-and-forget.
The parts that decide whether you trust us in year three
Rate limits and versioning are boring until the day they are not. Here is the shape of both, stated plainly.
Rate limiting
Applied per account. The ceiling is set so that normal ERP traffic and a busy analyser bridge running all day never approach it — if you are hitting it during ordinary operation, that is a bug on our side and we want to hear about it.
- Exceeding a limit returns 429 in the standard envelope
- Back off and retry — the platform expects you to
- Bulk migrations get a raised ceiling, arranged in advance
- Limits scale with your plan rather than being per-endpoint trivia
Versioning
The platform contract is versioned in the path. Your models are not versioned by us at all — they are your data, changed when you decide, without our involvement.
/api/v1/data/:modelKey
- A breaking change to the contract means a new path segment
- Adding a field in Studio is additive and never breaks a client
- New keys in a response are additive — parse defensively
- Making an existing field required is the one change worth coordinating
What it is actually built on
No mystery, no proprietary runtime. Boring technology, applied to a non-boring idea.
Node
runtime
Express
HTTP layer
MongoDB
documents
TypeScript
end to end
Expo
mobile + OTA
JWT
access + refresh
A document database is not incidental here. When your schema is data, the store that holds your records should not have opinions about what shape they are — which is precisely what DynamicRecord depends on.
Four calls to a working integration
If you have integrated against any REST API in the last decade, you already know how this goes. That is the intent.
POST /auth/login
Get an access token and a refresh token.
GET /models
See what your portal has. Or build one in Studio first.
GET /models/:modelKey
Read the field list you are about to write against.
POST /data/:modelKey
Write. Get a 201 or a 422 that names the field.
Before you write a line
Model the thing first. The most common mistake on this platform is treating a model like a legacy schema you have to work around — bolting values into a JSON blob because that is what the old system forced.
Give the fields real types. You get validation, filtering, relations and a form for free, and none of that reaches inside a JSON blob.
What you will want to know by the second hour
The questions that come up in every integration call, answered before the call.
No. The endpoint is not a thing that gets created — it is one route, /api/v1/data/:modelKey, that resolves your ModelDefinition at request time. The moment the model exists, the route answers for it. There is nothing to deploy, register or enable.
Bring the system you thought you were stuck with
Analyser, PACS, an HIS nobody wants to touch. If it can write to a URL, it can write to CareSewa.
Multi-country by design · tenant-isolated · every change audit-logged