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

@foundryco/sdk

v0.12.0

Published

Official Foundry SDK for Node.js — billing, subscriptions, support tickets, project pipelines, help center content and webhooks.

Downloads

1,011

Readme

Foundry SDK for Node.js

Status: beta — the surface is stable in practice but minor versions may still introduce breaking changes before 1.0.0.

Official SDK for Foundry — billing, subscriptions, support tickets, project pipelines and webhooks for SaaS products.

Install

npm install @foundryco/sdk

Requires Node.js 20+.

Quickstart

import { Foundry } from "@foundryco/sdk";

const foundry = new Foundry({
  apiKey: process.env.FOUNDRY_API_KEY!, // sk_live_... or sk_test_...
});

// Create a customer
const customer = await foundry.customers.create({
  name: "Tienda Don Pepe",
  email: "[email protected]",
  external_owner_id: "user_abc123",
});

// Open a checkout session for a price
const session = await foundry.checkoutSessions.create({
  customer: customer.id,
  price: "price_01h...",
  success_url: "https://recompry.com/billing/success",
  cancel_url: "https://recompry.com/pricing",
});

// session.url is the hosted page where your user pays
console.log(session.url);

Resources

| Resource | Methods | |----------|---------| | customers | list, retrieve, create, update | | products | list, retrieve | | prices | list, retrieve | | checkoutSessions | create, retrieve | | subscriptions | list, retrieve, create, cancel, discounts.list, discounts.apply, discounts.revoke | | entitlements | check | | billingPortalSessions | create | | tickets | list, retrieve, create, updateStatus, timeline, createMessage, deleteMessage, assign | | pipelines.items | list, retrieve, create, update, updateStage, links.*, comments.list, comments.create, tasks.list, tasks.retrieve, tasks.create, files.create | | helpArticles | list, retrieve, create, update, delete | | newsItems | list, retrieve, create, update, delete | | webhookEndpoints | list, retrieve, create, update, delete |

Pipelines

Manage the console's Pipelines module from your product. Three boards, all driven by the same resource:

| Pipeline | Stages | |----------|--------| | proyectos | solicitud → cotizacion → propuesta → aprobado → en_desarrollo → revision → entregado → facturacion → finalizado → cancelado | | onboarding | lead → demo → kickoff → setup → migration → training → go_live → support | | roadmap | backlog → planeado → en_progreso → qa → canary → produccion → bloqueado → archivado |

import { Foundry, PIPELINE_STAGES } from "@foundryco/sdk";

const foundry = new Foundry({ apiKey: process.env.FOUNDRY_API_KEY! });

// A new subscription kicks off an onboarding
const item = await foundry.pipelines.items.create({
  pipeline: "onboarding",
  name: `Onboarding ${customer.name}`,
  client_org: customer.name,
  client_email: customer.email,
  customer_id: customer.id,
  source: "api",
});

// Link the objects driving this onboarding
await foundry.pipelines.items.links.add(item.id, { kind: "subscription", target_id: subscription.id });
await foundry.pipelines.items.links.add(item.id, { kind: "user", target_id: appUserId, label: "Owner" });

// Move it through the board as setup progresses
await foundry.pipelines.items.updateStage(item.id, { stage: "setup", note: "Cuenta aprovisionada" });

// Read one board column
const inTraining = await foundry.pipelines.items.list({ pipeline: "onboarding", stage: "training" });

// PIPELINE_STAGES.roadmap → ["backlog", "planeado", ..., "archivado"]
  • stage must belong to the item's pipeline (422 otherwise); the pipeline is immutable after creation.
  • Links are unique per (item, kind, target_id) — adding a duplicate returns 409.
  • Webhooks: subscribe to project.created / project.updated to react to board movements.

Comments, tasks and files on an item

Each item carries the client-facing thread, work plan and deliverables:

// Client-visible thread, oldest → newest. Team-only notes are never returned.
const thread = await foundry.pipelines.items.comments.list(item.id);

const comment = await foundry.pipelines.items.comments.create(item.id, {
  body: "Entregamos el primer avance.",
  author: "Equipo Recompry",
});

// With an attachment — sent as multipart
await foundry.pipelines.items.comments.create(item.id, {
  file: new File([buffer], "avance.pdf", { type: "application/pdf" }),
  body: "Adjunto el informe.",
});

// Work plan
const tasks = await foundry.pipelines.items.tasks.list(item.id);
const task = await foundry.pipelines.items.tasks.create(item.id, {
  title: "Migrar catálogo",
  due_at: "2026-08-15",
});
const detail = await foundry.pipelines.items.tasks.retrieve(item.id, task.id);
detail.files;    // deliverables attached to the task
detail.comments; // notes shared with the client

