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

sound-tank

v2.3.1

Published

A library for interacting with the Reverb Marketplace API

Readme


Quick Start

npm install sound-tank
# or
yarn add sound-tank
# or
pnpm add sound-tank
import Reverb from 'sound-tank';

const reverb = new Reverb({ apiKey: process.env.REVERB_API_KEY });
const { data } = await reverb.listings.getMy({ perPage: 10, state: 'live' });

data.listings.forEach((listing) => {
  console.log(`${listing.title}: ${listing.price.display}`);
});

Features

  • Full TypeScript Support - Complete type definitions for all API entities
  • Automatic Pagination - Built-in helpers to fetch all results seamlessly
  • Streaming Pagination - Async generator to stream results page-by-page
  • Configuration Management - Easy setup for currency, locale, and shipping preferences
  • Comprehensive Coverage - Listings, orders, negotiations, messages, catalog data, and arbitrary endpoint access
  • Response Caching - Optional TTL-based cache for GET requests
  • HTTP Client Abstraction - Clean architecture with testable mock implementations
  • Dual Module Support - Both CommonJS and ESM builds included
  • Well Tested - Extensive unit and integration test coverage
  • Zero Dependencies - Uses the native fetch API; no runtime dependencies

Table of Contents

Installation

Install via your preferred package manager:

npm install sound-tank
# or
yarn add sound-tank
# or
pnpm add sound-tank

Getting a Reverb API Key

  1. Visit Reverb API Settings
  2. Generate a new API token
  3. Set the appropriate scopes for your use case
  4. Store securely in environment variables
# .env
REVERB_API_KEY=your_api_key_here

You can use the provided .env.example file as a template:

cp .env.example .env
# Edit .env and add your REVERB_API_KEY

Getting Started

Initialize the Client

import Reverb from 'sound-tank';

const reverb = new Reverb({
  apiKey: process.env.REVERB_API_KEY,
  displayCurrency: 'USD', // optional
  locale: 'en', // optional
  version: '3.0', // optional
  cache: { ttlMs: 60_000 }, // optional: cache GET responses for 60s
});

Fetch Your Listings

const response = await reverb.listings.getMy({
  perPage: 25,
  page: 1,
  state: 'live',
  query: 'Gibson Les Paul',
});

console.log(response.data.listings);

Error Handling

try {
  const response = await reverb.listings.getMy({ state: 'live' });
  console.log(`Found ${response.data.listings.length} listings`);
} catch (error) {
  console.error('Failed to fetch listings:', error.message);
}

Configuration

Constructor Options

| Option | Type | Required | Default | Description | | ----------------- | -------- | -------- | ------------------------------ | ---------------------------------------- | | apiKey | string | ✅ Yes | - | Your Reverb API key | | version | string | No | '3.0' | API version to use | | rootEndpoint | string | No | 'https://api.reverb.com/api' | API base URL | | displayCurrency | string | No | 'USD' | Currency for price display | | locale | string | No | 'en' | Language locale (e.g., 'en', 'fr', 'de') | | shippingRegion | string | No | undefined | Shipping region code (e.g., 'US', 'EU') | | cache | ReverbCacheOptions | No | undefined | Enable TTL response cache: { ttlMs: number } |

Runtime Configuration

You can update configuration after initialization using setters:

reverb.displayCurrency = 'EUR';
reverb.locale = 'fr';
reverb.shippingRegion = 'FR';
reverb.version = '3.0';

These changes automatically update the internal headers and configuration for subsequent API requests.

API Methods

listings

listings.getMy(options?)

Fetch a paginated list of your listings.

Parameters:

  • perPage?: number - Items per page
  • page?: number - Page number (starts at 1)
  • query?: string - Search query to filter listings
  • state?: string - Filter by state: 'live', 'sold', 'draft', or 'all'
const response = await reverb.listings.getMy({ perPage: 50, state: 'live' });
const { listings } = response.data;

listings.getAllMy(options?)

Automatically fetches all listings across all pages using automatic pagination.

Parameters:

  • query?: string - Search query to filter listings
  • state?: ListingStates - Filter by state: ListingStates.LIVE, ListingStates.SOLD, or ListingStates.DRAFT

Returns: Promise<HttpResponse<Listing[]>>

const response = await reverb.listings.getAllMy({ state: 'live' });
const allListings = response.data; // All listings from all pages

Note: Fetches all pages sequentially, throttling every 5 pages to respect rate limits.


