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 an organization 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, en, or ro; unsupported preferences resolve to the tenant default.
  • 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 localized content works

Send Accept-Language: nl, Accept-Language: en, or Accept-Language: ro when your site renders one language. Tenant APIs use the requested language only when that tenant has enabled it; otherwise they use the tenant default.

Every endpoint keeps one documented response shape. Fields documented as locale-resolved—such as FAQ category names, questions and answers, and team custom-field labels and values—come back as strings. Fields documented in OpenAPI as locale maps keep that stable shape.

For locale-resolved fields, a missing translation falls back to the best available published text instead of an empty string.

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 ro; each method keeps one TypeScript response type
  • 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' | 'ro'})
const location = client.location('amsterdam')const hours = await location.getOpeningHours()const team = await location.getTeamMembers()const faq = await location.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 location methods (await client.location('amsterdam').getTeamMembers(), await client.location('amsterdam').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 localized content works in the SDK

Set locale to nl, en, or ro. The SDK sends that value as Accept-Language on every request, and a language disabled for the tenant resolves to the tenant default.

Each method has one TypeScript response type. getFaq() returns locale-resolved category names, questions, and answers; team custom-field labels and values are resolved the same way. Fields generated as locale maps keep that documented shape and can be rendered with your application's i18n utilities.

View on npm

Webhooks

Subscribe to one resource type and one or more lifecycle actions, with an optional resource-specific condition—for example: resource type = service, lifecycle action = updated, condition = Location contains IJFysio.

  • CloudEvents — an occurrence ID for retry deduplication plus the changed resource type and ID in the subject and data
  • Durable delivery — timestamp-bound HMAC-SHA256 signatures, automatic retries, and 90-day delivery logs
http
POST /your-endpoint
X-Praxium-Signature: t=1784023200,sha256=<64-character-hex-digest>Content-Type: application/cloudevents+json
{  "specversion": "1.0",  "id": "019f60d2-3c47-7bb1-816f-458b53f520b5",  "type": "service.updated",  "source": "urn:praxium:tenant:019f5b62-21fa-7b40-88ee-9be2d71da2a1",  "subject": "service/019f5c7e-87f6-7449-9138-b0bc38d5bc65",  "time": "2026-07-14T12:00:00.000Z",  "data": {    "resource": {      "type": "service",      "id": "019f5c7e-87f6-7449-9138-b0bc38d5bc65"    }  }}

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, locations, and FAQ through the SDK. Subscribe to their explicit lifecycle events and invalidate the locale layout when a signed event arrives — no page list and no stale content.

Resource event + SDK data pull

React to entity changes in your own tools

Your webhook endpoint receives a signed CloudEvent with its occurrence ID and changed resource ID. Forward it to Slack, your CRM, a data lake, or any pipeline you run — Praxium signs and retries the source event, you choose the reaction.

Outbound webhooks