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

@sumhubs/sdk

v0.8.0

Published

Node.js SDK for the SumHubs Open API — typed hub client plus React components

Readme

@sumhubs/sdk

Node.js SDK for the SumHubs Open API. Initialise it with a hub API key, then read and write that hub's content, opportunities, forums, members and questionnaires. Ships optional React components for the common screens.

Full API reference: /developers · Markdown for agents: /llms.txt

Install

npm install @sumhubs/sdk

Node 18+ (uses the built-in fetch). React 18+ only if you use the /react entrypoint.

The one rule: the key is a server-side secret

An API key grants read and write over an entire hub, including members and questionnaire responses. It is not scoped to one visitor, and there is no way to lock it to a domain.

Never let it reach the browser. The client throws if it detects a browser environment rather than letting a key leak into a bundle:

createClient must not run in a browser: a SumHubs API key is a server-side secret.

Fetch on the server, pass plain data to your components, and submit through a route handler or server action. That is exactly how the React components in this package are designed to be used.

Quick start

import { createClient } from '@sumhubs/sdk';

const sumhubs = createClient({ apiKey: process.env.SUMHUBS_API_KEY! });

const hub = await sumhubs.hub.current();   // resolved from the key, no id needed
const { items } = await sumhubs.content.list({ limit: 20 });

Point it at a local backend with baseUrl:

createClient({ apiKey: process.env.SUMHUBS_API_KEY!, baseUrl: 'http://localhost:8080/api/v1' });

Options

| Option | Default | Notes | | --- | --- | --- | | apiKey | — | Required. Must start with sh_. | | baseUrl | https://api.sumhubs.com/api/v1 | For local or self-hosted backends. | | timeoutMs | 30000 | Per-request timeout. | | headers | {} | Extra headers on every request. | | fetch | global fetch | Injectable for tests. |

Resources

sumhubs.hub.current()                          // the hub this key belongs to
sumhubs.hub.stats()                            // content / opportunity / response counts

sumhubs.content.list({ page, limit, name })    // paginated
sumhubs.content.create({ title, description, article })
sumhubs.content.update(id, { title })
sumhubs.content.delete(id)

sumhubs.opportunities.list()
sumhubs.opportunities.get(id)
sumhubs.opportunities.create({ title })
sumhubs.opportunities.apply(id, { email })     // see below
sumhubs.opportunities.update(id, {})
sumhubs.opportunities.delete(id)

sumhubs.forums.list()
sumhubs.forums.get(id)
sumhubs.forums.create({ name, article })

sumhubs.members.list({ search, role, sort })   // always scoped to the key's hub
sumhubs.members.get(id)

sumhubs.questionnaires.list()
sumhubs.questionnaires.listWithCounts()        // adds questionsCount
sumhubs.questionnaires.get(id)                 // includes questions + options
sumhubs.questionnaires.create({ title, description, language })
sumhubs.questionnaires.addQuestion({ quizId, title, language, questionOrder, type })
sumhubs.questionnaires.listResults()           // personal data
sumhubs.questionnaires.listRespondents(quizId) // personal data
sumhubs.questionnaires.listAnswers()           // personal data

Anything not wrapped yet is reachable through the raw client:

await sumhubs.http.get('/some/new/endpoint', { query: { page: 1 } });

Pagination

List methods return { items, meta }. To walk everything without tracking page numbers:

for await (const member of sumhubs.members.iterate({ limit: 100 })) {
  console.log(member.user?.email);
}

Available on content, opportunities and members.

Applying to an opportunity

opportunities.apply() takes an email rather than a user id, so applicants need no SumHubs account first. The backend matches the email against existing users and creates an invited user when it is unknown, then adds them to the hub and records the application.

const { user, application } = await sumhubs.opportunities.apply(42, {
  email: '[email protected]',
  firstName: 'Ada',
  lastName: 'Lovelace',
  phone: '+976 9900 1122',
  detail: 'Available from March.',
});

user.isNew; // true if this call created the person, false if it matched an existing user

Applying twice with the same email throws SumHubsValidationError (409).

Errors

Every failure is a SumHubsError subclass, so you can branch on cause rather than status codes:

| Class | When | | --- | --- | | SumHubsAuthError | 401 — key unknown, deleted, expired, owner lost their hub role, or the endpoint does not accept keys | | SumHubsScopeError | 403 — key lacks the read or write scope | | SumHubsNotFoundError | 404 — no such record in this hub (also what another hub's ids return) | | SumHubsValidationError | 400/409 — missing or rejected field, duplicate application | | SumHubsError | anything else, including timeouts (status: 0) |

import { SumHubsScopeError } from '@sumhubs/sdk';

try {
  await sumhubs.content.create({ title: 'a', description: 'b', article: '<p>c</p>' });
} catch (error) {
  if (error instanceof SumHubsScopeError) {
    // read-only key
  }
}

React components

import { QuestionnaireForm, OpportunityBoard, OpportunityApplyForm, ContentList, ContentArticle }
  from '@sumhubs/sdk/react';

All five are presentational: they take data as props and hand submissions back through a callback. None of them call the API, which is what keeps the key server-side. They carry 'use client', so they drop straight into the Next App Router. Styling is inline with no CSS import — override via the theme prop or restyle with className.

| Component | Props | | --- | --- | | QuestionnaireForm | questionnaire, onSubmit(answers) | | OpportunityBoard | opportunities, hrefFor? / onSelect? | | OpportunityApplyForm | opportunity, onApply(input) | | ContentList | items, hrefFor? / onSelect? | | ContentArticle | content, sanitize |

QuestionnaireForm renders the right control for each of the platform's seven question types (choose_one, multiple_choice, true_false, scale_1_5, number, short_answer, long_answer), enforces isRequired, and returns answers keyed by question id.

ContentArticle requires a sanitize function on purpose. content.article is HTML written by hub editors, so rendering it raw would let a compromised editor account run script on your page:

import DOMPurify from 'dompurify';

<ContentArticle content={content} sanitize={(html) => DOMPurify.sanitize(html)} />

Pass (html) => html only if you fully trust every editor in the hub.

Wiring it up (Next App Router)

// app/opportunities/actions.ts
'use server';
import { createClient } from '@sumhubs/sdk';
import type { ApplyInput } from '@sumhubs/sdk';

const sumhubs = createClient({ apiKey: process.env.SUMHUBS_API_KEY! });

export async function apply(opportunityId: number, input: ApplyInput) {
  const { user } = await sumhubs.opportunities.apply(opportunityId, input);
  return { isNewUser: user.isNew };
}
// app/opportunities/[id]/page.tsx
import { createClient } from '@sumhubs/sdk';
import { OpportunityApplyForm } from '@sumhubs/sdk/react';
import { apply } from '../actions';

const sumhubs = createClient({ apiKey: process.env.SUMHUBS_API_KEY! });

export default async function Page({ params }: { params: { id: string } }) {
  const opportunity = await sumhubs.opportunities.get(Number(params.id));
  return (
    <OpportunityApplyForm
      opportunity={opportunity}
      onApply={async (input) => { await apply(opportunity.id, input); }}
    />
  );
}

Runnable versions are in examples/.

Scopes

A key carries read, write, or both. Grant the minimum an integration needs — write does not imply read. Questionnaire results and submitted answers are reachable with only read, so treat read keys for those integrations as sensitive and prefer an expiry date for one-off exports.

Development

npm install
npm run typecheck
npm run build