listings.streamAllMy(options?)

Stream all listings as an async generator — yields one Listing at a time without waiting for all pages to load.

Parameters: Same as getAllMy.

Returns: AsyncGenerator<Listing>

for await (const listing of reverb.listings.streamAllMy({ state: 'live' })) {
  console.log(listing.title);
}

listings.getOne(options)

Fetch a single listing by ID.

Parameters:

  • id: string - Listing ID (required)

Returns: Promise<HttpResponse<Listing>>

const response = await reverb.listings.getOne({ id: '12345' });
console.log(response.data.title);

listings.getPhotos(options)

Fetch full-resolution photo URLs for a listing.

Parameters:

  • id: string - Listing ID (required)

Returns: Promise<string[]>

const photos = await reverb.listings.getPhotos({ id: '12345' });
photos.forEach((url) => console.log(url));

listings.create(body)

Create a new listing.

Parameters:

  • body: ListingPostBody - Listing data (title, make, model, price, condition, etc.)

Returns: Promise<HttpResponse<Listing>>

const response = await reverb.listings.create({
  make: 'Fender',
  model: 'Stratocaster',
  title: 'Fender Stratocaster 1965',
  price: { amount: '1500.00', currency: 'USD' },
  condition: { uuid: 'f7a3f48c-972a-44c6-b01a-1d9dd5ca8879' }, // Excellent
  description: 'Great condition vintage Strat.',
  categories: [{ uuid: '4-electric-guitars' }],
  shipping: { us: { rate: '30.00', currency: 'USD' } },
});

listings.update(id, body)

Update an existing listing.

Parameters:

  • id: string - Listing ID
  • body: ListingUpdateBody - Fields to update (any subset of listing fields)

Returns: Promise<HttpResponse<Listing>>

await reverb.listings.update('12345', { price: { amount: '1200.00', currency: 'USD' } });

listings.publish(id)

Publish a draft listing (sets publish: true).

Parameters:

  • id: string - Listing ID

Returns: Promise<HttpResponse<Listing>>

await reverb.listings.publish('12345');

listings.end(id, reason)

End an active listing.

Parameters:

  • id: string - Listing ID
  • reason: EndListingReason - Reason for ending (e.g., 'sold_elsewhere', 'not_selling')

Returns: Promise<HttpResponse<Listing>>

await reverb.listings.end('12345', 'sold_elsewhere');

listings.delete(id)

Permanently delete a listing.

Parameters:

  • id: string - Listing ID

Returns: Promise<HttpResponse<void>>

await reverb.listings.delete('12345');

listings.getDrafts(options?)

Fetch a paginated page of draft listings.

Parameters:

  • perPage?: number
  • page?: number
const response = await reverb.listings.getDrafts({ perPage: 50 });

listings.getAllDrafts(options?)

Fetch all draft listings across all pages.

Returns: Promise<HttpResponse<Listing[]>>

const response = await reverb.listings.getAllDrafts();
const drafts = response.data;

listings.streamAllDrafts(options?)

Stream all draft listings as an async generator.

Returns: AsyncGenerator<Listing>

for await (const draft of reverb.listings.streamAllDrafts()) {
  console.log(draft.title);
}

listings.getImages(id)

Fetch all images attached to a listing.

Parameters:

  • id: string - Listing ID

Returns: Promise<HttpResponse<{ photos: ListingImage[] }>>

const response = await reverb.listings.getImages('12345');
response.data.photos.forEach((photo) => console.log(photo._links.full.href));

listings.deletePhoto(id, imageId)

Delete a photo from a listing.

Parameters:

  • id: string - Listing ID
  • imageId: string - Photo ID
await reverb.listings.deletePhoto('12345', 'photo-id');

listings.reorderPhotos(id, photoUrls)

Set the display order of a listing's photos.

Parameters:

  • id: string - Listing ID
  • photoUrls: string[] - Ordered array of photo URLs
await reverb.listings.reorderPhotos('12345', [url1, url2, url3]);

orders

orders.getMy(options?)

Fetch your orders with pagination.

Parameters:

  • page?: number - Page number (starts at 1)
  • perPage?: number - Items per page
const response = await reverb.orders.getMy({ page: 1, perPage: 25 });
const { orders } = response.data;
orders.forEach((order) => console.log(`Order ${order.order_number}: ${order.status}`));

negotiations

negotiations.getNegotiations(options)

Fetch your active offers/negotiations.

