TypeScript SDK

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

Verified against the published package. Everything on this page was checked against @phosra/[email protected] (the current npm release) and the code was run against the live sandbox — see Verified live at the bottom.

Installation

bash
npm install @phosra/sdk

Latest published version: 0.1.0. Pin the exact version in package.json — do not float on a caret range while the package is pre-1.0, since a 0.x minor may break (see SDK versioning):

json
{ "dependencies": { "@phosra/sdk": "0.1.0" } }

Quick Start

Every tab below leads with the open sandboxhttps://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 step (a real dashboard link) comes after you've seen it work.

typescript
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

Ran the TypeScript tab above verbatim against the sandbox and it printed Emma's Family preteen 20 — see Verified live for the exact command and output.

Beyond sandbox evaluation

Always pass the documented sandbox baseUrl during public evaluation. Production access is not enabled by omitting it or adding a phosra_live_… key. A design-partner pilot provisions the approved host and credentials explicitly.

Authentication

PhosraClientConfig accepts exactly these fields (verified against @phosra/[email protected]):

FieldHeader sentUse for
apiKeyX-Api-Key: phosra_…Server-to-server / B2B / agents
accessTokenAuthorization: Bearer <jwt>Parent-app user sessions (WorkOS token)
deviceKeyX-Device-Key: phosra_dev_…On-device enforcement
baseUrlOverride the API host (defaults to https://prodapi.phosra.com/api/v1)
onTokenExpired() => Promise<string> — called on 401; return a fresh access token to retry

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.

typescript
// API key (server-to-server)
const phosra = new PhosraClient({
  apiKey: process.env.PHOSRA_API_KEY, // provisioned sandbox key
});
 
// 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/[email protected]. 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
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:

The auth namespace ships register/login/refresh methods, but those three routes do not exist on the Go APIPOST /auth/register, /auth/login, and /auth/refresh all return 404 (identity lives in WorkOS AuthKit; see Authentication). Only auth.me() and auth.logout() are backed by live endpoints (logout is a documented no-op). Treat the other three as dead surface in @phosra/[email protected].

typescript
// Auth
phosra.auth.register({ email, password, name })   // 404 — no such route; use WorkOS AuthKit
phosra.auth.login({ email, password })            // 404 — no such route; use WorkOS AuthKit
phosra.auth.refresh(refreshToken)                 // 404 — no such route; use onTokenExpired
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:

typescript
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):

ClassThrown onExtra fields
PhosraErrorbase classmessage
PhosraApiErrorany non-2xxstatusCode, code?, details?
PhosraAuthError401(inherits PhosraApiError)
PhosraNotFoundError404(inherits PhosraApiError)
PhosraValidationError422(inherits PhosraApiError)
PhosraRateLimitError429retryAfter? (seconds)
typescript
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
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/[email protected]:

NamespaceMethods
authregister, login, refresh, logout, me
familieslist, create, get, update, delete
childrenlist, create, get, update, delete, ageRatings
memberslist, invite, remove
policieslist, create, get, update, delete, activate, pause, generateFromAge
ruleslist, create, update, delete, bulkUpsert
enforcementtrigger, triggerLink, listJobs, getJob, getResults, retry
compliancelist, create, verify, delete, enforce
platformslist, get, byCategory, byCapability
ratings, standards, devices, reports, sourcessee the API Reference
setupquick

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/[email protected]:

typescript
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
Emma's Family preteen 20
rules: 20

Point baseUrl at the sandbox to try these snippets with synthetic data. Do not rely on the client's default host for production access.