Build with Praxium.

Composable by design

Plug your favourite tools into Praxium. REST APIs, a typed TypeScript SDK, and signed webhooks for healthcare workflows.

Get started in 5 minutes

Pick your path. Either route gets you a working API integration in the same session.

New to Praxium

Self-service trial

  1. Sign up

    Spin up a practice in under 5 minutes. No credit card.

  2. Sample data, ready to go

    Trial tenants come pre-loaded with example staff, clients, and appointments. A reseed action resets you to a clean baseline whenever you want.

  3. Create an API profile + key

    Admin → API profiles. Each profile gets its own HMAC key and decides which entity types and custom fields it exposes.

  4. Call the APIs

    Use @praxium/sdk or curl against https://{your-slug}.admin.praxium.nl.

Start free trial
Already a customer?

Bring your own admin

  1. Ask your admin

    An admin in your tenant can create an API profile and share its key — the profile decides which fields you can read.

  2. Call the APIs

    Use @praxium/sdk or curl with the key your admin shares.

REST APIs

  • Routing — tenant-scoped REST at /api/{tenant-slug}/...
  • Auth — HMAC API key in Authorization: Bearer, never in URLs
  • Permissions — every key bound to an API profile that defines which entity types and fields it can read
  • Languages: use Accept-Language: nl or en for localized text, or * for supported translations as objects (one call, all supported languages)
  • Discovery — OpenAPI-described, browsable in Scalar UI
  • Observability — every call logged with status, latency, profile, IP (90-day retention, admin-visible)
bash
curl -H "Authorization: Bearer $PRAXIUM_API_KEY" \  https://demo.admin.praxium.nl/api/demo/team
How auth works

API keys are HMAC-signed at creation: the key itself contains its own integrity proof in the format praxium_v1_<tenant>_<profile>_<timestamp>_<signature>, where the signature is HMAC-SHA256-derived server-side from a per-profile signing secret that lives encrypted at rest in the database — the secret never crosses the wire, only the signature it produces. The tenant slug is cryptographically bound into the signature, so a key issued for tenant A cannot be re-targeted at tenant B without breaking verification. On arrival, the server verifies the key's integrity end-to-end before any data is returned, and forged or re-targeted keys are rejected with 403.

Requests authenticate via standard Authorization: Bearer <key> over HTTPS — the same pattern used by GitHub, OpenAI, Slack, and Notion.

On top, each key is scoped to one API profile, which defines exactly which entity types and fields it can read. Each integration only interacts with the data it needs — the principle of least privilege, enforced at the field level rather than the endpoint level. Revocation or rotation is per-profile from the admin portal.

How multilingual content works

Use Accept-Language to choose the response shape.

1. Language-specific mode: send Accept-Language: nl or Accept-Language: en when your site is rendering one language. Localized text fields come back as ordinary strings. For example, FAQ category names, questions, and answers are returned as strings in the requested language.

2. Multilingual mode: send Accept-Language: * when you need all translations in one response. Fields that support this mode come back as translation objects such as { nl, en }. Pick the visible value with text[locale], the SDK's localizeText(text, locale) helper, or your own i18n layer.

Non-language fields: IDs, dates, booleans, prices, ordering keep the same shape in both modes.

Fallback is built in where Praxium resolves a field to a single string: if the requested translation is missing, the API returns the best available published text instead of an empty string. When you receive translation objects, you control the display fallback in your UI.

Open your interactive API reference at /api-docs

Replace {tenant} with your tenant slug.

Try it out

@praxium/sdk

npm install @praxium/sdk
  • Types — TypeScript client with autocomplete for every endpoint
  • Auth — HMAC-derived API keys signed automatically, no boilerplate
  • Languages: set locale to nl, en, or * depending on the translation shape you need
  • Runtime — Node.js 20+, Edge runtimes, any fetch-capable environment
