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

@sherlockhealth/sdk

v2.0.0

Published

Typed TypeScript SDK for the OpenDoc protocol API. Use from any Node, browser, or edge runtime.

Readme

@sherlockhealth/sdk

Typed TypeScript SDK for the OpenDoc protocol API. Use it from any Node, browser, edge runtime (Bun, Deno, Cloudflare Workers).

Note on the package name. This SDK is published as @sherlockhealth/sdk. The older @opendoc/sdk name on the public npm registry is an unrelated third-party package — not this SDK. Do not install it.

Install

npm install @sherlockhealth/sdk   # or pnpm add / yarn add

Inside this monorepo, consume it as the workspace package instead:

# in your app's package.json dependencies:
#   "@sherlockhealth/sdk": "workspace:*"
pnpm add @sherlockhealth/sdk --workspace

Quickstart

1. Browse the catalog (no auth)

import { OpenDocClient } from '@sherlockhealth/sdk';

const client = new OpenDocClient({ baseUrl: 'https://api.opendoc.com' });

const { data } = await client.search({ q: 'knee MRI', geo: 'us-OH', limit: 10 });
for (const m of data) {
  console.log(`${m.providerName} @ ${m.scpName}: $${m.cashPriceCents / 100} (${m.confidence})`);
}

// Get the §VIII Sure Price ceiling guarantee.
const { data: quote } = await client.getSurePrice({ hsoSlug: 'knee-mri' });
if (quote) console.log(`Guaranteed max: $${quote.surePriceCents / 100}`);

2. Patient grants you a scoped agent token

The patient creates a token via opendoc.com → Settings → Agent Tokens. They paste the raw token (hk_agent_...) into your app config. The token has explicit:

  • Permissions (search, book, pay, cancel, ...)
  • Data tier (1=basic / 2=clinical / 3=full)
  • Spending caps (single transaction max, monthly cap)
  • Expiry
  • Ecosystem ID (so OpenDoc can mass-revoke if your ecosystem is compromised)

3. Drive a transaction (S0-S8)

const client = new OpenDocClient({
  baseUrl: 'https://api.opendoc.com',
  agentToken: 'hk_agent_...',
});

// S0 → S1
const { data: intent } = await client.declareIntent({
  providerHsoId: 'uuid',
  availabilitySlotId: 'uuid',
});

// S1 → S2 (price locked, HSO Instance immutable from here)
await client.authorize(intent.transactionId);

// S2 → S3
await client.acceptTerms(intent.transactionId);

// S3 → S4 atomic commit
await client.commit(intent.transactionId);

Prefer one call? client.createBooking({ providerHsoId, availabilitySlotId }) runs the compact S0→S4 path (booking + payment intent).

Preview before committing money: client.simulate(transactionId, 'commit') returns the exact signed price-lock (priceLock.jws, verifiable offline against /.well-known/opendoc-protocol/jwks.json) and the max obligation the real transition would carry — zero side effects. Note the JWS claims are snake_case (max_obligation_cents, valid_until); the consequence mirror is camelCase.

Idempotency — automatic

Every side-effecting method (createBooking, declareIntent, authorize, acceptTerms, commit, cancelBooking) sends an auto-generated UUID Idempotency-Key HTTP header, so a network-failure retry can never double-book or double-charge. Replays of an already-committed submit return the original response with replayed: true.

// default: SDK generates a fresh UUID per call
await client.commit(transactionId);

// bring your own key (e.g. derived from your own job id)
await client.commit(transactionId, 'my-stable-key');

// suppress entirely (the call also becomes non-retryable)
await client.commit(transactionId, false);

Set autoIdempotency: false in ClientOptions to disable generation globally.

Retries — safe by default, configurable off

The SDK retries GETs and idempotency-keyed writes only — never unkeyed writes — on 408, 429, 5xx, and network errors. Max 3 attempts, exponential backoff with jitter; a Retry-After header on 429s is honored when present and the SDK falls back to backoff when it is absent.

const client = new OpenDocClient({
  baseUrl: 'https://api.opendoc.com',
  retry: false,                        // opt out entirely
  // or tune it:
  // retry: { maxAttempts: 5, baseDelayMs: 500, maxDelayMs: 8000 },
});

Auto-pagination

Offset-paginated lists have for await iterators driven by each response's own pagination object (stops on hasMore: false):

for await (const offer of client.iterateOffers({ specialty: 'ORTHOPEDIC_SURGERY' })) {
  console.log(offer.hsoTitle, offer.cashPriceCents);
}
// also: iterateProviders, iterateHsos, iterateServices, iterateNpi, iterateMyBookings

(iterateMyBookings mirrors a real server quirk: /me/bookings reports no total and hasMore is a limit heuristic, so a page-aligned result set may cost one trailing empty request.)

