npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@owlic/sdk

v0.2.0

Published

Official TypeScript SDK for the Owlic public API (api.owlic.fr). Fully typed client derived from the API's OpenAPI/Zod contract — no runtime imports beyond fetch (zod is a type-level dependency), works on Node 18+, edge and serverless.

Readme

@owlic/sdk

Official TypeScript SDK for the Owlic public API.

  • Fully typed — every request/response type is inferred from @owlic/api-contract, the same Zod schemas that generate the OpenAPI document served at /v1/openapi.json. The SDK cannot drift from the API.
  • No runtime imports — a thin layer over fetch (zod appears only as a type-level dependency). Works on Node.js ≥ 18, Vercel/edge runtimes, Bun and Deno.
  • Batteries included — automatic retries with exponential backoff (Retry-After aware), per-request timeout & abort, typed errors, auto-pagination.

Installation

npm install @owlic/sdk

Inside the Owlic monorepo the package is consumed directly from source (root tsconfig paths) — no install needed.

Quickstart

import { Owlic } from '@owlic/sdk';

const owlic = new Owlic({
  apiKey: process.env.OWLIC_API_KEY, // 'owlic_sk_…' — created from the Owlic dashboard
});

// Check your credentials
const me = await owlic.whoami();
console.log(me.organizationId);

// Read resources
const trainers = await owlic.trainers.list();
const action = await owlic.trainingActions.get('action-id');

// Drive the Qualiopi workflow end-to-end
const created = await owlic.trainingActions.create({
  title: 'Formation Excel avancé',
  requiresQualiopiCompliance: true,
});

await owlic.trainingActions.saveBeneficiaries(created.id, {
  beneficiaries: [
    { firstName: 'Florian', lastName: 'Truchot', email: '[email protected]', companySiret: '99499860700018' },
  ],
  companies: [
    { siret: '99499860700018', denominationSociale: 'OWLIC', ville: 'Lyon', codePostal: '69001' },
  ],
  replaceExisting: true,
});

new Owlic() with no arguments reads OWLIC_API_KEY (and optionally OWLIC_BASE_URL) from the environment. A keyless client is allowed and can call the open endpoints (health()); authenticated methods then throw a helpful error at call time.

⚠️ Server-side only. API keys are secrets — never ship them to a browser. The constructor throws if it detects a browser environment while an API key is configured (override with dangerouslyAllowBrowser: true at your own risk).

Configuration

const owlic = new Owlic({
  apiKey: 'owlic_sk_…',            // default: process.env.OWLIC_API_KEY
  baseUrl: 'https://api.owlic.fr', // default: process.env.OWLIC_BASE_URL ?? https://api.owlic.fr
  timeout: 30_000,                 // ms per attempt, covers the body read (default 30s)
  maxRetries: 2,                   // see "Retries" below (default 2)
  fetch: customFetch,              // bring your own fetch (proxy, instrumentation…)
  defaultHeaders: { 'x-request-source': 'my-integration' },
});

Every method also accepts per-request options as its last argument:

await owlic.trainers.list({ timeout: 5_000, maxRetries: 0, signal: abortController.signal });

API surface

| SDK method | Endpoint | |---|---| | owlic.whoami() | GET /v1/whoami | | owlic.health() | GET /health (no auth — works on a keyless client; returns the payload even on 503) | | owlic.trainers.list() | GET /v1/trainers | | owlic.trainers.get(id) | GET /v1/trainers/{trainerId} | | owlic.trainingPrograms.list({ page, limit }) | GET /v1/training-programs | | owlic.trainingPrograms.get(id) | GET /v1/training-programs/{programId} | | owlic.trainingPrograms.create(body) | POST /v1/training-programs | | owlic.trainingPrograms.update(id, body) | PATCH /v1/training-programs/{programId} (core fields) | | owlic.trainingPrograms.delete(id) | DELETE /v1/training-programs/{programId} (soft delete) | | owlic.trainingPrograms.replaceModules(id, body) | PUT /v1/training-programs/{programId}/modules | | owlic.trainingPrograms.addTrainers(id, body) | POST /v1/training-programs/{programId}/trainers | | owlic.trainingPrograms.removeTrainer(id, trainerId) | DELETE /v1/training-programs/{programId}/trainers/{trainerId} | | owlic.trainingPrograms.updatePositioning(id, body) | PATCH /v1/training-programs/{programId}/positioning | | owlic.trainingPrograms.updateEvaluation(id, body) | PATCH /v1/training-programs/{programId}/evaluation | | owlic.trainingActions.list() | GET /v1/training-actions | | owlic.trainingActions.get(id) | GET /v1/training-actions/{actionId} | | owlic.trainingActions.create(body) | POST /v1/training-actions | | owlic.trainingActions.saveNeedsAnalysis(id, body) | POST /v1/training-actions/{actionId}/needs-analysis | | owlic.trainingActions.saveBeneficiaries(id, body) | POST /v1/training-actions/{actionId}/beneficiaries | | owlic.trainingActions.assignProgram(id, body) | POST /v1/training-actions/{actionId}/program | | owlic.trainingActions.planSessions(id, body) | POST /v1/training-actions/{actionId}/sessions | | owlic.trainingActions.assignTrainers(id, body) | POST /v1/training-actions/{actionId}/trainers | | owlic.trainingActions.setPricing(id, body) | POST /v1/training-actions/{actionId}/pricing | | owlic.trainingActions.generateConvention(id, body) | POST /v1/training-actions/{actionId}/convention |