ts
import { createPraxiumClient } from '@praxium/sdk'
const client = createPraxiumClient({  baseUrl: process.env.PRAXIUM_API_URL!,  apiKey: process.env.PRAXIUM_API_KEY!,  locale: 'nl',  // 'nl' | 'en' | '*' (see Multilingual Mode)})
const hours = await client.getOpeningHours()const team = await client.getTeamMembers()const faq = await client.getFaq()

Get started with the SDK: @praxium/sdk or jump to available methods

How the SDK authenticates

The SDK uses the same auth model as the REST API above — same praxium_v1_… keys, same server-side HMAC verification, same 403 on tampered or re-targeted keys. What the SDK adds on top: it derives the tenant slug from the key automatically (no separate config), attaches Authorization: Bearer <PRAXIUM_API_KEY> on every request, and gives you typed methods (await client.getTeamMembers(), await client.getOpeningHours(), …) so you don't write fetch boilerplate.

Key storage stays your responsibility: load it from a secrets manager or env var at runtime (PRAXIUM_API_KEY is the convention, but the name is yours), never commit it to source control, and rotate via the admin portal when staff turns over or the key may have been exposed. Generated keys are shown once at creation — only their SHA-256 hash is stored, so a lost key cannot be recovered (generate a new one and revoke the old).

How multilingual content works in the SDK

Set locale to choose the response shape. The SDK sends that value as Accept-Language on every request.

1. Language-specific mode: set locale: 'nl' or locale: 'en' when your app is rendering one language. Localized fields come back as plain strings. For example, getFaq() returns category names, questions, and answers as strings in the requested language.

2. Multilingual mode: set locale: '*' when you need all translations in one response. Supported fields come back as translation objects such as { nl, en }; render them with localizeText(text, locale) or your own i18n helper. For example, FAQ text fields are multilingual in this mode, and team custom fields use MultilingualTeamMember[].

localizeText() uses a developer-friendly fallback chain: requested locale, then English, then the first available value, then an empty string. That keeps render code small without exposing platform internals.

View on npm

Webhooks

Subscribe once. We sign and deliver every entity change.

  • Signatures — HMAC-SHA256 bound to the request timestamp, no shared secret on the wire
  • Secrets — per-webhook rotatable, each subscription has its own signing key
  • Observability — every delivery logged with status, latency, consecutive-failure counter (90-day retention, webhook detail page in admin)
http
POST /your-endpoint
X-Praxium-Signature: t=1742889600,sha256=4f3c8a7b9e2d1c5f6a0b8e7d9c2a5f3b1c8e9a3d2b6f4c7d8e1a3b9f6c2d4e7aContent-Type: application/json
{  "entity": "team"}

All webhook helpers and event types → @praxium/sdk webhooks reference

How signatures work (and how to verify them)

Each delivery includes a single header X-Praxium-Signature: t=<unix_ts>,sha256=<hmac_hex>, where the HMAC-SHA256 is computed server-side over ${timestamp}.${rawBody} using the per-webhook secret. The shared secret never crosses the wire — only the HMAC output does. The signature proves two things at once: the body wasn't tampered with in transit (integrity), and the call genuinely came from Praxium and not an attacker who guessed your endpoint URL (authenticity). All deliveries are dispatched over HTTPS — Praxium refuses to register webhook URLs that aren't HTTPS in deployed environments.

As a webhook recipient, you're responsible for verifying every delivery — Praxium signs and dispatches, but enforcement happens in your handler. If you're using @praxium/sdk, you don't write any of this by hand: processWebhook() (framework-agnostic) and createRevalidationHandler() (Next.js ISR) bake in all four steps plus replay protection. Hand-implementing in another runtime? The four steps are: (1) parse the timestamp and signature from the header, (2) reject deliveries older than your replay window — 5 minutes is the standard, (3) recompute the HMAC over ${timestamp}.${rawBody} with your shared secret, (4) compare with constant-time comparison (e.g. crypto.timingSafeEqual on Node).

This is the same scheme Stripe uses for webhook signatures. Per-webhook secrets are returned exactly once — in the response when you create the webhook and in each rotation response — and never re-surface afterwards. That single-exposure model means there's no long-lived attack surface for the secret on the platform side: even a compromised admin session can't pull it out again. Need a fresh one? Rotate from the admin portal — the new secret arrives in the rotation response, the previous one is invalidated immediately, and other subscriptions are untouched.

Integration patterns.

Pull data when your site needs it. React to changes the moment they happen.

Display Praxium data on your own site

Your site fetches staff, services, pricing, and FAQ from the SDK. A revalidation webhook tells the site to refresh the page the moment any of it changes in Praxium — no manual edits, no stale content.

Revalidation webhook trigger + SDK data pull

React to entity changes in your own tools

Your webhook endpoint receives the signed change event from Praxium. Forward it from there to Slack, your CRM, a data lake, or any pipeline you run — Praxium signs the source, you choose the destinations.

Outbound webhooks