Skip to the content
Language: English

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 referrals
  • bookings-read · bookings-readwrite — bookings, available times and services
  • calendars-read · calendars-readwrite — calendars, opening hours and pauses
  • journals-read · journals-readwrite — clinical notes — read, or write new drafts in
  • journal-files-read — files on the record, read only
  • invoices-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. name is 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 (/products works too)

Bookings

  • GET/bookingsbookings-readBookings in a window: start, end; filters client_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, with video_link if 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 as cancelled
  • 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; filters locked, draft
  • POST/clients/{id}/journalsjournals-readwriteNew note: content, date, draft. Without draft it is locked at once
  • GET/clients/{id}/journals/{journal}journals-readOne note
  • PUT/clients/{id}/journals/{journal}journals-readwriteEdit a draft — or lock it with locked: 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; filters client_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, optionally amount (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, not 4711. 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.