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
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 underclinikehr._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_.403 with the code scope_missing. See
Permissions.
Writing safely: idempotency is automatic
Every write function takes anidempotency_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.
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:
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 withhas_more and next_cursor. paginate walks every page for you; you give
it a function that fetches the page for a cursor:
has_more is false. See
Pagination.
Using it from async code
In version 0.1.0 use the synchronoussync_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 sendsAccept: application/fhir+json:
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 areattrs 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():
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
- Build and test against your sandbox with a test key.
- 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.
- Create a live key and swap it in. Nothing else in your code changes.
Versions
The client follows semantic versioning. While it is0.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.