Skip to main content

aiofarmad

Check PyPI version Python versions License: MIT

Unofficial asynchronous Python library to interact with the Mijn Farmad Apotheek API. Requires Python >= 3.14.

This is a reverse-engineered client and the Farmad API may change or withdraw access without notice. It is not affiliated with or endorsed by Farmad.

Install

uv add aiofarmad

Login

import asyncio

from aiofarmad import FarmadClient


async def main() -> None:
    async with FarmadClient(
        email="you@example.com",
        password="your-password",
    ) as client:
        await client.async_login()
        account = await client.async_get_account()
        print(account.full_name, account.entitled_pharmacies)


asyncio.run(main())

The login scripts the same authorization-code flow the web app uses: PKCE against the hosted login form on signin.procura.farmad.be, then a token exchange for an access token and a rotating refresh token. Accounts with multi-factor authentication pass an otp provider, which the login awaits only when the hosted form demands a one-time code.

async def read_code() -> str:
    return input("code: ")


async with FarmadClient(
    email="you@example.com",
    password="your-password",
    otp_provider=read_code,
) as client:
    await client.async_login()

The wait for the code does not count against the request timeout. A rejected code raises FarmadAuthenticationError, and a login that needs a code but has no provider raises FarmadMfaRequiredError.

Token persistence

Log in once, store the pair that async_login returns, and pass it back on the next start. The client refreshes before the access token expires and calls on_token_refresh whenever Farmad rotates the pair.

from aiofarmad import FarmadClient


async def run(stored_access: str, stored_refresh: str) -> None:
    async def save_tokens(access_token: str, refresh_token: str | None) -> None:
        await write_tokens_somewhere(access_token, refresh_token)

    async with FarmadClient(
        access_token=stored_access,
        refresh_token=stored_refresh,
        on_token_refresh=save_tokens,
    ) as client:
        scheme = await client.async_get_medication_day_scheme(
            "343602",
            from_=start_of_today,
            until=end_of_today,
        )

Pharmacists and permissions

Every pharmacy-scoped call takes an apb number, the identifier of one pharmacy. An account only has roles at pharmacies it has been linked to through the app's self-onboarding, which the library exposes as well:

linked = await client.async_link_pharmacy("343602")

Calls against a pharmacy the account has no role at raise FarmadAuthorizationError.

Reading data

Method Returns
async_get_account() The account with its pharmacy memberships
async_get_patient() The patient record per pharmacy
async_get_organization(apb) Public pharmacy information
async_get_pharmacy_preferences(apb) What the pharmacy allows for orders
async_get_medication_day_scheme(apb, from_=..., until=...) Every intake per day in the window
async_get_medication_nondaily_products(apb, day=...) Medications taken outside the daily scheme
async_get_conversations(apb) Conversations with the pharmacy
async_get_conversation_messages(apb, customer_account_id) Messages in one conversation
async_search_products_in_apb(apb, query) The products at one pharmacy matching a search term
async_get_baskets(apb) Submitted orders
async_get_draft_basket(apb) The current draft basket, or None

Ordering

Write calls build a draft, submit it, and can cancel a submitted order before the pharmacy processes it:

from aiofarmad import DraftProduct, FarmadClient

products = (DraftProduct(product_cnk="1234567", quantity=1),)

async with FarmadClient(access_token=..., refresh_token=...) as client:
    draft_id = await client.async_save_draft_basket("343602", products=products)
    await client.async_submit_basket(
        "343602",
        draft_id,
        products=products,
        unit_prices=(("1234567", 4.95),),
    )

Orders are paid at pickup by default. Most pharmacies do not allow online payments, and asking for one there answers 400. Cancelling a submitted order is the pharmacy's decision: customer accounts regularly get FarmadAuthorizationError from async_cancel_basket.

Prescriptions

Prescriptions sit behind the Belgian eHealth platform, and Farmad gates the eHealth session to the browser that completed the itsme consent. Tested against the platform: replaying a live consent session from any non-browser client, with the exact cookies, the exact tokens, and a browser TLS fingerprint, answers 401 every time. No cookie transfer, token pairing, or fingerprint trick carries the session out of the browser.

The library models this honestly:

  • async_get_prescriptions and async_get_prescription carry the verified wire paths.
  • Every eHealth call from a non-browser client raises FarmadEhealthAuthorizationRequiredError, without burning a refresh token cycle.
  • ehealth_cookie exists to pass a consent session through, so the library works the day Farmad relaxes the gate. It currently does not open the prescriptions.

Until Farmad changes the platform, prescriptions work in the web app only.

Errors

Exception Meaning
FarmadAuthenticationError Credentials or tokens were rejected
FarmadMfaRequiredError The login needed a one-time code and no otp provider was passed
FarmadAuthorizationError The account has no role at this pharmacy
FarmadEhealthAuthorizationRequiredError The eHealth consent is missing
FarmadCommunicationError The API is unreachable or answered with a failure
FarmadTimeoutError A request exceeded the configured timeout
FarmadInvalidResponseError A response payload was unusable
FarmadNotFoundError The requested object does not exist
FarmadClientClosedError The client was closed

Development

uv sync
uv run python -m scripts.check

License

MIT. See LICENSE.

Metadata

Release files for aiofarmad 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aiofarmad 0.4.0
File Size Uploaded
aiofarmad-0.4.0.tar.gz 24.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aiofarmad 0.4.0
File Interpreter ABI Platform
aiofarmad-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 54.1 kB

Release files / aiofarmad-0.4.0.tar.gz

Download URL aiofarmad-0.4.0.tar.gz
Size 24.5 kB
Tags Source
SHA-256 checksum
How to use checksums
c84ada52a32b8861965c0b3a60e1b686aa723155d66e37646748ca381f7cedd1
BLAKE2b-256 checksum
How to use checksums
c424db95fbb795d20ce45b4056fdbe0c2185d249f3f726d0413df5fa4b1b2273
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / aiofarmad-0.4.0-py3-none-any.whl

Download URL aiofarmad-0.4.0-py3-none-any.whl
Size 29.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d99623e6f860c374fb96a997d47ccded8b78a703f2d58f2b7eb9c8d5ab4e4dff
BLAKE2b-256 checksum
How to use checksums
d2767edd363458303a4a02f0066e1f768c5615b51acc6578638c2de644dbd21c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.0

2 release files

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page