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

@picturehouse/sdk

v0.8.0

Published

Typed TypeScript / JavaScript client for the PictureHouse public API. Editorial press-photo storefronts, multi-tenant. Works in modern Node (>=18) and the browser.

Readme

@picturehouse/sdk

Typed TypeScript / JavaScript client for the PictureHouse public API. Works in modern Node (≥18) and in the browser without a bundler shim.

This SDK is the recommended way for tenant frontends (Orange Pictures, etc.) and B2B partners (newsroom CMSs, automation jobs) to talk to PictureHouse.


Install

npm install @picturehouse/sdk

Pre-built JavaScript + TypeScript declarations included; no bundler shim or postinstall step needed. Works in modern Node (≥ 18) and in any browser bundler (Vite, Webpack, Rspack, esbuild, Parcel, Bun, …).

The package is published from packages/sdk-typescript/ in the PictureHouse repo. Source is MIT-licensed.


Quickstart

Browser storefront (Orange Pictures style)

import { PictureHouse } from "@picturehouse/sdk";

const ph = new PictureHouse({
  // Hit your tenant origin so cookies, CORS, and Stripe redirects all
  // line up. The /api/v1 path lives on PictureHouse but is reachable
  // through your tenant's own domain via the allowedOrigins list set
  // by the tenant admin.
  baseUrl: "https://orange-pictures.com/api/v1",
});

// 1. Sign the buyer in. The SDK stores the JWT internally so every
//    later call carries it automatically.
await ph.auth.login({ email, password });

// 2. List events for the events page.
const events = await ph.events.list({ limit: 20 });

// 3. Browse images for an event.
const photos = await ph.images.list({ event: "ajax-feyenoord" });

// 4. Buyer picks favourites → save to a lightbox (PictureHouse's
//    "cart" equivalent, but persistent).
const lb = await ph.lightbox.create({ name: "Friday selects" });
for (const img of selectedImages) {
  await ph.lightbox.addItem(lb.id, { imageId: img.id });
}

// 5. Checkout the unpurchased contents of the lightbox.
const session = await ph.checkout.create({
  lightboxId: lb.id,
  successUrl: "https://orange-pictures.com/thanks",
  cancelUrl: "https://orange-pictures.com/cart",
});
window.location.href = session.sessionUrl!;

After the buyer returns from Stripe, your /thanks page can:

// Show "owned" images and offer downloads.
const purchases = await ph.purchases.list();
for (const p of purchases.data) {
  const dl = await ph.download.issue({ imageId: p.imageId });
  // Hand the buyer a 24h-valid link. Clicking it 302-redirects to
  // a 5-minute Cloudflare R2 signed URL of the original.
  console.log(dl.downloadUrl);
}

Server-to-server (newsroom CMS, automation)

import { PictureHouse } from "@picturehouse/sdk";

const ph = new PictureHouse({
  baseUrl: "https://orange-pictures.com/api/v1",
  apiKey: process.env.ORANGE_PICTURES_API_KEY!, // ph_<64 hex>
});

const events = await ph.events.list({ status: "COMPLETED" });
for (const event of events.data) {
  const photos = await ph.images.list({ event: event.slug, limit: 100 });
  // …import into the newsroom CMS…
}

Authentication

Two modes share the same Authorization: Bearer <token> header:

| Use case | Credential | Where it lives | | ----------------------------- | ------------------------ | ------------------------------------ | | B2B server-to-server | ph_<64 hex> API key | apiKey option (server env var) | | Buyer browser storefront | Short-lived JWT | jwtToken option (set by login()) |

If you pass both, the SDK prefers the JWT (browser flow always wins).

To sign out:

await ph.auth.logout(); // clears the SDK-side JWT

End-to-end storefront walkthrough

This section is meant to be copy-pasted into a fresh Next.js / React / Vue app to wire up the buyer flow on Orange Pictures (or any tenant frontend).

1. Create the SDK once at app boot

// src/lib/picturehouse.ts
import { PictureHouse } from "@picturehouse/sdk";

export const ph = new PictureHouse({
  baseUrl: process.env.NEXT_PUBLIC_PICTUREHOUSE_API_BASE!,
});

2. Login + register pages

