OSPulse

SDK overview

The OSPulse API is a plain JSON REST API with a complete OpenAPI 3.0.1 contract, so you have three honest options: use the generated Python client, generate a typed client for your own language from the spec, or talk to the API directly over HTTP. This page covers all three and gives you a production-quality hand-rolled client for Node.js, Python, Go, and C#.

What actually exists today

There is a generated Python client and the OSPulse CLI. There is no published npm or Go module — if a page anywhere tells you to npm install ospulse, it is wrong. For every other language the supported path is generating a client from https://ospulse.app/openapi.json or calling the API directly. Both are first-class: the spec is the same artefact the API is built from.

Choosing an approach

ApproachBest forTypedMaintenance
Generated client (any language)Broad API surface, many endpoints, refactor safetyYes — models for all 360 schemasRegenerate when the spec changes
Direct HTTP wrapperA handful of endpoints, minimal dependencies, edge/serverless runtimesOnly what you declareYou own ~100 lines
OSPulse CLICI gating and terminal workflows, no code at alln/aPin a version in CI

A useful rule of thumb: if your integration touches more than five or six endpoints, or you care about compile-time safety when the API adds fields, generate. If you are pushing a scan verdict into a build pipeline and reading one collection, write the wrapper — it is smaller than the generated package and has no transitive dependencies.

Generate a client from the OpenAPI spec

The spec is the source of truth for all 246 operations and 360 schemas, and it is served publicly with no authentication required:

bash
curl -sO https://ospulse.app/openapi.json

Point any OpenAPI 3 generator at it. The examples below use OpenAPI Generator via its Docker image, so you do not need a local Java or Node toolchain.

bash
docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate \
  -i https://ospulse.app/openapi.json \
  -g typescript-fetch \
  -o /local/src/ospulse \
  --additional-properties=supportsES6=true,withoutRuntimeChecks=false

Override the server URL

The servers block in the published spec points at a development host. Whatever generator you use, set the base URL explicitly to https://api.ospulse.app when you construct the client — do not rely on the baked-in default.

Generated clients need two more things wired up before they are usable:

Bearer tokensecurity schemeRequired

The spec declares a single scheme, ApiToken (HTTP bearer). Most generators expose it as an accessToken or bearerToken configuration property. See Authentication.

Base URLstringRequired

Set to https://api.ospulse.app. Paths in the spec already include the /api/v1 prefix.

Commit the generation command — not just the output — so anyone can reproduce the client, and see Keeping clients in sync for how to detect spec changes in CI.

Direct HTTP: a small, real client

If you would rather not carry a generated package, the API is simple enough to wrap by hand. A good wrapper is more than a fetch call — it needs to do four things:

  1. Attach Authorization: Bearer {token} to every request.
  2. Turn a non-2xx response into a typed error carrying the RFC 7807 correlationId, because that is what support will ask for.
  3. Walk the take/skip pagination envelope without you writing a loop at every call site.
  4. Retry 429 and 5xx with backoff, honouring Retry-After.

The implementations below do exactly that, in about a hundred lines each.

js
// ospulse.mjs — zero dependencies, Node 18+ (global fetch).
export class OSPulseError extends Error {
  constructor(problem, status) {
    super(problem?.detail || problem?.title || `HTTP ${status}`)
    this.name = 'OSPulseError'
    this.status = status
    this.type = problem?.type
    this.correlationId = problem?.correlationId
    this.errors = problem?.errors // validation errors: { field: [messages] }
  }
}
 
export class OSPulse {
  constructor({ token = process.env.OSPULSE_TOKEN, baseUrl = 'https://api.ospulse.app', maxRetries = 3 } = {}) {
    if (!token) throw new Error('OSPULSE_TOKEN is not set')
    this.token = token
    this.baseUrl = baseUrl.replace(/\/$/, '')
    this.maxRetries = maxRetries
  }
 
  async request(path, { method = 'GET', query, body, signal } = {}) {
    const url = new URL(`${this.baseUrl}/api/v1/${path.replace(/^\//, '')}`)
    for (const [k, v] of Object.entries(query ?? {})) {
      if (v !== undefined && v !== null) url.searchParams.set(k, String(v))
    }
 
    for (let attempt = 0; ; attempt++) {
      const res = await fetch(url, {
        method,
        signal,
        headers: {
          Authorization: `Bearer ${this.token}`,
          Accept: 'application/json',
          ...(body ? { 'Content-Type': 'application/json' } : {}),
        },
        body: body ? JSON.stringify(body) : undefined,
      })
 
      if (res.status === 204) return null
      if (res.ok) return res.json()
 
      // Retry throttling and transient server errors only.
      const retryable = res.status === 429 || res.status >= 500
      if (retryable && attempt < this.maxRetries) {
        const header = Number(res.headers.get('retry-after'))
        const wait = Number.isFinite(header) && header > 0 ? header * 1000 : 2 ** attempt * 500
        await new Promise((r) => setTimeout(r, wait + Math.random() * 250))
        continue
      }
 
      // Errors are problem+json; fall back gracefully if a proxy mangles it.
      const problem = await res.json().catch(() => null)
      throw new OSPulseError(problem, res.status)
    }
  }
 
  /** Async iterator over an offset-paginated collection. */
  async *paginate(path, { query = {}, pageSize = 100 } = {}) {
    let skip = 0
    for (;;) {
      const page = await this.request(path, { query: { ...query, take: pageSize, skip } })
      for (const item of page.items) yield item
      skip += page.items.length
      if (page.items.length === 0 || skip >= page.totalEstimate) return
    }
  }
}

Using it

The wrapper collapses a typical integration to a few lines. Listing every tracked package and flagging the low-scoring ones:

js
import { OSPulse, OSPulseError } from './ospulse.mjs'
 
const client = new OSPulse()
 
try {
  for await (const pkg of client.paginate('packages', { query: { ecosystem: 'Npm' } })) {
    if (pkg.healthScore < 50) console.log(`${pkg.name} → ${pkg.healthScore}`)
  }
} catch (err) {
  if (err instanceof OSPulseError) {
    console.error(`${err.status} ${err.message} (correlationId ${err.correlationId})`)
  }
  throw err
}

Log the correlationId, always

Every error body carries a correlationId. Surface it in your logs and exception messages — it is the one value that lets support find your exact request in our traces.

What to build on top

Three additions turn the wrapper above into something you will not regret in a year:

  • Idempotency for writes. Retrying a POST is only safe when the endpoint supports it — see Idempotency before you add retries to anything other than reads.
  • Rate-limit awareness. Limits are reported in the RateLimit-* response headers; read them and pace yourself rather than waiting to be rejected. See Rate limits.
  • A pinned spec hash. Store the SHA-256 of openapi.json and fail your build when it moves, so you review API changes deliberately instead of discovering them at runtime.

Next steps