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

basecamp-client

v1.0.8

Published

Type-safe Basecamp API client.

Readme

basecamp-client

TypeScript client for Basecamp 4 with typed requests and responses, automatic pagination, rate-limit retries, and OAuth token refresh.

Installation

npm install basecamp-client
# or
yarn add basecamp-client

Both CommonJS (require) and ESM (import) entry points are included, along with full TypeScript declarations.

Quick start

import { buildClient, getBearerToken } from 'basecamp-client';

const bearerToken = await getBearerToken({
  clientId: process.env.BASECAMP_CLIENT_ID!,
  clientSecret: process.env.BASECAMP_CLIENT_SECRET!,
  refreshToken: process.env.BASECAMP_REFRESH_TOKEN!,
});

const client = buildClient({
  bearerToken,
  accountId: process.env.BASECAMP_ACCOUNT_ID!,
});

const { body: projects } = await client.projects.list({ query: {} });
console.log(projects);

Usage

Create a client

import { buildClient } from 'basecamp-client';

const client = buildClient({
  bearerToken: process.env.BASECAMP_ACCESS_TOKEN!,
  accountId: process.env.BASECAMP_ACCOUNT_ID!,
  userAgent: process.env.BASECAMP_USER_AGENT!, // optional but recommended by Basecamp
});

const { status, body } = await client.projects.list({ query: {} });

buildClient creates a ts-rest client that targets https://3.basecampapi.com/{accountId}. It sets Authorization, Accept, and Content-Type headers automatically and appends .json to every request path.

Refresh OAuth tokens

Basecamp access tokens expire. Use getBearerToken to exchange a refresh token for a fresh access token via the 37signals OAuth endpoint:

import { getBearerToken } from 'basecamp-client';

const bearerToken = await getBearerToken({
  clientId: process.env.BASECAMP_CLIENT_ID!,
  clientSecret: process.env.BASECAMP_CLIENT_SECRET!,
  refreshToken: process.env.BASECAMP_REFRESH_TOKEN!,
  userAgent: process.env.BASECAMP_USER_AGENT!,
});

Iterate through paginated endpoints

Basecamp collection endpoints return paginated results using Link headers. asyncPagedIterator follows those headers automatically:

import { asyncPagedIterator } from 'basecamp-client';

for await (const project of asyncPagedIterator({
  fetchPage: client.projects.list,
  request: { query: {} },
})) {
  console.log(project.name);
}

To collect all pages into a single array:

import { asyncPagedToArray } from 'basecamp-client';

const allProjects = await asyncPagedToArray({
  fetchPage: client.projects.list,
  request: { query: {} },
});

Options:

| Option | Description | | --- | --- | | fetchPage | The client method to call (e.g. client.projects.list). | | request | Arguments forwarded to fetchPage. | | successStatus | Expected HTTP status (default 200). | | extractItems | Custom function to pull items from the response (defaults to using body as an array). | | maxPages | Stop after this many pages. |

Supported resources

The client covers the following Basecamp 4 API resources. Each resource is accessed as a property on the client object (e.g. client.projects, client.todos).

Projects and core

| Client property | Operations | | --- | --- | | projects | list, get, create, update, trash | | people | list, listForProject, listPingable, get, me, updateProjectAccess | | recordings | list, trash, archive, activate | | events | listForRecording | | lineupMarkers | create, update, destroy |

Messages and communication

| Client property | Operations | | --- | --- | | messageBoards | get, listForProject | | messages | list, get, create, update, pin, unpin, trash | | messageTypes | list, get, create, update, destroy | | comments | list, get, create, update, trash | | campfires | list, get, listLines, getLine, createLine, deleteLine | | inboxes | get | | forwards | list, get, trash | | inboxReplies | list, get | | clientCorrespondences | list, get | | clientApprovals | list, get | | clientReplies | list, get | | clientVisibility | update |

To-dos

| Client property | Operations | | --- | --- | | todoSets | get | | todoLists | list, get, create, update, trash | | todoListGroups | list, create, reposition | | todos | list, get, create, update, complete, uncomplete, reposition, trash |

Card tables (Kanban)

| Client property | Operations | | --- | --- | | cardTables | get | | cardTableColumns | get, create, update, move, watch, unwatch, enableOnHold, disableOnHold, updateColor | | cardTableCards | list, get, create, update, move | | cardTableSteps | create, update, setCompletion, reposition |

Scheduling

| Client property | Operations | | --- | --- | | schedules | get, update | | scheduleEntries | list, get, getOccurrence, create, update, trash | | questionnaires | get | | questions | list, get |