// Standalone deliverable
await foundry.pipelines.items.files.create(item.id, {
  file: new File([buffer], "entregable.zip", { type: "application/zip" }),
});

Comment length: body is capped at 5000 characters after trimming, the same cap as ticket messages, exported as PIPELINE_COMMENT_BODY_MAX_LENGTH. The SDK throws a TypeError before the request when you exceed it.

Comments are always the client's. Unlike tickets.createMessage, there is no role / on_behalf_of: everything posted through the API is recorded as client-authored and client-visible. Internal notes stay in the console.

Task visibility (0.12.0+): tasks are created as visibility: "client" so they show up in list and retrieve. Passing visibility: "team" creates an internal task that the API can no longer read back — retrieve will answer 404, by design.

Attachments: max 25 MB; extensions pdf, doc(x), ppt(x), xls(x), csv, txt, zip, png, jpg, jpeg, webp, svg, gif, avif. Download urls are signed and expire. Comment and task lists come back complete — these endpoints are not paginated, so there is no has_more / next_cursor.

Billing portal (Stripe-style)

Mint a hosted self-service URL for the customer:

const session = await foundry.billingPortalSessions.create({
  customer: "cus_...",
  return_url: "https://yourapp.com/account",
});

// session.url expires in 24h. Redirect the customer to it.

The hosted page lets the customer see their subscription, download invoices (PDF/XML, including Colombia DIAN when applicable), and return to your app.

Support tickets

Tickets reported from the support widget (or created programmatically) are readable and manageable from the SDK:

// List tickets (a product-scoped key only sees its product's tickets)
const page = await foundry.tickets.list({ status: "abierto" });

// Fetch one ticket + its client-visible timeline (messages + events, oldest → newest)
const ticket = await foundry.tickets.retrieve("tick_...");
const timeline = await foundry.tickets.timeline("tick_...");
ticket.assignee; // { id, email } | null

// Change status
await foundry.tickets.updateStatus("tick_...", { status: "resuelto" });

// Assign / reassign to a workspace member by email (or unassign with null)
await foundry.tickets.assign("tick_...", { assignee: "[email protected]" });

// Reply as the customer-facing team, on behalf of a specific member.
// WITHOUT role/on_behalf_of the message is attributed to the CLIENT — that default
// exists because the main use case is relaying your own end user's message.
const reply = await foundry.tickets.createMessage("tick_...", {
  body: "Ya está corregido, prueba de nuevo.",
  role: "team",
  on_behalf_of: "[email protected]",
});

// Published something by mistake? Retract it from the requester's thread.
// Not a hard delete: it becomes a team-only internal note with an audit event.
await foundry.tickets.deleteMessage("tick_...", reply.id);

Scoping (0.7.0+): a product-scoped key is confined to its own product's tickets across every ticket endpoint. timeline returns client-visible entries only — internal team notes stay in the console. assignee/on_behalf_of must be the email of an active workspace member.

Message length: body is capped at 5000 characters, measured after trimming and in UTF-16 code units (an emoji costs 2). The cap is exported as TICKET_MESSAGE_BODY_MAX_LENGTH so long notes can be chunked deterministically; createMessage throws a TypeError naming the field and the actual length rather than letting the API answer with a generic 422. Split notes keep their order in the timeline.

Reading internal notes: there is no read endpoint for messages — POST .../messages is the only verb on that path (plus DELETE on a single message), so GET answers 405 by design. A note sent with visibility: "team" is not returned by timeline either, which is client-visible only. The 201 response (its id + visibility) is the confirmation that it was stored; to read it back, use the console.

Help center & news (support widget)

The content behind the widget's Ayuda y novedades tabs — help articles and release announcements, with optional cover images — is fully manageable via the SDK:

import { readFile } from "node:fs/promises";

// Publish a help article with an uploaded cover (stored as a private asset,
// served through signed URLs). `product` is required with workspace keys.
const article = await foundry.helpArticles.create({
  product: "prod_...",
  title: "Cómo conectar tu cuenta",
  body: "Ve a Ajustes → Integraciones y sigue los pasos.",
  url: "https://docs.miapp.com/integraciones",
  image: { data: await readFile("cover.png"), content_type: "image/png" },
});
article.image_url; // signed URL, ready to render

// Publish a news item (Novedades tab) with a date and a YouTube video card
const news = await foundry.newsItems.create({
  product: "prod_...",
  title: "Lanzamos reportes v2",
  body: "Nuevos dashboards y export a Excel.",
  date: "2026-07-17",
  video_url: "https://www.youtube.com/watch?v=...",
  image_url: "https://cdn.miapp.com/banners/reportes-v2.png", // externally hosted cover
});