Responses are unwrapped: methods return the resource itself (the API's { data } envelope is handled for you).

For an endpoint the SDK doesn't wrap yet, use the escape hatch — it reuses auth, retries, timeout and error mapping:

const raw = await owlic.request<{ data: unknown }>('GET', '/v1/new-endpoint', { query: { page: 1 } });

Pagination

Paginated endpoints return a Page<T>:

const page = await owlic.trainingPrograms.list({ limit: 50 });

page.data;               // TrainingProgram[] — this page
page.pagination;         // { page, limit, total, totalPages }
await page.nextPage();   // Page<TrainingProgram> | null

// Or let the SDK walk all pages for you:
for await (const program of page) {
  console.log(program.title);
}

Error handling

All failures extend OwlicError. HTTP errors are OwlicAPIError subclasses carrying status + the API's machine-readable code:

import { NotFoundError, RateLimitError, OwlicAPIError } from '@owlic/sdk';

try {
  await owlic.trainers.get('unknown-id');
} catch (error) {
  if (error instanceof NotFoundError) {
    // 404 — not in your organization
  } else if (error instanceof RateLimitError) {
    console.log(`Retry in ${error.retryAfter}ms`); // already retried `maxRetries` times
  } else if (error instanceof OwlicAPIError) {
    console.log(error.status, error.code, error.message);
  }
}

| Class | Status | Meaning | |---|---|---| | BadRequestError | 400 | Invalid request body/parameters | | AuthenticationError | 401 | Missing or invalid API key | | PermissionDeniedError | 403 | Key disabled/expired, or apiKeys feature off | | NotFoundError | 404 | Resource not found in your organization | | UnprocessableEntityError | 422 | Rejected by a domain rule | | RateLimitError | 429 | Rate limit / quota exceeded (retryAfter in ms) | | InternalServerError | 5xx | API-side failure | | OwlicConnectionError | — | Network failure (no HTTP response) | | OwlicTimeoutError | — | Request exceeded timeout |

Retries

Retries (maxRetries, exponential backoff, honors Retry-After) are method-aware:

  • 429 is retried for every method — the API's rate limiters reject requests before processing them, so a retry can never duplicate work. Exceptions: quota exhaustion (USAGE_EXCEEDED) is never retried, and a Retry-After beyond 60s fails fast instead of blocking.
  • Timeouts, connection errors and 5xx are retried for idempotent methods only (GET/HEAD/PUT/DELETE). POSTs are never replayed after a timeout or 5xx: the API has no idempotency keys, and the server may have committed the write before the failure — a replay could silently duplicate the resource (e.g. a training action and its Qualiopi folder).
  • Other 4xx client errors are never retried.

Aborting via RequestOptions.signal takes effect immediately, including during a retry backoff.

Types

All request/response types are exported:

import type { Trainer, TrainingProgramDetail, CreateTrainingActionRequest } from '@owlic/sdk';

They are z.infer<…> of the @owlic/api-contract schemas — the single source of truth shared with the OpenAPI document, so a contract change immediately surfaces as a compile error here.

Public mirror

This package is mirrored to Owlic-App/Typescript-SDK by the Sync SDK Mirror GitHub Action (.github/workflows/sync-sdk-mirror.yml): every merge to main touching packages/sdk/** force-pushes a fresh git subtree split of this directory over the mirror's main. The monorepo is the single source of truth — never commit to the mirror directly, changes there are overwritten on the next sync.

Changelog

0.2.0

  • Training-program writes — the trainingPrograms resource is no longer read-only. Added create, update (core fields: title, description, pricing, distribution, prerequisites, objectives), delete (soft), replaceModules, addTrainers / removeTrainer, and updatePositioning / updateEvaluation, covering the per-section write endpoints of /v1/training-programs*. Backward compatible — no existing method changed.

0.1.0

  • Initial release: whoami, health, trainers (list/get), trainingPrograms (list/get, auto-paginated), and trainingActions (list/get + the Qualiopi workflow writes). Typed errors, retries, timeouts and auto-pagination.

Release process (npm)

The package is published to npm as @owlic/sdk by the Publish SDK to npm GitHub Action (.github/workflows/publish-sdk.yml, manual trigger). The build (tsup) emits ESM + CJS + .d.ts with the private @owlic/api-contract types inlined, so the published package is self-contained (zod is its only — type-level — dependency).

To release:

  1. Bump version in packages/sdk/package.json (semver) and merge to main.
  2. Run the Publish SDK to npm workflow from the Actions tab. It tests, builds, verifies the .d.ts is self-contained, refuses to overwrite an existing version, then publishes.
  3. Requires the NPM_TOKEN repo secret (npm Automation token with publish rights on the @owlic scope).

Local dry run: npm run build --workspace @owlic/sdk && npm pack --workspace @owlic/sdk.

Development

npm run type:check --workspace @owlic/sdk   # type-check
npx vitest run packages/sdk/src              # tests