Skip to main content
clinikehr is the official Python client for the ClinikEHR API. It is a thin layer over the same HTTPS endpoints this reference documents, and it takes care of the parts that are easy to get wrong: sending your key, retrying safely, never creating a record twice, walking paged lists, typed errors and checking webhook signatures.

Install

The package contains a typed function for every route in this reference, and a typed model for every request and response body. Each version is published from the public source by GitHub Actions using PyPI trusted publishing, so PyPI shows a signed attestation for each file on that file’s page.

Your first call

In the developer portal, switch to Test mode, open Keys and select Create key. A test key reaches only your workspace’s sandbox — invented clinics, patients and stock — so you can try everything safely. Put it in your server’s environment, never in source code:
GET /v1/me tells you which key you are using, its environment and its permissions. With a test key, me.clinic_id is your sandbox clinic — use it in the calls below.

Calling a route

Every route is a module under clinikehr._generated.api.<group>, named after the route’s operationId in snake case: listInventoryItems is clinikehr._generated.api.inventory.list_inventory_items, and your editor lists the others as you type. Call its sync_detailed function with the client, the path and query parameters as keyword arguments, and a request model as body for a write. It returns a response, and clinikehr.raise_for_response hands you the parsed body or raises the API’s error.
The response comes back as a typed object, so your editor completes page.data and item.name. Parameters that accept a fixed set of values, such as status, take the enum from clinikehr._generated.models; passing a plain string raises an error before any request is sent. A parameter named from, which Python reserves, is spelled from_.
A route your key’s permissions do not cover answers 403 with the code scope_missing. See Permissions.

Writing safely: idempotency is automatic

Every write function takes an idempotency_key. Pass a fresh uuid4 for each new record. The client sends it as the Idempotency-Key header and sends the same key again if it retries that request, so a timeout after the server has already saved your record can never create a second one — the retry returns the first answer. See Retries and duplicates.
If your own job may repeat a logical operation (for example, a queue that redelivers a message), derive the key from something stable instead, such as idempotency_key=f"stock-item:{message.id}", so both attempts are recognised as one. When you call the underlying httpx client yourself (see FHIR R4), the client still adds an Idempotency-Key to any POST or PATCH that has none, because some PATCH routes, such as updating an appointment, require one. A DELETE carries a key only when you set one.

Retries

The client retries a request that failed for a temporary reason, up to 4 attempts in total: Waits grow exponentially from 500 ms up to 8 s, with random jitter, and a Retry-After header from the API always wins. Change the policy, and set a timeout, when you create the client:
See Rate limits for the limits themselves.

Errors

raise_for_response raises ClinikEHRApiError for any refused request, carrying the API’s error fields:
A network failure — DNS, a refused connection, a timeout — is not a ClinikEHRApiError: no answer came back. It surfaces as the httpx exception (httpx.ConnectError, httpx.TimeoutException, and so on, all subclasses of httpx.HTTPError) once the retries are used up; the request may or may not have reached us. For a POST, retrying with the same idempotency_key is safe.

Paging through a list

List routes return one page at a time with has_more and next_cursor. paginate walks every page for you; you give it a function that fetches the page for a cursor:
It asks for the next page only when you have used up the current one, and stops when has_more is false. See Pagination.

Using it from async code

In version 0.1.0 use the synchronous sync_detailed calls. The asyncio_detailed variants of the generated functions are not usable yet: they fail before sending anything. In an async program, run the synchronous call in a worker thread so it does not block your event loop:

FHIR R4

To receive FHIR resources instead of JSON, turn FHIR on for the client. Every request then sends Accept: application/fhir+json:
Read a FHIR answer from the httpx response as above. The typed functions and models describe the JSON shape, so calling one of them on a FHIR client fails when it tries to read a FHIR resource. The client’s retry and idempotency behaviour apply to this httpx client too. See FHIR R4 for which routes answer in FHIR and how.

Verifying a webhook

verify_webhook checks the signature on an event we deliver to your endpoint. Pass the raw request body as text — not a re-serialised object — and your endpoint’s signing secret. It returns nothing when the delivery is genuine and raises WebhookVerificationError when it is not:
headers is any mapping that holds the webhook-id, webhook-timestamp and webhook-signature headers, such as your framework’s request headers. In Flask, call handle_webhook(request.headers, request.get_data(as_text=True)); in FastAPI, (await request.body()).decode(). While you rotate a signing secret, pass both — secrets=[current, previous] — and either one is accepted. An event older than 5 minutes is refused (tolerance_seconds changes that). See Verifying a webhook’s signature and Events.

Typed requests and responses

Request and response bodies are attrs models in clinikehr._generated.models. Build a request from keyword arguments as above, or from a dictionary with Model.from_dict(...); turn any model back into plain data with .to_dict():
The client is also a context manager, which closes its connections when the block ends:

Keep your key on the server

Unlike the TypeScript client, the Python client cannot tell where it runs, so the discipline is yours: read the key from your server’s environment or a secret store, and never ship a live key in code that runs on an end user’s device. A live key reaches real patient information. Call the API from your server and give your front end only what it needs.

From test to live

  1. Build and test against your sandbox with a test key.
  2. To reach a real clinic, the clinic connects your organization and grants the permissions you need — see Connections. Anything about a patient also needs patient-data access.
  3. Create a live key and swap it in. Nothing else in your code changes.

Versions

The client follows semantic versioning. While it is 0.x, a minor version may change its interface; pin a version range you have tested (pip install "clinikehr>=0.1,<0.2"). Releases are listed in the changelog and on PyPI. Found a problem? Open an issue on GitHub.