> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phosra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> Official TypeScript/JavaScript SDK for the Phosra API

The `@phosra/sdk` package provides a typed client for the Phosra API, with support for Node.js and browser environments.

<Info>
  **Verified against the published package.** Everything on this page was checked against **`@phosra/sdk@0.1.0`** (the current npm release) and the code was run against the live sandbox — see [Verified live](#verified-live) at the bottom.
</Info>

## Installation

```bash theme={null}
npm install @phosra/sdk
```

Latest published version: **`0.1.0`**. Pin it in `package.json` for reproducible builds:

```json theme={null}
{ "dependencies": { "@phosra/sdk": "^0.1.0" } }
```

## Quick Start

Every tab below **leads with the open sandbox** — `https://phosra-api-sandbox-production.up.railway.app` — so your **first copy-paste returns `201` with no API key**. Paste any tab as-is and run it; nothing you create in the sandbox touches production. The [Get a production key](#going-to-production) step (a real dashboard link) comes after you've seen it work.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { PhosraClient } from '@phosra/sdk';

  // Sandbox-first: no API key required. Point baseUrl at the open sandbox.
  const phosra = new PhosraClient({
    baseUrl: 'https://phosra-api-sandbox-production.up.railway.app/api/v1',
  });

  // One-call setup
  const result = await phosra.setup.quick({
    child_name: 'Emma',
    birth_date: '2016-03-15',
    strictness: 'recommended',
  });

  console.log(`Created family: ${result.family.name}`); // Emma's Family
  console.log(`Age group: ${result.age_group}`);         // preteen
  console.log(`Rules enabled: ${result.rule_summary.total_rules_enabled}`); // 20
  ```

  ```bash cURL theme={null}
  # Sandbox-first: no Authorization header needed.
  curl -X POST https://phosra-api-sandbox-production.up.railway.app/api/v1/setup/quick \
    -H "Content-Type: application/json" \
    -d '{
      "child_name": "Emma",
      "birth_date": "2016-03-15",
      "strictness": "recommended"
    }'
  ```

  ```python Python theme={null}
  import requests

  # Sandbox-first: no Authorization header needed.
  BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"

  r = requests.post(
      f"{BASE}/setup/quick",
      json={"child_name": "Emma", "birth_date": "2016-03-15", "strictness": "recommended"},
      timeout=30,
  )
  r.raise_for_status()
  result = r.json()

  print("Created family:", result["family"]["name"])          # Emma's Family
  print("Age group:", result["age_group"])                    # preteen
  print("Rules enabled:", result["rule_summary"]["total_rules_enabled"])  # 20
  ```

  ```go Go theme={null}
  // Full program — `go run .` prints: Emma's Family preteen 20
  package main

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"net/http"
  )

  func main() {
  	// Sandbox-first: no Authorization header needed.
  	const base = "https://phosra-api-sandbox-production.up.railway.app/api/v1"

  	payload, _ := json.Marshal(map[string]string{
  		"child_name": "Emma", "birth_date": "2016-03-15", "strictness": "recommended",
  	})
  	req, _ := http.NewRequest("POST", base+"/setup/quick", bytes.NewReader(payload))
  	req.Header.Set("Content-Type", "application/json")

  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer resp.Body.Close()

  	var result struct {
  		Family      struct{ Name string `json:"name"` } `json:"family"`
  		AgeGroup    string                              `json:"age_group"`
  		RuleSummary struct {
  			TotalRulesEnabled int `json:"total_rules_enabled"`
  		} `json:"rule_summary"`
  	}
  	json.NewDecoder(resp.Body).Decode(&result)

  	fmt.Println(result.Family.Name, result.AgeGroup, result.RuleSummary.TotalRulesEnabled)
  }
  ```
</CodeGroup>

<Tip>
  Ran the TypeScript tab above verbatim against the sandbox and it printed `Emma's Family preteen 20` — see [Verified live](#verified-live) for the exact command and output.
</Tip>

### Going to production

The sandbox is open. Production requires a `phosra_` API key — pass it as `apiKey` (the SDK sends it as `X-Api-Key`) and drop the sandbox `baseUrl` so the client uses the default production host:

```typescript theme={null}
const phosra = new PhosraClient({
  apiKey: process.env.PHOSRA_API_KEY, // phosra_live_… — no baseUrl → https://prodapi.phosra.com/api/v1
});
```

