Patients API
Build integrations with Dentare's REST API - authentication, endpoints, and runnable curl examples for every operation.
What you'll build
Dentare exposes a REST API for managing patients programmatically. Use it to import a legacy patient list, sync patients from another system, or build custom integrations on top of your clinic data.
This guide takes you from zero to a working integration: generate a token, run your first authenticated request, then walk through the full CRUD lifecycle with copy-paste curl examples for every operation.
Before you start
You'll need:
- A Dentare account with admin access
- A terminal with
curlinstalled, or Postman - Basic familiarity with HTTP requests and JSON
Authentication
How it works
Every request to the API must include a Bearer token in the Authorization header. Tokens are scoped to your account - they can only read or modify data belonging to your clinic, not other Dentare accounts.
Tokens never expire automatically. You can revoke them at any time from the dashboard.
Generate an API token
Open the API Tokens page
Log in to Dentare as an account admin and go to API Tokens.
Create a token
Click Create an API Token, give it a descriptive name (e.g., "Patient sync script"), and click Create.
Copy the token immediately
The full token is displayed once. Copy it now and store it somewhere secure - you cannot view it again.
Make your first authenticated request
The simplest "ping" you can run is to fetch your own user profile from /api/v1/me.json. If your token works, you'll get a 200 response with your account info.
curl https://dentare.io/api/v1/me.json \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Expected response:
{
"id": 1,
"name": "Your Name",
"avatar_url": "https://dentare.io/users/1/avatar.png",
"sgid": "BAh7CEkiCGdpZAY6BkVUS..."
}A 200 with this payload confirms the token works. The id and name fields are the most useful for sanity checks. sgid is a signed global ID used internally and is safe to ignore.
Common authentication errors
401 Unauthorized means the token wasn't accepted. Three things to check:
- The
Authorizationheader uses the literalBearerprefix (note the trailing space) - The token was copied without leading or trailing whitespace
- The token hasn't been revoked from the dashboard
Revoke a token
If a token leaks, ends up in the wrong hands, or is no longer needed, revoke it from the dashboard. The token stops working immediately.
Open the API Tokens page
Log in to Dentare as an account admin and go to API Tokens.
Open the token
Click the name of the token you want to revoke.
Click Revoke
The token is deleted on the spot and cannot be re-enabled. Any script or integration using it will start receiving 401 Unauthorized on the next request.
Base URL and headers
| Base URL | https://dentare.io/api/v1 |
| Authentication | Authorization: Bearer YOUR_API_TOKEN |
| Content type (write requests) | Content-Type: application/json |
| Accept (recommended) | Accept: application/json |
Using Postman
Save yourself from pasting the token into every request:
- Create a new Postman environment named "Dentare"
- Add a variable:
baseUrl=https://dentare.io/api/v1 - Add a variable:
apiToken= your token (mark as secret type) - In every request, use
{{baseUrl}}for the URL andBearer {{apiToken}}for the Authorization header - Save the collection as "Dentare API" so your team can fork it
This pattern works whether you eventually publish a hosted collection or not.
List patients
GET /api/v1/patients
Returns a paginated list of patients in your account. Active patients only by default.
Basic example
curl https://dentare.io/api/v1/patients \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"GET {{baseUrl}}/patients
Authorization: Bearer {{apiToken}}
Accept: application/jsonResponse:
{
"data": [
{
"id": 4821,
"first_name": "Maria",
"last_name": "Hoxha",
"display_name": "Maria Hoxha",
"email": "[email protected]",
"phone": "+355691234567",
"phone_e164": "+355691234567",
"locale": "sq",
"is_active": true,
"created_at": "2026-04-25T14:32:00Z"
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 137
}
}Pagination
Pass page and per_page to walk through results. Default page size is 20, maximum is 100.
curl "https://dentare.io/api/v1/patients?page=2&per_page=50" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Search
The q parameter searches across name, email, phone, and personal number.
curl "https://dentare.io/api/v1/patients?q=hoxha" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Filtering
Pass any of these to narrow the result set. All are optional and combine with AND.
| Parameter | Matches |
|---|---|
email | Exact email match |
phone | Partial phone number match |
personal_no | National ID / EMBG |
external_id | ID from another system |
legacy_id | ID from a previous Dentare migration |
is_active | true or false (defaults to true only) |
curl "https://dentare.io/api/v1/[email protected]" \
-H "Authorization: Bearer YOUR_API_TOKEN"is_active=false to include archived patients.Supports ?lang=en|sq|mk to localize display_name in the response. See Common parameters.
Get a single patient
GET /api/v1/patients/:id
Returns the full record for one patient. Pass fields (comma-separated) to include extra data not returned by default.
Example
curl https://dentare.io/api/v1/patients/4821 \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response:
{
"data": {
"id": 4821,
"first_name": "Maria",
"last_name": "Hoxha",
"display_name": "Maria Hoxha",
"display_first_name": "Maria",
"display_last_name": "Hoxha",
"email": "[email protected]",
"phone": "+355691234567",
"phone_e164": "+355691234567",
"phone_country": "AL",
"birthdate": "1990-05-15",
"gender": "female",
"personal_no": null,
"external_id": null,
"legacy_id": null,
"locale": "sq",
"is_active": true,
"created_at": "2026-04-25T14:32:00Z",
"updated_at": "2026-04-25T14:32:00Z"
}
}With expansion fields
The default response excludes medical, emergency contact, insurance, and VIP fields to keep payloads small. Request them explicitly with fields:
curl "https://dentare.io/api/v1/patients/4821?fields=allergies,medical_conditions,insurance_provider" \
-H "Authorization: Bearer YOUR_API_TOKEN"Supports ?lang=en|sq|mk to localize display_name, display_first_name, and display_last_name. See Common parameters.
Patient not found
{
"error": "Not Found"
}A 404 response means the patient does not exist, or it belongs to a different account. The API never leaks data across accounts.
Create a patient
POST /api/v1/patients
Required fields
Only two:
first_name(string)last_name(string)
Everything else is optional.
Minimal example
curl -X POST https://dentare.io/api/v1/patients \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"first_name": "Maria",
"last_name": "Hoxha"
}'POST {{baseUrl}}/patients
Authorization: Bearer {{apiToken}}
Content-Type: application/json
Accept: application/json
Body (raw, JSON):
{
"first_name": "Maria",
"last_name": "Hoxha"
}Response (201 Created):
{
"data": {
"id": 4821,
"first_name": "Maria",
"last_name": "Hoxha",
"display_name": "Maria Hoxha",
"email": null,
"phone": null,
"phone_e164": null,
"is_active": true,
"created_at": "2026-04-25T14:32:00Z",
"updated_at": "2026-04-25T14:32:00Z"
}
}Full example
A more realistic create with contact, demographic, and locale fields:
curl -X POST https://dentare.io/api/v1/patients \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"first_name": "Maria",
"last_name": "Hoxha",
"email": "[email protected]",
"phone": "+355691234567",
"phone_country": "AL",
"birthdate": "1990-05-15",
"gender": "female",
"personal_no": "I90515123A",
"locale": "sq",
"external_id": "ext_2841"
}'All available fields
| Field | Type | Notes |
|---|---|---|
first_name | string | Required |
last_name | string | Required |
email | string | |
phone | string | Auto-normalized to E.164 |
phone_country | string | Two-letter code (MK, XK, AL) - helps phone normalization |
birthdate | string | ISO 8601 date: YYYY-MM-DD |
gender | string | male, female, or other |
personal_no | string | National ID / EMBG |
locale | string | en, sq, or mk - language used for patient-facing messages |
external_id | string | Your reference ID from another system |
blood_type | string | |
allergies | string | Free text |
medical_conditions | string | Free text |
emergency_contact_name | string | |
emergency_contact_relationship | string | |
emergency_contact_phone | string | |
insurance_provider | string | |
insurance_id | string | Insurance policy number |
is_active | boolean | Defaults to true |
preferred_doctor_id | integer | ID of the patient's preferred doctor |
Validation errors
If a required field is missing or a value is malformed, the API returns 422 Unprocessable Entity with a per-field breakdown:
{
"error": "Validation failed",
"errors": {
"first_name": ["can't be blank"],
"email": ["is invalid"]
}
}+355691234567). The normalized value is returned in phone_e164. Pass phone_country if your input numbers don't include a country code.Update a patient
PATCH /api/v1/patients/:id
PATCH is partial: send only the fields you want to change. Anything you omit stays as it is.
Example
curl -X PATCH https://dentare.io/api/v1/patients/4821 \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "[email protected]",
"phone": "+355692223344"
}'Response (200 OK):
{
"data": {
"id": 4821,
"first_name": "Maria",
"last_name": "Hoxha",
"email": "[email protected]",
"phone": "+355692223344",
"phone_e164": "+355692223344",
"updated_at": "2026-04-25T15:10:00Z"
}
}Why PATCH vs PUT
PATCH only updates the fields in the request body. To clear a field, send null explicitly. The Dentare API does not support PUT (full-replace) on patients - use PATCH for everything.
Archive a patient
The Patients API does not expose a DELETE endpoint. To remove a patient from active lists, archive them by setting is_active to false:
curl -X PATCH https://dentare.io/api/v1/patients/4821 \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_active": false}'Archived patients are excluded from list responses by default. Pass is_active=false to see them, or omit the filter to see only active patients.
Common parameters
These four patterns apply to every GET endpoint:
- Pagination -
?page=N&per_page=M(default 20, max 100). Collection endpoints only. - Filtering - any combination of
email,phone,personal_no,external_id,legacy_id,is_active, plus the search paramq. Collection endpoints only. - Expansion -
?fields=field1,field2,...to include fields excluded from the default response. - Localized responses -
?lang=en|sq|mkto control the language of computed fields likedisplay_name. Defaults to the patient's storedlocaleif set, otherwise English. Available on all read endpoints.
For the patient detail endpoint, available expansion fields are:
allergies, medical_conditions, blood_type, emergency_contact_name, emergency_contact_relationship, emergency_contact_phone, insurance_provider, insurance_id, vip, vip_start_date, vip_expiration_date, vip_notes, preferences, medical_issues
Error reference
HTTP status codes
| Code | Meaning | When you'll see it |
|---|---|---|
200 | OK | Successful read or update |
201 | Created | Successful create |
401 | Unauthorized | Missing, malformed, or revoked token |
404 | Not Found | Patient ID doesn't exist or belongs to another account |
422 | Unprocessable Entity | Validation failed - check the errors object |
Error response shape
{
"error": "Validation failed",
"errors": {
"first_name": ["can't be blank"],
"phone": ["is invalid"]
}
}error is always a human-readable string. errors (plural, lowercase) is present on 422 responses and maps each invalid field to an array of error messages.
Rate limits
The Patients API does not enforce per-token rate limits today. Be a good citizen: avoid tight loops, paginate large reads, and back off if you start seeing 429 responses (which would come from the platform edge, not the API itself).
Troubleshooting
401 Unauthorized on every request
Check that your Authorization header uses the correct format: Bearer YOUR_API_TOKEN. The literal word Bearer is required, with one space before the token. If the format is right, confirm the token has not been revoked from the dashboard.
404 Not Found when accessing a patient
The patient either does not exist or belongs to a different account. API requests are scoped to your account: you cannot read or modify patients from another clinic, even if you guess a valid ID.
422 Validation error on create
At minimum, first_name and last_name are required. The errors object in the response maps each invalid field to an array of messages. Read it field by field.
Phone number appears different after create or update
Dentare normalizes phone numbers to E.164 international format (e.g. +38970123456). The normalized value is returned in the phone_e164 field. Pass phone_country in the request body to help normalization when your input numbers do not include a country code.
Search returns no results
The q parameter searches across name, email, phone, and personal number. Make sure the search term matches at least one of those fields. Active patients only by default; pass is_active=false to include archived ones.
Special characters appear garbled
Ensure your request uses UTF-8 encoding. Set the Content-Type: application/json; charset=utf-8 header on write requests.
Postman
The variable-based pattern in the Postman section above is the recommended starting point: copy a single example, save it to a "Dentare API" collection, and reuse {{baseUrl}} and {{apiToken}} across every request.
What's next
- Import Patients from CSV - the no-code alternative for one-time migrations