4. Subscribe to events

Webhooks (server-to-server)

const { data: sub } = await client.createEventSubscription({
  callbackUrl: 'https://your-app.example.com/webhooks/opendoc',
  eventTypes: ['transaction.state_changed', 'transaction.funds_released'],
});

// Save sub.signingSecret — it's shown ONCE.

Verify signatures on incoming webhooks. OpenDoc signs the timestamped scheme x-opendoc-signature: t=<unix>,v1=<hmac_sha256_hex(secret, t + '.' + rawBody)> (replay-resistant; default tolerance 300s). The legacy bare-hex digest still rides in x-opendoc-signature-legacy for one deprecation cycle and this verifier accepts both. Delivery is at-least-once — dedupe by the x-opendoc-event-id header (also eventId in the body).

import { verifyWebhookSignature } from '@sherlockhealth/sdk/webhooks';

app.post('/webhooks/opendoc', express.raw({ type: 'application/json' }), async (req, res) => {
  const ok = await verifyWebhookSignature({
    rawBody: req.body.toString('utf-8'),
    signatureHeader: req.headers['x-opendoc-signature'],
    signingSecret: process.env.OPENDOC_WEBHOOK_SECRET!,
    toleranceSeconds: 300,
  });
  if (!ok) return res.status(401).send('bad signature');

  const event = JSON.parse(req.body.toString('utf-8'));
  // handle event.eventType...
  res.sendStatus(200);
});

Or use the bundled middleware:

import { webhookSignatureMiddleware } from '@sherlockhealth/sdk/webhooks';

app.post(
  '/webhooks/opendoc',
  express.raw({ type: 'application/json' }),
  webhookSignatureMiddleware(process.env.OPENDOC_WEBHOOK_SECRET!),
  (req, res) => { /* signature already verified */ }
);

SSE (browser)

const stream = client.openEventStream();
stream.addEventListener('transaction.state_changed', (e) => {
  console.log(JSON.parse(e.data));
});

Errors

All errors throw OpenDocError with a stable machine-readable code:

import { OpenDocError } from '@sherlockhealth/sdk';

try {
  await client.commit(transactionId);
} catch (e) {
  if (e instanceof OpenDocError) {
    if (e.code === 'payment_required') {
      // exceeded spending limit
    } else if (e.code === 'rate_limited') {
      // back off
    } else if (e.code === 'forbidden') {
      // missing consent or permission
      console.error(e.message);
    }
  }
}

What this SDK enforces vs what the server enforces

The SDK is a transport. All compliance — HIPAA audit, AKS, consent gating, spending caps, concentration ceiling, the clinical-safety guard on generated prose — lives on the server. The SDK can't be tricked into bypassing them.

The SDK does validate types, surface errors clearly, auto-generate idempotency keys, and retry safely by default (see above — retries are scoped to reads and keyed writes only, and can be turned off). It does not:

  • Cache responses (let the caller cache as needed)
  • Retry unkeyed writes, ever (an ambiguous failure on an unkeyed write is surfaced to you, not replayed)
  • Hide PHI (the server does that)

Coverage highlights

Beyond search/transactions/patient-self, the client covers:

  • Attenuated delegation — mintChildAgentToken() mints a strictly-weaker child token for a specialist sub-agent; getMyAgentToken() returns the calling token's live budget (meta.effectiveRemainingCents — check it before promising to transact).
  • Clinical routing — clinicalRoute({ q, state }) and getCareLaneCarePlan({ q }) for symptom → specialist routing with deterministic safety intercepts.
  • The 9M-NPI directory — listNpi() / getNpiProfile(npi) — the public fallback pivot when a provider isn't onboarded yet; listSpecialties().
  • Price Registry — getRegistryPrices() / getRegistrySummary(): committed (escrow-backed, all-in) prices and labeled estimates in separate fields, never mixed. getPriceBenchmark({ cpt }) returns the attributed Medicare PFS anchor ({ data: null } when no row is seeded — render nothing).
  • Crawl surface — getProviderSitemapFeed().
  • Card on file — getPaymentMethod() (display fields only; saving or removing a card is a first-person browser act the SDK does not wrap).

Capability discovery

const proto = await client.protocol();
console.log(proto.version);
console.log(proto.tools.length);  // every tool the server exposes
console.log(proto.capabilities);  // mcp, events, sure_price, etc.
console.log(proto.permissions, proto.consents, proto.error_shape.codes);

Compatibility

  • Node 19+ (uses WebCrypto for webhook verification — Node ≥ 19 has it built in)
  • Bun, Deno, Cloudflare Workers
  • Browsers (modern; Web Crypto required)

For Node 16-18 use the crypto module directly via webhookSignatureMiddleware which falls through to Buffer-based handling.