import { ph } from "@/lib/picturehouse";
import { PictureHouseValidationError, PictureHouseAuthError } from "@picturehouse/sdk";

async function handleLogin(email: string, password: string) {
  try {
    const { user } = await ph.auth.login({ email, password });
    redirect(`/${user.id}/lightboxes`);
  } catch (err) {
    if (err instanceof PictureHouseValidationError) showFieldErrors(err.details);
    else if (err instanceof PictureHouseAuthError) toast("Invalid email or password");
    else throw err;
  }
}

ph.auth.register() is a discriminated union on buyerType — pick the variant for the entity signing up and TypeScript enforces the right fields:

// Press / media organisation (KvK for NL, VAT for non-NL)
await ph.auth.register({
  buyerType: "BUSINESS",
  email, password,
  company: "De Telegraaf",
  country: "NL",
  kvkNumber: "12345678",
});

// Sports club / association (KvK for NL clubs; VAT optional)
await ph.auth.register({
  buyerType: "CLUB",
  email, password,
  company: "AFC Ajax",
  country: "NL",
  kvkNumber: "34123456",
});

// Individual professional athlete (name only)
await ph.auth.register({
  buyerType: "ATHLETE",
  email, password,
  name: "Lieke Martens",
});

3. Storefront browse

const eventsPage = await ph.events.list({ status: "COMPLETED", limit: 24 });
const featured   = await ph.events.list({ status: "LIVE",      limit: 4 });

// Event detail
const players = await ph.events.persons("ajax-feyenoord");

// Filtered image grid
const photos = await ph.images.list({
  event: "ajax-feyenoord",
  person: "Hakim Ziyech",
  limit: 50,
});

4. Image detail

const image = await ph.images.get(imageId);
// image.purchased === true once the buyer owns this one — gate the
// "Download" button on this flag instead of re-fetching purchases.

5. Lightbox (cart)

const lightboxes = await ph.lightbox.list();

const cart = await ph.lightbox.create({ name: "Today's selects" });
await ph.lightbox.addItem(cart.id, { imageId: image.id, note: "for cover" });

const detail = await ph.lightbox.get(cart.id);
// detail.items[i].purchased — true for already-bought rows, false for
// rows that still need a checkout. Filter on this for the cart subtotal.

await ph.lightbox.removeItem(cart.id, image.id);

Claiming a shared lightbox into the cart

When a staff member shares a curated selection via a /share/<token> link, the recipient signs in (or registers — no guest checkout) and claims it into their cart, then checks out or downloads:

// The recipient lands on /share/<token>, then signs in. The storefront
// reads the token (e.g. from a `?claim=` query param) and claims it:
const claimed = await ph.lightbox.claim(shareToken);
// → { cartId, name, addedCount, skippedCount, unavailableCount, totalItems }

// Per-image / wallet / image-credit buyers: check out the new cart.
const session = await ph.checkout.create({ lightboxId: claimed.cartId });

// Subscription buyers: skip checkout — download each image directly
// (quota is debited at download time).
// for (const item of (await ph.lightbox.get(claimed.cartId)).items) {
//   await ph.download.create({ imageId: item.imageId });
// }

Pass ph.lightbox.claim(shareToken, { cartId }) to merge into an existing cart instead of creating a new one. Only purchasable (published, same-tenant) images are copied; unavailableCount reports any that were skipped.

6. Checkout

const res = await ph.checkout.create({
  lightboxId: cart.id, // OR { imageId } / { imageIds: [...] }
  // successUrl / cancelUrl are optional — omit them and the server
  // defaults to your storefront's origin.
});

if (res.paidFromWallet) {
  // Paid in full from the buyer's wallet — no Stripe redirect. The
  // purchase is already COMPLETED; show a success state.
  showPurchaseComplete(res);
} else {
  // Paying by card. If the buyer has *some* wallet balance but not
  // enough, explain why before redirecting:
  if (res.walletBalanceCents && res.walletBalanceCents < res.totalAmount) {
    toast(
      `Your wallet (€${(res.walletBalanceCents / 100).toFixed(2)}) doesn't ` +
        `cover the €${(res.totalAmount / 100).toFixed(2)} total — paying by card.`,
    );
  }
  window.location.href = res.sessionUrl!;
}