<Card title="Get your API key" icon="key" href="https://dashboard.phosra.com/dashboard/developers/keys">
  Sign up at [dashboard.phosra.com/signup](https://dashboard.phosra.com/signup), then mint a key on the **Keys** page — `phosra_test_…` for the sandbox, `phosra_live_…` for production. See [Authentication](/authentication) for the full key lifecycle.
</Card>

## Authentication

`PhosraClientConfig` accepts exactly these fields (verified against `@phosra/sdk@0.1.0`):

| Field            | Header sent                   | Use for                                                                         |
| ---------------- | ----------------------------- | ------------------------------------------------------------------------------- |
| `apiKey`         | `X-Api-Key: phosra_…`         | Server-to-server / B2B / agents                                                 |
| `accessToken`    | `Authorization: Bearer <jwt>` | Parent-app user sessions (WorkOS token)                                         |
| `deviceKey`      | `X-Device-Key: phosra_dev_…`  | On-device enforcement                                                           |
| `baseUrl`        | —                             | Override the API host (defaults to `https://prodapi.phosra.com/api/v1`)         |
| `onTokenExpired` | —                             | `() => Promise<string>` — called on `401`; return a fresh access token to retry |

<Note>
  The SDK sends `apiKey` as the `X-Api-Key` header internally; the raw curl / Python / Go tabs above
  send the same key as `Authorization: Bearer` (the canonical header the rest of the docs use). The API
  accepts both interchangeably — pick either. See [Authentication](/authentication).
</Note>

```typescript theme={null}
// API key (server-to-server)
const phosra = new PhosraClient({
  apiKey: process.env.PHOSRA_API_KEY, // phosra_test_… or phosra_live_…
});

// Bearer token (user sessions)
const phosra = new PhosraClient({
  accessToken: 'eyJhbGciOi...',
});

// Device key — either via config or the static helper:
const device = new PhosraClient({ deviceKey: 'phosra_dev_...' });
const device2 = PhosraClient.forDevice({ deviceKey: 'phosra_dev_...' });
```

Precedence when more than one credential is set: `deviceKey` > `apiKey` > `accessToken`.

### Refreshing an expired token

There is **no** `maxRetries`/`retryDelay` option in `@phosra/sdk@0.1.0`. Token refresh is handled by the `onTokenExpired` callback — the client calls it on a `401`, then retries the request once with the returned token:

```typescript theme={null}
const phosra = new PhosraClient({
  accessToken: currentAccessToken,
  onTokenExpired: async () => {
    const { access_token } = await refreshMySession(); // your refresh logic
    return access_token; // the request is retried with this token
  },
});

// After a manual login/refresh you can also set it directly:
phosra.setAccessToken(newAccessToken);
```

## Resource Namespaces

The client organizes endpoints into resource namespaces:

```typescript theme={null}
// Auth
phosra.auth.register({ email, password, name })
phosra.auth.login({ email, password })
phosra.auth.refresh(refreshToken)
phosra.auth.logout()
phosra.auth.me()

// Families
phosra.families.list()
phosra.families.create({ name })
phosra.families.get(familyId)
phosra.families.update(familyId, { name })
phosra.families.delete(familyId)

// Children
phosra.children.create(familyId, { name, birth_date })
phosra.children.list(familyId)
phosra.children.get(childId)
phosra.children.update(childId, { name })
phosra.children.delete(childId)
phosra.children.ageRatings(childId)

// Policies
phosra.policies.create(childId, { name })
phosra.policies.list(childId)
phosra.policies.get(policyId)
phosra.policies.update(policyId, { name, priority })
phosra.policies.delete(policyId)
phosra.policies.activate(policyId)
phosra.policies.pause(policyId)
phosra.policies.generateFromAge(policyId)

// Rules
phosra.rules.list(policyId)
phosra.rules.create(policyId, { category, enabled, config })
phosra.rules.update(ruleId, { enabled, config })
phosra.rules.delete(ruleId)
phosra.rules.bulkUpsert(policyId, rules)

// Enforcement
phosra.enforcement.trigger(childId, { platform_ids? })
phosra.enforcement.listJobs(childId)
phosra.enforcement.getJob(jobId)
phosra.enforcement.getResults(jobId)
phosra.enforcement.retry(jobId)

// Compliance
phosra.compliance.create({ family_id, platform_id, credentials })
phosra.compliance.list(familyId)
phosra.compliance.verify(linkId)
phosra.compliance.delete(linkId)

// Webhooks
phosra.webhooks.create({ family_id, url, events })
phosra.webhooks.list(familyId)
phosra.webhooks.get(webhookId)
phosra.webhooks.update(webhookId, { url, events, active })
phosra.webhooks.delete(webhookId)
phosra.webhooks.test(webhookId)
phosra.webhooks.deliveries(webhookId)

// Platforms
phosra.platforms.list()
phosra.platforms.get(platformId)
phosra.platforms.byCategory(category)
phosra.platforms.byCapability(capability)

// Setup
phosra.setup.quick({ child_name, birth_date, strictness? })
```

## Error Handling

The SDK throws typed errors. `PhosraError` is the base class (it carries only `message`); API failures throw a `PhosraApiError` (or one of its subclasses) which adds `statusCode`, `code`, and `details`. For the full wire-level status/class matrix (every 4xx/5xx, its cause, and its fix) see the [Errors reference](/errors):

```typescript theme={null}
import {
  PhosraClient,
  PhosraApiError,
  PhosraNotFoundError,
} from '@phosra/sdk';

try {
  // A well-formed UUID that does not exist → 404 (PhosraNotFoundError).
  // A malformed id (e.g. 'nonexistent-id') returns 400 (invalid UUID) and
  // surfaces as the base PhosraApiError, not PhosraNotFoundError.
  const family = await phosra.families.get('00000000-0000-4000-8000-000000000000');
} catch (err) {
  if (err instanceof PhosraNotFoundError) {
    // 404 — dedicated subclass
    console.log('not found');
  } else if (err instanceof PhosraApiError) {
    console.log(err.statusCode);  // e.g. 404, 422, 429
    console.log(err.code);        // machine-readable code, if the API sent one
    console.log(err.details);     // extra fields from the response body
    console.log(err.message);
  }
}
```

The full exported error hierarchy (all extend `PhosraError`):

| Class                   | Thrown on   | Extra fields                      |
| ----------------------- | ----------- | --------------------------------- |
| `PhosraError`           | base class  | `message`                         |
| `PhosraApiError`        | any non-2xx | `statusCode`, `code?`, `details?` |
| `PhosraAuthError`       | `401`       | (inherits `PhosraApiError`)       |
| `PhosraNotFoundError`   | `404`       | (inherits `PhosraApiError`)       |
| `PhosraValidationError` | `422`       | (inherits `PhosraApiError`)       |
| `PhosraRateLimitError`  | `429`       | `retryAfter?` (seconds)           |

```typescript theme={null}
import { PhosraRateLimitError } from '@phosra/sdk';

try {
  await phosra.families.list();
} catch (err) {
  if (err instanceof PhosraRateLimitError) {
    await new Promise(r => setTimeout(r, (err.retryAfter ?? 1) * 1000));
    // ...then retry
  }
}
```

## Full Example: Setup to Enforcement

```typescript theme={null}
import { PhosraClient } from '@phosra/sdk';

const phosra = new PhosraClient({
  apiKey: process.env.PHOSRA_API_KEY!,
});

async function main() {
  // 1. Quick setup
  const setup = await phosra.setup.quick({
    child_name: 'Emma',
    birth_date: '2016-03-15',
    strictness: 'recommended',
  });

  console.log(`Created ${setup.age_group} policy with ${setup.rule_summary.total_rules_enabled} rules`);

  // 2. Connect a platform
  const link = await phosra.compliance.create({
    family_id: setup.family.id,
    platform_id: 'nextdns',
    credentials: process.env.NEXTDNS_API_KEY!,
  });

  console.log(`Connected NextDNS: ${link.status}`);

  // 3. Trigger enforcement
  const job = await phosra.enforcement.trigger(setup.child.id);
  console.log(`Enforcement job started: ${job.id}`);

  // 4. Poll for results
  let status = job.status;
  while (status === 'pending' || status === 'running') {
    await new Promise(r => setTimeout(r, 2000));
    const updated = await phosra.enforcement.getJob(job.id);
    status = updated.status;
  }

  // 5. Check results
  const results = await phosra.enforcement.getResults(job.id);
  for (const result of results) {
    console.log(`${result.platform_id}: ${result.rules_applied} applied, ${result.rules_failed} failed`);
  }
}

main();
```

## Resource methods (verified surface)

Every namespace and method below exists on `PhosraClient` in `@phosra/sdk@0.1.0`:

| Namespace                                               | Methods                                                                             |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `auth`                                                  | `register`, `login`, `refresh`, `logout`, `me`                                      |
| `families`                                              | `list`, `create`, `get`, `update`, `delete`                                         |
| `children`                                              | `list`, `create`, `get`, `update`, `delete`, `ageRatings`                           |
| `members`                                               | `list`, `invite`, `remove`                                                          |
| `policies`                                              | `list`, `create`, `get`, `update`, `delete`, `activate`, `pause`, `generateFromAge` |
| `rules`                                                 | `list`, `create`, `update`, `delete`, `bulkUpsert`                                  |
| `enforcement`                                           | `trigger`, `triggerLink`, `listJobs`, `getJob`, `getResults`, `retry`               |
| `compliance`                                            | `list`, `create`, `verify`, `delete`, `enforce`                                     |
| `platforms`                                             | `list`, `get`, `byCategory`, `byCapability`                                         |
| `ratings`, `standards`, `devices`, `reports`, `sources` | see the [API Reference](/api-reference/overview)                                    |
| `setup`                                                 | `quick`                                                                             |

## Verified live

The Quick Start code was run against the open sandbox census (`https://phosra-api-sandbox-production.up.railway.app/api/v1`, no key required) with `@phosra/sdk@0.1.0`:

```typescript theme={null}
import { PhosraClient } from '@phosra/sdk';

const phosra = new PhosraClient({
  baseUrl: 'https://phosra-api-sandbox-production.up.railway.app/api/v1',
});

const r = await phosra.setup.quick({
  child_name: 'Emma',
  birth_date: '2016-03-15',
  strictness: 'recommended',
});
console.log(r.family.name, r.age_group, r.rule_summary.total_rules_enabled);

const rules = await phosra.rules.list(r.policy.id);
console.log('rules:', rules.length);
```

Actual output:

```text theme={null}
Emma's Family preteen 20
rules: 20
```

<Tip>
  Point `baseUrl` at the sandbox to try any snippet on this page without an API key. For production, drop `baseUrl` (it defaults to `https://prodapi.phosra.com/api/v1`) and pass a `phosra_` key — [get one on the dashboard](https://dashboard.phosra.com/dashboard/developers/keys).
</Tip>
