npm package: @praxium/sdk
Description: Official TypeScript SDK for the Praxium platform API
Latest version: 0.3.77

# @praxium/sdk

Official TypeScript SDK for the [Praxium](https://praxium.nl) platform API. Build tenant websites that display practice data (team, services, FAQ, opening hours) with full locale support.

- [Quick Start](#quick-start) — Installation and usage examples
- [Configuration](#configuration) — Environment variables and tenant routing
- [Available Methods](#available-methods) — All API endpoints
- [Error Handling](#error-handling) — Typed error classes
- [Webhooks](#webhooks) — React to entity changes (ISR revalidation, cache busting, etc.)
- [Contributing](#contributing) — Development commands

## Quick Start

```bash
npm install @praxium/sdk
```

### Locale-Specific Mode

Pass a locale to get pre-resolved labels and values in that language:

```typescript
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()
```

All methods return data directly or throw a typed `PraxiumError` on failure (see [Error Handling](#error-handling)).

### Multilingual Mode

Pass `locale: '*'` to receive all translations as objects. Resolve them at render time with `localizeText()`:

```typescript
import { createPraxiumClient, localizeText } from '@praxium/sdk'

const client = createPraxiumClient({
  baseUrl: process.env.PRAXIUM_API_URL!,
  apiKey: process.env.PRAXIUM_API_KEY!,
  locale: '*',
})

const team = await client.getTeamMembers()
// Returns MultilingualTeamMember[] — labels and values are multilingual objects:
// {
//   name: "Dr. Jan de Vries",
//   role: "Fysiotherapeut",
//   customFields: [
//     { label: { nl: "Specialisatie", en: "Specialization" }, value: { nl: "Sport", en: "Sports" } }
//   ]
// }

const label = localizeText(team[0].customFields[0].label, 'nl')  // → "Specialisatie"
const value = localizeText(team[0].customFields[0].value, 'nl')  // → "Sport"
```

### Contact Form

Submit a contact form on behalf of a website visitor:

```typescript
const result = await client.submitContactForm({
  name: 'Jan de Vries',
  email: 'jan@example.nl',
  phone: '+31612345678',
  subject: 'Appointment request',
  message: 'I would like to book an appointment.',
})
// → { success: true, emailStatus: 'sent' }
```

## Configuration

Your website needs two environment variables to connect to the Praxium platform, plus an optional third for webhook-based cache revalidation:

**Required:**

| Variable | Purpose | Example |
|----------|---------|---------|
| `PRAXIUM_API_URL` | Your tenant's admin portal URL. The SDK sends API requests to this host. | `https://mypractice.admin.praxium.nl` |
| `PRAXIUM_API_KEY` | HMAC API key for authentication. Generated in the admin portal under API Profiles. The tenant slug is embedded in the key — no need to configure it separately. | `hmac_v1_mypractice_default_17..._abc...` |

**Optional (only if using ISR revalidation webhooks):**

| Variable | Purpose | Example |
|----------|---------|---------|
| `PRAXIUM_WEBHOOK_SECRET` | Shared secret for webhook signature verification. Only needed if you want the platform to notify your site when data changes (team, FAQ, opening hours). See [Webhooks](#webhooks). | (32+ character random string) |

```bash
# .env.local
PRAXIUM_API_URL="https://mypractice.admin.praxium.nl"
PRAXIUM_API_KEY="hmac_v1_mypractice_default_1234567890_abcdef..."
PRAXIUM_WEBHOOK_SECRET="your-webhook-secret-from-admin-portal"  # optional
```

> **How tenant routing works:** Each tenant has its own admin subdomain (`{slug}.admin.praxium.nl`). The platform identifies your tenant from both the hostname AND the API key's embedded slug, cross-validating them for security. You don't need to configure the tenant slug separately — it's derived from your API key automatically.

## Available Methods

| Method | Description |
|--------|-------------|
| `getOpeningHours()` | Weekly opening schedule |
| `getTeamMembers()` | Staff members with photos and custom fields |
| `getContactDetails()` | Practice contact information |
| `getLocation()` | Address and map coordinates |
| `getBusinessName()` | Practice display name |
| `getSocialLinks()` | Social media URLs |
| `getFaq()` | FAQ grouped by category |
| `getServiceVariants()` | Service pricing and variants |
| `getInsuranceInfo()` | Accepted insurance providers |
| `getFeatures()` | Practice features and amenities |
| `getPaymentMethods()` | Accepted payment methods |
| `getPolicyInfo()` | Practice policies |
| `getBookableServices()` | Services available for online booking |
| `submitContactForm(body)` | Submit a contact form |

## Error Handling

All methods throw typed errors that you can catch individually:

```typescript
import { PraxiumNotFoundError, PraxiumAuthError } from '@praxium/sdk'

try {
  const team = await client.getTeamMembers()
} catch (error) {
  if (error instanceof PraxiumNotFoundError) {
    // 404 — resource not found
  } else if (error instanceof PraxiumAuthError) {
    // 401 — invalid or expired API key
  }
}
```

| Error Class | HTTP Status | When |
|-------------|-------------|------|
| `PraxiumAuthError` | 401 | Invalid API key |
| `PraxiumForbiddenError` | 403 | Key doesn't match tenant |
| `PraxiumNotFoundError` | 404 | Resource not found |
| `PraxiumValidationError` | 400 | Invalid request data |
| `PraxiumRateLimitError` | 429 | Too many requests |
| `PraxiumError` | Other | Base class for all errors |

## Webhooks

When an admin updates data (team, FAQ, opening hours), the platform sends an HMAC-signed webhook to your registered endpoint. You can use this to react to entity changes in any way your application needs.

**Common use cases:**

| Use Case | What you do on entity change |
|----------|------------------------------|
| **ISR revalidation** | Call `revalidatePath()` to refresh cached pages |
| **Search index** | Re-index changed entities in your search engine |
| **CDN cache** | Purge cached API responses or assets |

**Prerequisites:**

1. Register your webhook endpoint URL in the admin portal (Settings → Webhooks)
2. Set `PRAXIUM_WEBHOOK_SECRET` to the shared secret from the admin portal

All webhook handlers include HMAC-SHA256 signature verification, timestamp-based replay protection (5-minute window), and timing-safe comparison.

### Next.js ISR Revalidation

The SDK provides a ready-made handler for Next.js [ISR](https://nextjs.org/docs/app/building-your-application/data-fetching/incremental-static-regeneration) revalidation. It verifies the webhook signature and calls `revalidatePath()` for the affected pages.

The `pathMap` is fully customizable — you define which pages to revalidate for each entity based on your website's routing structure:

```typescript
// app/api/revalidate/route.ts
import { createRevalidationHandler } from '@praxium/sdk/webhooks'

export const POST = createRevalidationHandler({
  secret: process.env.PRAXIUM_WEBHOOK_SECRET!,
  // Map platform entities to YOUR website's page paths.
  // Adjust the paths to match your project's routing structure.
  pathMap: {
    'team': ['/nl/team', '/en/team'],
    'rates': ['/nl/tarieven', '/en/rates'],
    'faq': ['/nl/faq', '/en/faq'],
    'opening-hours': ['/nl', '/en'],    // homepage shows opening hours
    'all': ['/nl', '/en'],              // full-site revalidation
  },
})
```

Requires `next` as a peer dependency.

### Custom Webhook Handler

For non-Next.js use cases or custom logic, use `processWebhook()` to verify the signature and extract the changed entity:

```typescript
// Example: invalidate a Redis cache on entity change
import { processWebhook, WEBHOOK_SIGNATURE_HEADER } from '@praxium/sdk/webhooks'

export async function POST(request: Request) {
  const body = await request.text()

  const result = await processWebhook({
    body,
    signature: request.headers.get(WEBHOOK_SIGNATURE_HEADER)!,
    secret: process.env.PRAXIUM_WEBHOOK_SECRET!,
  })

  if (!result.valid) {
    return new Response(result.error, { status: 401 })
  }

  // result.entity = 'team' | 'faq' | 'rates' | ...
  await redis.del(`cache:${result.entity}`)

  return new Response('OK')
}
```

## Contributing

```bash
npm run generate   # Regenerate client from OpenAPI spec
npm run build      # Build dist/
npm run test       # Run tests
npm run typecheck  # Type-check without emitting
```

## License

[MIT](LICENSE)