// List, edit, remove
const { data } = await foundry.newsItems.list({ product: "prod_..." });
await foundry.helpArticles.update(article.id, { title: "Conectar tu cuenta (2 min)" });
await foundry.helpArticles.update(article.id, { image: null }); // remove the cover
await foundry.newsItems.delete(news.id);

Items belong to a product's widget install: the product must have a widget key (Console → Settings → Widget de soporte) or create responds 409 widget_not_configured. Each tab holds at most 30 items per product (409 content_limit_reached). Covers: max 2 MB; content_type must be one of image/png, image/jpeg, image/webp, image/gif, image/svg+xml, image/avif. Pass raw bytes/base64 via image or an https image_url — never both. Webhooks: help_article.created|updated|deleted, news_item.created|updated|deleted.

API key scopes

Keys are issued per workspace and can optionally be scoped to a single product. The SDK transparently honors the scope — same code, the server filters responses:

  • Workspace-wide key (default): full access to every product, plan, price, subscription and checkout session in the workspace.
  • Product-scoped key: products, prices, subscriptions, checkoutSessions, tickets, helpArticles, newsItems and — since 0.12.0pipelines.items only see resources for that product. Creating a checkout session against a price that belongs to a different product returns a validation error.

customers, invoices and webhookEndpoints remain workspace-wide regardless of scope — contacts and billing records are shared, and webhooks are workspace-level by design.

Changed in 0.12.0: pipeline items used to ignore the product scope, so a key pinned to product A listed every project in the workspace. They now behave like tickets: items of another product answer 404, and an item created with a product-scoped key belongs to that product instead of the workspace default. Sub-resources (comments, tasks, files, links) inherit the scope from their item.

A key never reaches another workspace: every resource, including children addressed by raw id, is resolved against the key's workspace first.

Generate keys (and pick a product scope) under Settings → API keys in the console.

Webhooks

Verify and parse webhook deliveries from Foundry:

import { Foundry, WebhookSignatureError } from "@foundryco/sdk";

const foundry = new Foundry({ apiKey: process.env.FOUNDRY_API_KEY! });

// In your HTTP handler, with the raw request body:
try {
  const event = await foundry.webhooks.constructEvent(
    rawBody,
    request.headers["foundry-signature"],
    process.env.FOUNDRY_WEBHOOK_SECRET!, // whsec_...
  );

  switch (event.type) {
    case "subscription.created":
      // event.data.object is the Subscription resource
      break;
    case "checkout_session.completed":
      // event.data.object is the CheckoutSession resource
      break;
    // ...
  }
} catch (err) {
  if (err instanceof WebhookSignatureError) {
    return new Response("invalid signature", { status: 401 });
  }
  throw err;
}

Errors

All API errors inherit from FoundryAPIError and carry status, code, message, requestId:

import {
  AuthenticationError,
  NotFoundError,
  RateLimitError,
  ValidationError,
} from "@foundryco/sdk";

try {
  await foundry.customers.retrieve("cus_doesnotexist");
} catch (err) {
  if (err instanceof NotFoundError) {
    // 404 — resource doesn't exist or doesn't belong to your workspace
  } else if (err instanceof RateLimitError) {
    // 429 — back off (err.retryAfter has the seconds, when present)
  } else if (err instanceof ValidationError) {
    // 422 — body/query failed validation; err.details has per-field info
  }
}

Configuration

Server-side only. The secret key (sk_...) is a bearer credential — never expose it in browser or mobile bundles. Load it from an environment variable, not a string literal. The SDK requires an https:// base URL (localhost excepted) and warns if instantiated in a browser.

const foundry = new Foundry({
  apiKey: process.env.FOUNDRY_API_KEY!, // sk_live_... / sk_test_... — from env, never hardcoded
  baseURL: "https://api.foundry.greenstudio.vc", // default
  timeout: 30_000,    // ms, default 30s
  maxRetries: 3,      // default — only 408/429/5xx
  fetch: customFetch, // optional — defaults to globalThis.fetch
});

Mutating calls (POST/PATCH/DELETE) get an auto-generated Idempotency-Key when you don't supply one, so a retried request can't create a duplicate resource.

Per-request:

await foundry.customers.create(
  { name: "..." },
  {
    idempotencyKey: crypto.randomUUID(),
    signal: abortController.signal,
  },
);

Modes

API keys carry their environment in the prefix:

  • sk_live_... → operates against live customers and real charges
  • sk_test_... → operates against test data; charges go to Wompi sandbox

The same SDK code works for both — switch by setting a different apiKey.

License

MIT — see LICENSE.