Parameters:

  • page?: number
  • perPage?: number
  • status?: 'active' | 'active_for_seller' | 'all'
  • negotiation_type?: 'standard' | 'auto_push_offer'

Returns: Promise<HttpResponse<PaginatedReverbResponse<{ listings: ListingWithNegotiations[] }>>>

const response = await reverb.negotiations.getNegotiations({ status: 'active' });
const { listings } = response.data;
listings.forEach((l) => console.log(`${l.title}: ${l.negotiations.length} offers`));

negotiations.getNegotiation(offerId)

Fetch a single offer by ID.

Parameters:

  • offerId: string - Offer ID

Returns: Promise<HttpResponse<Negotiation>>

const response = await reverb.negotiations.getNegotiation('offer-id-here');
console.log(response.data);

messages

messages.getMy(options?)

Fetch your conversations (messages).

Parameters:

  • unread_only?: boolean - Filter to unread conversations only
const response = await reverb.messages.getMy({ unread_only: true });

messages.getById(messageId)

Fetch a single conversation by ID.

Parameters:

  • messageId: number - Conversation ID
const response = await reverb.messages.getById(12345);

messages.markAsRead(messageId)

Mark a conversation as read.

Parameters:

  • messageId: number - Conversation ID
await reverb.messages.markAsRead(12345);

messages.reply(messageId, replyBody)

Reply to a conversation.

Parameters:

  • messageId: number - Conversation ID
  • replyBody: string - Message text
await reverb.messages.reply(12345, 'Thanks for your offer!');

catalog

catalog.getCategories()

Fetch all Reverb listing categories.

const response = await reverb.catalog.getCategories();

catalog.getConditions()

Fetch all item condition options (UUIDs and display names).

const response = await reverb.catalog.getConditions();

catalog.getShippingRegions()

Fetch all supported shipping regions.

const response = await reverb.catalog.getShippingRegions();

catalog.getCurrencies()

Fetch all supported listing currencies.

const response = await reverb.catalog.getCurrencies();

_getArbitraryEndpoint(url, params?)

Escape hatch to call any Reverb endpoint not yet wrapped by a resource. The _ prefix indicates this is not part of the stable public API but is intentionally supported.

Parameters:

  • url: string - Endpoint URL (absolute or relative to root endpoint)
  • params?: object - Query parameters
const categories = await reverb._getArbitraryEndpoint('/categories/flat');
const conditions = await reverb._getArbitraryEndpoint('/listing_conditions');

TypeScript Usage

Sound Tank is written in TypeScript and provides comprehensive type definitions for the entire Reverb API.

Importing Types

import Reverb, {
  Listing,
  Order,
  Negotiation,
  ListingWithNegotiations,
  Price,
  ListingStates,
  ListingCondition,
  ListingImage,
  ListingPostBody,
  ListingUpdateBody,
  EndListingReason,
  ShippingRate,
  Category,
  ReverbShippingRegion,
  ListingCurrency,
  ReverbOptions,
  ReverbCacheOptions,
} from 'sound-tank';

Working with Typed Responses

const response = await reverb.listings.getMy();
const listings: Listing[] = response.data.listings;

listings.forEach((listing: Listing) => {
  const price: Price = listing.price;
  const condition: ListingCondition = listing.condition;

  console.log(`${listing.title}`);
  console.log(`  Price: ${price.display} (${price.currency})`);
  console.log(`  Condition: ${condition.display_name}`);
  console.log(`  Year: ${listing.year}`);
});

Available Types

Listing Types:

  • Listing - Complete listing data with make, model, price, condition, shipping, etc.
  • ListingState - Listing status information
  • ListingStates - Enum for state values (LIVE, SOLD, DRAFT)
  • ListingCondition - Item condition with UUID and display name
  • ListingShipping - Shipping information and rates
  • ListingImage - Photo attached to a listing (includes _links.full.href)
  • ListingStats - View and watch counts
  • ListingPostBody - Body for creating a listing
  • ListingUpdateBody - Body for updating a listing
  • EndListingReason - Valid reasons for ending a listing

Order Types:

  • Order - Complete order information with buyer, seller, shipping, pricing details
  • OrderStatus - Order status information
  • ShippingAddress - Complete address information

Negotiation Types:

  • Negotiation - Individual offer/negotiation details
  • ListingWithNegotiations - Listing with attached negotiations array

Pricing Types:

  • Price - Currency-aware price with amount, currency, symbol, and formatted display
  • ShippingRate - Regional shipping costs