For card payments the buyer pays on Stripe Checkout (hosted by Stripe, not by you). Stripe webhooks fire back at PictureHouse, which marks the Purchase rows as COMPLETED. Your /thanks page can poll ph.purchases.list() or ph.lightbox.get(cart.id) for the freshly-flipped purchased: true flags.

6b. Wallet & credits (prepaid)

Buyers can prepay and spend with no card friction. Top-ups are charged on the tenant's Stripe account; the platform's cut is taken once at top-up, so spending is fee-free. A tenant pack is one of two kinds:

  • MONEY — tops up a currency balance (balanceCents), spent against each image's price. Set creditCents > priceCents to offer a bonus.
  • IMAGES — grants image-count credits (imageCredits): 1 credit = 1 image regardless of its price.
// Show the buyer both balances + the tenant's top-up packs
const { balanceCents, currency, imageCredits } = await ph.wallet.getBalance();
const packs = await ph.wallet.listPacks();
// MONEY pack:  { id, name, kind: "MONEY", priceCents, creditCents, bonusCents }
// IMAGES pack: { id, name, kind: "IMAGES", priceCents, creditImages }

// Buyer picks a pack → Stripe Checkout (works for either kind)
const topup = await ph.wallet.topUp({ packId: packs[0].id });
window.location.href = topup.sessionUrl!;
// On return, the balance (money or credits) is granted by webhook.

At checkout, ph.checkout.create() settles automatically with no Stripe round-trip when the buyer can cover the whole order — image credits are tried first (paidFromImageCredits: true), then the money wallet (paidFromWallet: true), otherwise it falls back to card. Each source is pay-in-full-or-not-at-all.

7. Downloads

import { PictureHousePaymentRequiredError } from "@picturehouse/sdk";

try {
  const dl = await ph.download.issue({ imageId });
  // dl.downloadUrl is a 24h single-use link. Hand it to the browser:
  window.open(dl.downloadUrl, "_blank");
} catch (err) {
  if (err instanceof PictureHousePaymentRequiredError) showBuyDialog();
  else throw err;
}

8. Subscriptions

// Pricing page (public, no auth needed)
const plans = await ph.subscriptions.plans();

// Buyer picks one
const sub = await ph.subscriptions.subscribe({
  planId: plans[0].id,
  successUrl: window.location.origin + "/account",
});
window.location.href = sub.sessionUrl!;

// Account page
const me = await ph.subscriptions.me();
console.log(me.subscription?.downloadsRemaining); // null = unlimited

// Cancel-at-period-end
await ph.subscriptions.cancel();

Errors

Every method either resolves with the parsed body or rejects with a typed subclass of PictureHouseError:

| Error class | HTTP | When | | ------------------------------------ | ---- | ----------------------------------------------- | | PictureHouseValidationError | 400 | Field errors in .details | | PictureHouseAuthError | 401, 403 | Missing / expired token, inactive client | | PictureHousePaymentRequiredError | 402 | Image not bought, no active subscription quota | | PictureHouseNotFoundError | 404 | Image / event / lightbox doesn't exist | | PictureHouseRateLimitError | 429 | .retryAfter carries the suggested wait | | PictureHouseServerError | 5xx | API is down — retry with backoff | | PictureHouseError | any | Base class, always safe to instanceof check |

Raw response body is preserved on error.body for debugging.


Advanced

Custom fetch (tracing, retries, mock in tests)

const ph = new PictureHouse({
  baseUrl,
  fetch: async (url, init) => {
    const start = performance.now();
    const res = await fetch(url, init);
    metric("picturehouse.latency_ms", performance.now() - start);
    return res;
  },
});

Per-request timeout

Defaults to 20s, override globally:

new PictureHouse({ baseUrl, timeoutMs: 5_000 });

Calling endpoints not yet wrapped

const data = await ph.client.request<MyShape>({
  method: "GET",
  path: "/some-future-endpoint",
});

Further reading

The README covers the basics. For real storefront work, see docs/:


Versioning

v0.x = pre-locked API. We may break method signatures between releases. Pin to a specific commit until v1.


License

UNLICENSED — internal use only until further notice.