Documents and files

| Client property | Operations | | --- | --- | | vaults | list, get, create, update, trash | | documents | list, get, create, update, trash | | uploads | list, get, create, update, trash | | attachments | create |

Conventions

Path parameters

Most resource-scoped endpoints require a bucketId parameter (the Basecamp project ID) along with a resource-specific ID. All IDs are non-negative integers, coerced from path parameters via Zod.

// Fetch a single to-do
const { body: todo } = await client.todos.get({
  params: { bucketId: 12345, todoId: 67890 },
});

Recording-based status changes

Basecamp models many resources (messages, to-dos, documents, etc.) as "recordings". Trashing, archiving, and re-activating use a shared endpoint pattern through the recordings resource:

// Trash any recording
await client.recordings.trash({
  params: { bucketId: 12345, recordingId: 67890 },
});

Individual resources also expose convenience trash operations that map to the same underlying endpoint.

Querying and filtering

Collection endpoints accept query parameters for filtering and sorting:

const { body: todos } = await client.todos.list({
  params: { bucketId: 12345, todolistId: 67890 },
  query: { status: 'active', completed: 'true' },
});

Common query parameters across resources:

| Parameter | Values | | --- | --- | | status | active, archived, trashed | | sort | created_at, updated_at | | direction | asc, desc | | page | Page number (1-indexed) |

Rate-limit handling

The built-in fetcher automatically retries on HTTP 429 responses (up to 20 attempts). It uses linear backoff with jitter and respects the Retry-After header when present. No configuration is needed.

Error handling

The client is configured with throwOnUnknownStatus: true, meaning any response with a status code not declared in the contract will throw. Expected error statuses (404, 507, etc.) are part of the contract and returned as typed discriminated unions:

const response = await client.projects.get({
  params: { projectId: 999 },
});

if (response.status === 404) {
  console.log('Project not found');
} else {
  console.log(response.body.name);
}

The contract

Under the hood, every endpoint in this package is defined as a ts-rest contract. A contract is a declarative description of an API: its routes, path parameters, query parameters, request bodies, and response shapes -- all expressed with Zod schemas. The client you get from buildClient is generated directly from this contract, which is what makes every call fully type-safe with no code generation step.

Because the contract is a plain data structure (not tied to any HTTP library), it can be reused in ways that go beyond making API calls:

  • Custom fetchers -- pass the contract to initClient from @ts-rest/core with your own fetch wrapper (e.g. to add logging, custom auth, or use a different HTTP library).
  • OpenAPI generation -- the package already ships an openapi.json built from the contract. You can regenerate it or use the contract to produce docs, mock servers, or SDK stubs for other languages.
  • Server-side validation -- if you build a Basecamp proxy or middleware, the same Zod schemas that type-check the client can validate incoming requests on the server.
  • Shared types -- import the contract's inferred types into any TypeScript project so that producers and consumers of Basecamp data agree on the same shapes at compile time.
  • Runtime introspection -- because the contract is a plain object with Zod schemas, you can iterate over its routes, inspect parameter and response schemas, or build tooling (e.g. CLI generators, permission auditors) that adapts automatically as the contract grows.

Use the contract directly

import { contract } from 'basecamp-client';
import { initClient } from '@ts-rest/core';

const client = initClient(contract, { /* custom fetcher config */ });

Type exports

The package exports the Client and Contract types, as well as Zod-inferred types for every schema:

import type { Client, Contract } from 'basecamp-client';

Development

npm install

Scripts

  • npm run build -- bundle the package with tsup (CJS + ESM + types).
  • npm run contract:check -- type-check the contract with tsc --noEmit.
  • npm test -- execute the Vitest live smoke suite (requires Basecamp credentials and hits the real API).
  • npm run format / npm run lint / npm run check -- Biome formatting and linting utilities.

Environment variables

Live tests and manual scripts expect the following variables (see tests/utils.ts):

BASECAMP_CLIENT_ID
BASECAMP_CLIENT_SECRET
BASECAMP_REFRESH_TOKEN
BASECAMP_USER_AGENT
BASECAMP_ACCOUNT_ID
BASECAMP_BUCKET_ID

Populate them via .env (ignored from git) or your shell environment before running tests.

Running live tests

The Vitest suite provisions, updates, and trashes real Basecamp resources in a dedicated sandbox project. To execute the tests:

npm test

These tests are not mocked. Ensure your credentials target an account that is safe for automated writes.

Publishing

prepublishOnly runs npm run build, producing artifacts in dist/ via tsup. After verifying the bundle and contract typings, publish as usual:

npm publish

License

MIT