Catalog Types:

  • ReverbShippingRegion - Shipping region returned by catalog.getShippingRegions()
  • ListingCurrency - Currency option returned by catalog.getCurrencies()

Other Types:

  • Category - Product categorization
  • Link - HATEOAS navigation links
  • PaginatedReverbResponse<T> - Paginated API response wrapper
  • ReverbOptions - SDK configuration options
  • ReverbCacheOptions - Cache configuration: { ttlMs: number }

Advanced Features

Automatic Pagination

// Manually paginate
let page = 1;
let allListings = [];
let response;

do {
  response = await reverb.listings.getMy({ page, perPage: 50 });
  allListings = allListings.concat(response.data.listings);
  page++;
} while (response.data.listings.length === 50);

// Or use the built-in helper
const autoResponse = await reverb.listings.getAllMy();
const listings = autoResponse.data; // Same result, simpler code

Streaming Pagination

Stream results without waiting for all pages to complete:

let count = 0;
for await (const listing of reverb.listings.streamAllMy({ state: 'live' })) {
  count++;
  console.log(`[${count}] ${listing.title}`);
}

Response Caching

Cache repeated GET requests with a TTL to avoid hitting rate limits:

const reverb = new Reverb({
  apiKey: process.env.REVERB_API_KEY,
  cache: { ttlMs: 60_000 }, // cache for 60 seconds
});

// These two calls hit the network only once
await reverb.catalog.getCategories();
await reverb.catalog.getCategories(); // served from cache

HTTP Client Abstraction

Sound Tank uses a fetch-based HTTP client with an interface that can be swapped for testing:

import { MockHttpClient } from 'sound-tank/http';

// Use mock client for testing
const mockClient = new MockHttpClient();

Configuration Access

const config = reverb.config;
console.log(config.rootEndpoint); // 'https://api.reverb.com/api'
console.log(config.displayCurrency); // 'USD'
console.log(config.locale); // 'en'

const headers = reverb.headers;
console.log(headers['Authorization']); // 'Bearer your_api_key'
console.log(headers['X-Display-Currency']); // 'USD'

Examples

Find All Guitars Under $1000

const reverb = new Reverb({ apiKey: process.env.REVERB_API_KEY });

const response = await reverb.listings.getAllMy({ query: 'guitar' });
const affordable = response.data.filter(
  (listing) => listing.price.amount_cents < 100000, // $1000 = 100,000 cents
);

console.log(`Found ${affordable.length} guitars under $1000`);

Multi-Currency Price Display

reverb.displayCurrency = 'USD';
const usdResponse = await reverb.listings.getMy({ perPage: 5 });

reverb.displayCurrency = 'EUR';
const eurResponse = await reverb.listings.getMy({ perPage: 5 });

Export Listings to CSV

const response = await reverb.listings.getAllMy({ state: 'live' });

const csvHeader = 'ID,Title,Price,Currency,Condition,Year,State\n';
const csvRows = response.data
  .map(
    (listing) =>
      `${listing.id},"${listing.title}",${listing.price.amount},${listing.price.currency},${listing.condition.display_name},${listing.year},${listing.state.slug}`,
  )
  .join('\n');

console.log(csvHeader + csvRows);

Respond to All Unread Messages

const response = await reverb.messages.getMy({ unread_only: true });

for (const conversation of response.data.conversations) {
  await reverb.messages.reply(conversation.id, 'Thanks for reaching out!');
  await reverb.messages.markAsRead(conversation.id);
}

Review Active Offers

const response = await reverb.negotiations.getNegotiations({ status: 'active_for_seller' });

for (const listing of response.data.listings) {
  console.log(`${listing.title}: ${listing.negotiations.length} pending offer(s)`);
}

Development

Prerequisites

  • Node.js 18 or higher
  • Yarn package manager

Setup

# Clone the repository
git clone https://github.com/ZacharyEggert/sound-tank.git
cd sound-tank

# Install dependencies
yarn install

# Copy environment template
cp .env.example .env
# Edit .env and add your REVERB_API_KEY

Project Structure

