The open API
The same API as Terapeutbooking — at your own address
An accounting package, a website or any other system that already talks to Terapeutbooking/EasyPractice only has to change the address and the key. The routes, the fields and the errors are the same.
Getting started
One call and you are in
The address is the clinic’s own: https://clinic.anotera.clinic/api/v1 — or your own domain, if the clinic runs on one. The key goes in as a Bearer token. JSON in, JSON out, UTF-8, times in ISO 8601 with the clinic’s time zone.
curl https://klinik.anotera.clinic/api/v1/clients?active \ -H "Authorization: Bearer an_…" \ -H "Accept: application/json"
Lists come paginated as they do there: data, links and meta, with page and page_size (at most 100) in the query.
{
"data": [
{ "id": "k-3f9a1c2b7d10", "name": "Anna Berg",
"email": "anna@eksempel.dk", "phone": "+45 20 00 00 00",
"address": "Gammelvej 1", "zip": "2000", "city": "Frederiksberg",
"status": "active", "tags": [],
"created_at": "2026-09-01T10:12:00+02:00" }
],
"links": { "first": "…?page=1", "last": "…?page=1", "prev": null, "next": null },
"meta": { "current_page": 1, "per_page": 15, "total": 1 }
}
There is room for 60 calls a minute per key. The response carries X-RateLimit-Limit and X-RateLimit-Remaining; beyond the limit you get 429 with Retry-After.
The API here is for programs that need to talk to Anotera. If it is the clinic’s own clients, appointments and notes that have to come across from the old system, that is a different page: Move data in from Terapeutbooking.
The key
Personal, with exactly the rights you give it
The key is made in the workbench under More → Integrations → The open API. It is shown once and kept only as a fingerprint on our side. It can do what the clinician can — and only within the areas that were ticked:
clients-read·clients-readwrite— client records, tags and referralsbookings-read·bookings-readwrite— bookings, available times and servicescalendars-read·calendars-readwrite— calendars, opening hours and pausesjournals-read·journals-readwrite— clinical notes — read, or write new drafts injournal-files-read— files on the record, read onlyinvoices-read·invoices-readwrite— invoices, mark as paid, send, fetch PDF
A call outside the rights gets a 403 naming the missing scope. A key can be revoked on the same screen; it stops working immediately.
The shape
The fields have the same names
A booking is created as it is there: start, end, client_id, products, optionally calendar_id, heading, notes, status and type (client_booking, blocked, other).
POST /api/v1/bookings
{
"start": "2026-10-02T10:00:00+02:00",
"end": "2026-10-02T10:50:00+02:00",
"client_id": "k-3f9a1c2b7d10",
"products": [{ "id": "individuel" }],
"notes": "Første samtale"
}
A problem with what was sent is answered with 422, naming the field and the reason — in the clinic’s language:
HTTP 422
{
"error": { "message": "The given data was invalid.",
"status_code": 422 },
"data": { "start": ["Tiden ligger uden for åbningstiden"] }
}
Booking status: pending, confirmed, finished, no_show, cancelled. Something that does not exist is a 404; something that cannot be done (a locked note, a client with a record) is a 409.
The routes
All routes
The third column is the scope the key needs. All paths live under /api/v1.
The key
- GET
/me—Who the key is, and what it may do
Clients
- GET
/clientsclients-readThe list. Filters:active,inactive,has_email,has_phone,email,cpr,tag_id,search,order_by_created|updated|email - POST
/clientsclients-readwriteCreate.nameis required;email,phone,address,zip,city,cpr,date_of_birth,notes,tags,ean - GET
/clients/{id}clients-readOne client record - PUT
/clients/{id}clients-readwriteUpdate. Only the fields sent are changed - DELETE
/clients/{id}clients-readwriteDelete — only a client without a record; otherwise 409 - GET
/client-tagsclients-readThe clinic’s tags - GET
/client-vouchersclients-readReferrals and courses with sessions left - GET
/client-custom-attributesclients-readCustom attributes (empty — we have none)
Calendar
- GET
/calendarscalendars-readThe clinic’s clinicians as calendars - GET
/calendars/{id}calendars-readOne calendar - GET
/calendars/{id}/opening-timescalendars-readThe week:monday_start,monday_end,monday_closed… and the pause after each session - PUT
/calendars/{id}/opening-timescalendars-readwriteChange the week — your own calendar only - GET
/calendars/{id}/pausescalendars-readClosures: holiday, course, a day off - POST
/calendars/{id}/pausescalendars-readwriteNew closure:date,start_time,end_time,reason - DELETE
/calendars/{id}/pauses/{pause}calendars-readwriteRemove a closure - GET
/servicesbookings-readServices with duration and price (/productsworks too)
Bookings
- GET
/bookingsbookings-readBookings in a window:start,end; filtersclient_id,calendar_id,product_id,status - POST
/bookingsbookings-readwriteNew booking — see the shape above - POST
/bookings/bulkbookings-readwriteSeveral at once; each gets its own answer - GET
/bookings/{id}bookings-readOne booking, withvideo_linkif it is a video session - PUT
/bookings/{id}bookings-readwriteMove (start/end), change status, edit heading and notes - DELETE
/bookings/{id}bookings-readwriteCancel — the booking stays ascancelled - GET
/online-booking/available-datesbookings-readDays with free time:product_id,from,to - GET
/online-booking/available-timesbookings-readFree times on one day:product_id,date
Records
- GET
/clients/{id}/journalsjournals-readNotes on the client; filterslocked,draft - POST
/clients/{id}/journalsjournals-readwriteNew note:content,date,draft. Withoutdraftit is locked at once - GET
/clients/{id}/journals/{journal}journals-readOne note - PUT
/clients/{id}/journals/{journal}journals-readwriteEdit a draft — or lock it withlocked: true. A locked note cannot be edited: 409 - DELETE
/clients/{id}/journals/{journal}journals-readwriteDelete a draft. A locked note cannot be deleted: 409 - GET
/clients/{id}/journal-filesjournal-files-readFiles on the record - GET
/clients/{id}/journal-files/{file}journal-files-readOne file: name, type, size - GET
/clients/{id}/journal-files/{file}/downloadjournal-files-readThe file itself
Invoices
- GET
/invoicesinvoices-readInvoices; filtersclient_id,paid,unpaid,number,include_credit_notes - GET
/invoices/{id}invoices-readOne invoice with lines and totals - POST
/invoices/{id}/mark-as-paidinvoices-readwriteMark as paid:date,payment_type, optionallyamount(otherwise the balance) - POST
/invoices/{id}/sendinvoices-readwriteEmail it to the recipient:subject,text - GET
/invoices/{id}/pdfinvoices-readThe invoice as PDF
The differences
What is different — on purpose
- Ids are strings, not integers.
k-3f9a1c2b7d10, not4711. A system that assumes integers needs a string. - The record is encrypted with the clinician’s key. A note written in from outside sits in the record like any other — and a locked note can neither be edited nor deleted, whoever asks. That is the Danish record-keeping order, not a technical limit.
- There is no EasyPay.
/easypay/*answers 404 with an explanation. Payments go through the invoices. - Calendars are not created through the API. A calendar is a clinician, and clinicians come in through the workbench.
- A booking is not deleted — it is cancelled. It stays as
cancelled, so the numbers and the record still add up afterwards.
Missing a route you use with them? Write to kontakt@anoteraclinic.com with the call, and we will look at it.