Skip to main content
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/sdk@0.1.0 (the current npm release) and the code was run against the live sandbox — see Verified live at the bottom.

Installation

npm install @phosra/sdk
Latest published version: 0.1.0. Pin it in package.json for reproducible builds:
{ "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.
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.

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:
const phosra = new PhosraClient({
  apiKey: process.env.PHOSRA_API_KEY, // phosra_live_… — no baseUrl → https://prodapi.phosra.com/api/v1
});

Get your API key

Sign up at dashboard.phosra.com/signup, then mint a key on the Keys page — phosra_test_… for the sandbox, phosra_live_… for production. See Authentication for the full key lifecycle.

Authentication

PhosraClientConfig accepts exactly these fields (verified against @phosra/sdk@0.1.0):
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.
// 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:
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:
// 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:
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)
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

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:
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/sdk@0.1.0:
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:
Emma's Family preteen 20
rules: 20
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.