sound-tank/
├── src/
│   ├── Reverb.ts           # Main SDK class
│   ├── index.ts            # Entry point
│   ├── types.ts            # TypeScript type definitions
│   ├── config/
│   │   └── ReverbConfig.ts # Configuration management
│   ├── http/
│   │   ├── HttpClient.ts         # HTTP client interface
│   │   ├── FetchHttpClient.ts    # Native fetch implementation
│   │   └── MockHttpClient.ts     # Mock for testing
│   ├── methods/
│   │   ├── listings/       # getListings, postListing, updateListing, endListing, listingImages
│   │   ├── orders/         # getOrders
│   │   ├── negotiations/   # getNegotiations
│   │   ├── messages/       # getMessages, postMessages
│   │   └── catalog/        # getCatalog (categories, conditions, shippingRegions, currencies)
│   ├── resources/
│   │   ├── ListingsResource.ts      # getMy, getOne, getPhotos, getAllMy, streamAllMy, getDrafts, getAllDrafts, streamAllDrafts, create, update, publish, end, delete, getImages, deletePhoto, reorderPhotos
│   │   ├── OrdersResource.ts        # getMy
│   │   ├── NegotiationsResource.ts  # getNegotiations, getNegotiation
│   │   ├── MessagesResource.ts      # getMy, getById, markAsRead, reply
│   │   └── CatalogResource.ts       # getCategories, getConditions, getShippingRegions, getCurrencies
│   └── utils/              # pagination, urlBuilder, queryBuilder, logger, cache
├── tests/                  # Test files
├── dist/                   # Build output (git-ignored)
├── package.json
├── tsconfig.json
├── tsup.config.ts
└── vite.config.mts

Debugging

Set SOUNDTANK_LOG_LEVEL to enable SDK logging:

SOUNDTANK_LOG_LEVEL=DEBUG yarn dev

Valid values: ERROR | WARN | INFO | DEBUG | TRACE. Silent by default.

Available Scripts

| Command | Description | | -------------- | --------------------------------------------------- | | yarn dev | Run tests in watch mode during development | | yarn test | Run all tests once | | yarn build | Build production bundles (CJS + ESM + declarations) | | yarn lint | Type-check code with TypeScript compiler | | yarn ci | Run full CI pipeline (install, lint, test, build) | | yarn release | Build and publish to npm (uses changesets) |

Build System

Sound Tank uses tsup for building:

  • Dual Output: CommonJS (dist/index.js) and ESM (dist/index.mjs)
  • Type Declarations: Full TypeScript .d.ts files
  • Source Maps: Included for debugging
  • Tree-shaking: Optimized bundles

Build outputs:

dist/
├── index.js        # CommonJS
├── index.mjs       # ES Modules
├── index.d.ts      # TypeScript declarations
└── *.map           # Source maps

Testing

Sound Tank uses Vitest for testing with comprehensive coverage.

Run Tests

# Run all tests once
yarn test

# Run tests in watch mode (for development)
yarn dev

# Run a single test file
yarn test tests/methods/listings/getListings.test.ts

Test Structure

  • Unit Tests: Test individual functions in isolation using MockHttpClient
  • Integration Tests: Test real API interactions (require REVERB_API_KEY in .env)

Writing Tests

Integration tests require a valid API key:

import { config } from 'dotenv';
config(); // Load .env

const reverb = new Reverb({ apiKey: process.env.REVERB_API_KEY });

// Your tests here

Tests are located in the tests/ directory and mirror the src/ structure.

Contributing

Contributions are welcome! Here's how to get started:

Process

  1. Fork the repository on GitHub
  2. Clone your fork locally
  3. Create a feature branch: git checkout -b feature/amazing-feature
  4. Make your changes with clear, focused commits
  5. Test thoroughly: yarn test
  6. Lint your code: yarn lint
  7. Commit with descriptive messages
  8. Push to your fork: git push origin feature/amazing-feature
  9. Open a Pull Request

Guidelines

  • Write TypeScript with strict types
  • Add tests for new features
  • Update documentation as needed
  • Follow existing code style (Prettier configured)
  • Ensure all tests pass (yarn test)
  • Keep commits atomic and well-described
  • Run full CI locally before pushing: yarn ci

Changesets

This project uses Changesets for version management:

# After making changes, create a changeset
npx changeset

# Follow prompts to describe your changes
# Commit the generated changeset file

Release Process

Releases are automated via GitHub Actions:

  1. Create and commit a changeset for your changes
  2. Push to main branch (after PR approval)
  3. Changesets GitHub Action creates a "Version Packages" PR
  4. Review and merge the Version Packages PR
  5. Package automatically publishes to npm with provenance

The project uses OIDC trusted publishing for npm, so no NPM_TOKEN is needed.

Links & Resources

License

MIT © Zachary Eggert

See LICENSE for details.