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
| Approach | Best for | Typed | Maintenance |
|---|---|---|---|
| Generated client (any language) | Broad API surface, many endpoints, refactor safety | Yes — models for all 360 schemas | Regenerate when the spec changes |
| Direct HTTP wrapper | A handful of endpoints, minimal dependencies, edge/serverless runtimes | Only what you declare | You own ~100 lines |
| OSPulse CLI | CI gating and terminal workflows, no code at all | n/a | Pin 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:
curl -sO https://ospulse.app/openapi.jsonPoint 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.
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=falseOverride 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 schemeRequiredThe spec declares a single scheme, ApiToken (HTTP bearer). Most generators expose it as an
accessToken or bearerToken configuration property. See Authentication.
Base URLstringRequiredSet 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:
- Attach
Authorization: Bearer {token}to every request. - Turn a non-2xx response into a typed error carrying the
RFC 7807
correlationId, because that is what support will ask for. - Walk the
take/skippagination envelope without you writing a loop at every call site. - Retry
429and5xxwith backoff, honouringRetry-After.
The implementations below do exactly that, in about a hundred lines each.
// 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:
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
POSTis 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.jsonand fail your build when it moves, so you review API changes deliberately instead of discovering them at runtime.
