Skip to content
CareSewa
For developers

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
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"
}'
201 · validated, stored, audited
response
{
"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"
}
}
Auto-CRUD

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.

The envelope

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.

successbooleanDid it work. Not a status code you have to interpret.
messagestringHuman-readable. Safe to log, never the thing you branch on.
dataT | T[] | nullThe payload. An object, an array, or null on failure.
metaobject?Present when there is more to say: pagination, field errors.
envelope
{
"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.

Auth

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.

POST /api/v1/auth/login
curl -X POST https://api.caresewa.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{ "email": "admin@yourhospital.example", "password": "..." }'
200 OK
{
"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.

The API, up close

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.

api.caresewa.com/api/v1
v1 · stable
Tenant token
{
"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.

Reference

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.

/auth

Sign in, refresh, sign out. Tenant and patient tokens are issued from separate families.

  • POST/auth/login
  • POST/auth/refresh
  • POST/auth/logout
/models

The definitions themselves. Builder roles only — this is Studio’s API.

  • GET/models
  • GET/models/:modelKey
  • POST/models
  • PATCH/models/:modelKey
/data/:modelKey

Auto-CRUD for every model you ever define. The route you will actually live in.

  • GET/data/:modelKey
  • POST/data/:modelKey
  • GET/data/:modelKey/:id
  • PATCH/data/:modelKey/:id
  • DELETE/data/:modelKey/:id
/portals

The ERPs your account is entitled to, and the one you are currently acting in.

  • GET/portals
  • GET/portals/:key
/pages

The navigation and page config the DynamicPage renderer reads.

  • GET/pages
  • GET/pages/:key
/account

The tenant itself — profile, public code, and the settings that scope everything else.

  • GET/account
  • PATCH/account
/account/staff

Staff and their grants. Changes here are read on the very next request, not the next login.

  • GET/account/staff
  • POST/account/staff
  • PATCH/account/staff/:id
  • DELETE/account/staff/:id
/bookings

Service requests from patients, arriving in a queue you confirm or complete.

  • GET/bookings
  • PATCH/bookings/:id
/community

Discussions. Clinician answers carry a professional flag and mark a thread answered.

  • GET/community/threads
  • POST/community/threads/:id/answers
/user

The authenticated identity behind the token, whichever family it came from.

  • GET/user/me
  • PATCH/user/me
/activity

The append-only audit log, read-only by construction. There is no POST here, ever.

  • GET/activity
/public/tenants/:tenantUniqueId

The one unauthenticated surface — how a patient finds you by your public code.

  • GET/public/tenants/:tenantUniqueId

Paths are shown relative to /api/v1. Everything except /public/tenants/:tenantUniqueId requires a bearer token, and every list route is paginated.

Pagination & filtering

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
curl
# Page 2, 50 per page, filtered on a field you defined
curl -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"
200 OK
{
"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
}
}
Validation

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.

400Malformed request. The JSON itself is wrong.
401No token, expired token, or wrong token family.
403Authenticated, but your grants do not cover this model or action.
404No such model, or no such record in your tenant.
422The payload failed your own definition. Fields named in meta.errors.
429Rate limited. Same envelope, back off and retry.
422 Unprocessable
{
"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.

Integration

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.

analyser bridge
# 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.

Contract

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
Underneath

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.

Start here

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.

01

POST /auth/login

Get an access token and a refresh token.

02

GET /models

See what your portal has. Or build one in Studio first.

03

GET /models/:modelKey

Read the field list you are about to write against.

04

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.

Model firstJSON lastRelations, not string ids
Questions

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