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-tankimport 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
fetchAPI; no runtime dependencies
Table of Contents
- Installation
- Getting Started
- Configuration
- API Methods
- TypeScript Usage
- Advanced Features
- Examples
- Development
- Testing
- Contributing
- License
Installation
Install via your preferred package manager:
npm install sound-tank
# or
yarn add sound-tank
# or
pnpm add sound-tankGetting a Reverb API Key
- Visit Reverb API Settings
- Generate a new API token
- Set the appropriate scopes for your use case
- Store securely in environment variables
# .env
REVERB_API_KEY=your_api_key_hereYou can use the provided .env.example file as a template:
cp .env.example .env
# Edit .env and add your REVERB_API_KEYGetting 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 pagepage?: number- Page number (starts at 1)query?: string- Search query to filter listingsstate?: 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 listingsstate?: ListingStates- Filter by state:ListingStates.LIVE,ListingStates.SOLD, orListingStates.DRAFT
Returns: Promise<HttpResponse<Listing[]>>
const response = await reverb.listings.getAllMy({ state: 'live' });
const allListings = response.data; // All listings from all pagesNote: 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 IDbody: 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 IDreason: 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?: numberpage?: 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 IDimageId: 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 IDphotoUrls: 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?: numberperPage?: numberstatus?: '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 IDreplyBody: 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 informationListingStates- Enum for state values (LIVE, SOLD, DRAFT)ListingCondition- Item condition with UUID and display nameListingShipping- Shipping information and ratesListingImage- Photo attached to a listing (includes_links.full.href)ListingStats- View and watch countsListingPostBody- Body for creating a listingListingUpdateBody- Body for updating a listingEndListingReason- Valid reasons for ending a listing
Order Types:
Order- Complete order information with buyer, seller, shipping, pricing detailsOrderStatus- Order status informationShippingAddress- Complete address information
Negotiation Types:
Negotiation- Individual offer/negotiation detailsListingWithNegotiations- Listing with attached negotiations array
Pricing Types:
Price- Currency-aware price with amount, currency, symbol, and formatted displayShippingRate- Regional shipping costs
Catalog Types:
ReverbShippingRegion- Shipping region returned bycatalog.getShippingRegions()ListingCurrency- Currency option returned bycatalog.getCurrencies()
Other Types:
Category- Product categorizationLink- HATEOAS navigation linksPaginatedReverbResponse<T>- Paginated API response wrapperReverbOptions- SDK configuration optionsReverbCacheOptions- 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 codeStreaming 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 cacheHTTP 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_KEYProject 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.mtsDebugging
Set SOUNDTANK_LOG_LEVEL to enable SDK logging:
SOUNDTANK_LOG_LEVEL=DEBUG yarn devValid 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.tsfiles - 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 mapsTesting
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.tsTest Structure
- Unit Tests: Test individual functions in isolation using
MockHttpClient - Integration Tests: Test real API interactions (require
REVERB_API_KEYin.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 hereTests are located in the tests/ directory and mirror the src/ structure.
Contributing
Contributions are welcome! Here's how to get started:
Process
- Fork the repository on GitHub
- Clone your fork locally
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes with clear, focused commits
- Test thoroughly:
yarn test - Lint your code:
yarn lint - Commit with descriptive messages
- Push to your fork:
git push origin feature/amazing-feature - 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 fileRelease Process
Releases are automated via GitHub Actions:
- Create and commit a changeset for your changes
- Push to
mainbranch (after PR approval) - Changesets GitHub Action creates a "Version Packages" PR
- Review and merge the Version Packages PR
- Package automatically publishes to npm with provenance
The project uses OIDC trusted publishing for npm, so no NPM_TOKEN is needed.
Links & Resources
- npm Package: sound-tank on npm
- GitHub Repository: ZacharyEggert/sound-tank
- Report Issues: GitHub Issues
- Reverb API Docs: reverb.com/page/api
- Reverb Marketplace: reverb.com
License
MIT © Zachary Eggert
See LICENSE for details.
