Package Exports
- listbee
Readme
listbee
Official TypeScript SDK for the ListBee API — one API call to sell and deliver digital content.
- Zero runtime dependencies (native fetch, Node >= 18)
- Types generated from OpenAPI spec — zero drift
- Full error hierarchy (RFC 9457)
- Cursor-based pagination
- Retry with exponential backoff
- Idempotency support
Install
npm install listbeepnpm add listbeeQuick start
import { ListBee, Deliverable, CheckoutField } from 'listbee';
const client = new ListBee({ apiKey: 'lb_...' });
// Create a listing, attach a deliverable, and publish in one call
const listing = await client.listings.createComplete({
name: 'SEO Playbook',
price: 2900, // $29.00 in cents
description: 'A comprehensive guide to modern SEO.',
deliverables: [
Deliverable.url('https://example.com/seo-playbook.pdf'),
],
});
const published = await client.listings.publish(listing.id);
console.log(published.url); // https://buy.listbee.so/r7kq2xy9Or read from the environment:
export LISTBEE_API_KEY="lb_..."const client = new ListBee(); // reads LISTBEE_API_KEY automaticallyAuthentication
Using an existing API key
Pass your API key explicitly or via the LISTBEE_API_KEY environment variable:
const client = new ListBee({ apiKey: 'lb_...' });
const client = new ListBee(); // reads LISTBEE_API_KEYAPI keys start with lb_. The key is validated lazily — an AuthenticationError is raised only when you make the first API call.
Getting an API key (Bootstrap)
If you're building an agent that needs to programmatically create a ListBee account and get an API key, use the three-step bootstrap flow:
import { ListBee } from 'listbee';
// No API key needed for bootstrap — these endpoints are public
const client = new ListBee({ apiKey: '' });
// Step 1: Submit email — sends a one-time code
const { session } = await client.bootstrap.start({ email: 'seller@example.com' });
// Step 2: Verify the OTP from email
const { session: verified } = await client.bootstrap.verify({ session, code: '123456' });
// Step 3: Create a store — returns StoreResponse with api_key (shown once, save it!)
const store = await client.bootstrap.createStore({
session: verified,
storeName: 'Acme Agency',
});
const apiKey = store.api_key!;
// Now use this key for all future calls:
const authedClient = new ListBee({ apiKey });This is the only way to obtain an API key programmatically. API keys are now tied to stores, not account-level management endpoints.
Resources
| Resource | Methods |
|---|---|
listings |
create, get, list, update, publish, setDeliverables, removeDeliverables, addDeliverable, removeDeliverable, createComplete, delete |
orders |
get, list, fulfill, refund |
customers |
get, list |
files |
upload |
plans |
list |
webhooks |
create, get, list, update, delete, listEvents, retryEvent, test, verify |
account |
get, update, delete |
store |
get, update |
bootstrap |
start, verify, createStore |
stripe |
connect, disconnect |
utility |
ping |
Listings
// Create
const listing = await client.listings.create({
name: 'SEO Playbook 2026',
price: 2900,
description: 'A comprehensive guide to modern SEO techniques.',
tagline: 'Updated for 2026 algorithm changes',
highlights: ['50+ pages', 'Actionable tips', 'Free updates'],
cta: 'Get Instant Access',
cover_url: 'https://example.com/cover.png',
compare_at_price: 3900,
metadata: { source: 'n8n', campaign: 'launch-week' },
});
console.log(listing.id); // lst_r7kq2xy9m3pR5tW1
// Set deliverables (file, URL, or text)
import { Deliverable } from 'listbee';
await client.listings.setDeliverables(listing.id, {
deliverables: [
Deliverable.url('https://example.com/seo-playbook.pdf'),
],
});
// Publish
const published = await client.listings.publish(listing.id);
console.log(published.url); // https://buy.listbee.so/r7kq2xy9
// Get by ID
const listing = await client.listings.get('lst_r7kq2xy9m3pR5tW1');
// List — with filters
const page = await client.listings.list({
status: 'published',
limit: 20,
});
for (const l of page.data) {
console.log(l.id, l.name, l.status);
}
console.log(page.total_count); // total matching listings
// Update
await client.listings.update('lst_r7kq2xy9m3pR5tW1', { price: 3900 });
// Remove deliverables (revert to external fulfillment)
await client.listings.removeDeliverables('lst_r7kq2xy9m3pR5tW1');
// Add a single deliverable using the Deliverable class
import { Deliverable } from 'listbee';
await client.listings.addDeliverable('lst_r7kq2xy9m3pR5tW1', Deliverable.url('https://example.com/playbook.pdf'));
await client.listings.addDeliverable('lst_r7kq2xy9m3pR5tW1', Deliverable.text('Your license key: XXXX-XXXX'));
await client.listings.addDeliverable('lst_r7kq2xy9m3pR5tW1', Deliverable.fromToken(file.token));
// Remove a single deliverable by del_ ID
await client.listings.removeDeliverable('lst_r7kq2xy9m3pR5tW1', 'del_4hR9nK2mQ7tV5wX1');
// Create a listing and attach deliverables in one call
const listing = await client.listings.createComplete({
name: 'SEO Playbook',
price: 2900,
deliverables: [
Deliverable.url('https://example.com/seo-playbook.pdf'),
],
});
await client.listings.publish(listing.id);
// Delete
await client.listings.delete('lst_r7kq2xy9m3pR5tW1');Orders
// List — with filters
const page = await client.orders.list({
status: 'paid',
buyer_email: 'buyer@example.com',
created_after: new Date('2026-03-01'),
created_before: '2026-03-31T23:59:59Z',
});
console.log(page.total_count);
// Get by ID
const order = await client.orders.get('ord_9xM4kP7nR2qT5wY1');
console.log(order.status); // "pending" | "paid" | "fulfilled" | "canceled" | "failed"
console.log(order.has_deliverables); // true = auto-delivered by ListBee, false = seller/agent handles delivery
console.log(order.payment_status); // "unpaid" | "paid" | "refunded"
console.log(order.checkout_data); // custom checkout field values
console.log(order.paid_at); // ISO 8601 timestamp
// Fulfill — close out the order (no content delivery)
await client.orders.fulfill('ord_9xM4kP7nR2qT5wY1');
// Fulfill — push generated content to the buyer (for orders where has_deliverables is false)
import { Deliverable } from 'listbee';
const fulfilled = await client.orders.fulfill('ord_9xM4kP7nR2qT5wY1', {
deliverables: [
Deliverable.text('Your AI-generated report is ready.'),
],
});
console.log(fulfilled.status); // "fulfilled"
// Or fulfill with a URL or file token
await client.orders.fulfill('ord_9xM4kP7nR2qT5wY1', {
deliverables: [
Deliverable.url('https://example.com/report.pdf'),
],
});
// Or upload a file and deliver it in one call
const fileDeliverable = Deliverable.file(Buffer.from(pdfBytes), 'report.pdf');
await client.orders.fulfill('ord_9xM4kP7nR2qT5wY1', { deliverables: [fileDeliverable] });
// Refund
const refunded = await client.orders.refund('ord_9xM4kP7nR2qT5wY1');
console.log(refunded.status); // "canceled"Customers
// List customers
const page = await client.customers.list({ limit: 20, email: 'buyer@example.com' });
for (const c of page.data) {
console.log(c.id, c.email);
}
// Get a customer
const customer = await client.customers.get('cus_7kQ2xY9mN3pR5tW1');
console.log(customer.email, customer.order_count);Files
import { createReadStream } from 'fs';
import { Deliverable } from 'listbee';
// Upload a file (multipart, native FormData)
const file = await client.files.upload({
file: createReadStream('/path/to/report.pdf'),
filename: 'report.pdf',
});
console.log(file.token); // use this token in setDeliverables / Deliverable.fromToken()
// Then attach to a listing
await client.listings.setDeliverables(listing.id, {
deliverables: [
Deliverable.fromToken(file.token),
],
});Webhooks
import { ListBee, WebhookEventType, verifyWebhookSignature, parseWebhookEvent } from 'listbee';
const client = new ListBee({ apiKey: 'lb_...' });
// Create — subscribe to specific events
const webhook = await client.webhooks.create({
name: 'Production endpoint',
url: 'https://example.com/webhooks/listbee',
events: [
WebhookEventType.ORDER_PAID,
WebhookEventType.ORDER_FULFILLED,
WebhookEventType.CUSTOMER_CREATED,
],
});
console.log(webhook.id); // wh_3mK8nP2qR5tW7xY1
console.log(webhook.secret); // HMAC signing key — save this
// In your webhook handler:
const isValid = await verifyWebhookSignature({
payload: rawBodyString,
signature: req.headers['listbee-signature'],
secret: webhook.secret,
});
if (!isValid) {
throw new Error('Webhook signature invalid');
}
// Parse and handle the event
const event = await parseWebhookEvent(rawBodyString, {
signature: req.headers['listbee-signature'],
secret: webhook.secret,
});
console.log(event.type); // 'order.paid' | 'order.fulfilled' | etc.
console.log(event.data); // typed event payload
// List delivery events
const events = await client.webhooks.listEvents('wh_3mK8nP2qR5tW7xY1', { status: 'failed' });
// Retry a failed event
await client.webhooks.retryEvent('wh_3mK8nP2qR5tW7xY1', 'evt_abc123');
// Update, test, delete
await client.webhooks.update('wh_3mK8nP2qR5tW7xY1', { enabled: false });
await client.webhooks.test('wh_3mK8nP2qR5tW7xY1');
await client.webhooks.delete('wh_3mK8nP2qR5tW7xY1');Account
const account = await client.account.get();
console.log(account.id); // acc_7kQ2xY9mN3pR5tW1
console.log(account.email);
console.log(account.plan); // free | growth | scale
console.log(account.notify_orders); // true if order email notifications are enabled
console.log(account.readiness.operational);
// Update notification and analytics settings
await client.account.update({ gaMeasurementId: 'G-XXXXXXXXXX' });
await client.account.update({ notifyOrders: false }); // disable order email notifications
await client.account.delete();Store
Each account has one store. Brand identity — display name, bio, avatar, and slug — lives on the store.
// Get your store
const store = await client.store.get();
console.log(store.display_name); // "Acme Agency"
console.log(store.slug); // "acme-agency"
console.log(store.url); // "https://buy.listbee.so/acme-agency"
console.log(store.readiness.sellable); // true when connected to Stripe
// Check what's needed before selling
if (!store.readiness.sellable) {
for (const action of store.readiness.actions ?? []) {
console.log(`${action.code}: ${action.message}`);
// e.g. "connect_stripe: Connect Stripe to accept payments"
}
}
// Update brand info
const updated = await client.store.update({
displayName: 'Acme Agency',
bio: 'We make great things.',
avatarUrl: 'https://example.com/avatar.jpg',
slug: 'acme-agency', // changes public store URL
});
console.log(updated.url); // "https://buy.listbee.so/acme-agency"Bootstrap
Create a new ListBee account and get an API key programmatically — no manual sign-up required. Useful for agents onboarding sellers automatically.
import { ListBee } from 'listbee';
// Bootstrap endpoints are public — no API key needed
const client = new ListBee({ apiKey: '' });
// Step 1: Start — submit email, receive session ID (OTP sent to email)
const step1 = await client.bootstrap.start({ email: 'seller@example.com' });
console.log(step1.session); // "sess_abc123"
console.log(step1.otp_sent); // true
// Step 2: Verify — submit OTP code, get verified session
const step2 = await client.bootstrap.verify({
session: step1.session,
code: '123456', // from email
});
console.log(step2.verified); // true
console.log(step2.session); // "sess_verified_xyz"
// Step 3: Create store — completes onboarding, returns StoreResponse with api_key
const store = await client.bootstrap.createStore({
session: step2.session,
storeName: 'Acme Agency',
});
console.log(store.api_key); // "lb_prod_abc123..." — save this securely, shown only once
console.log(store.url); // "https://buy.listbee.so/acme-agency"
// Use the new API key for all future calls
const authedClient = new ListBee({ apiKey: store.api_key! });
const listing = await authedClient.listings.create({ name: 'My First Listing', price: 2900 });Stripe
// Generate a Stripe Connect onboarding link
const connect = await client.stripe.connect();
console.log(connect.url); // redirect seller here
// Disconnect Stripe
await client.stripe.disconnect();Plans
List available pricing plans (public endpoint, no authentication required):
const plans = await client.plans.list();
console.log(plans);
// [
// {
// object: 'plan',
// id: 'free',
// name: 'Free',
// tagline: 'Start instantly',
// price_monthly: 0,
// fee_rate: '0.10',
// },
// {
// object: 'plan',
// id: 'pro',
// name: 'Pro',
// tagline: 'Scale your business',
// price_monthly: 2999,
// fee_rate: '0.05',
// },
// ]
for (const plan of plans) {
console.log(`${plan.name}: $${(plan.price_monthly / 100).toFixed(2)}/month, ${(parseFloat(plan.fee_rate) * 100).toFixed(0)}% fee`);
}Fulfillment modes
ListBee determines how orders are fulfilled based on whether deliverables are attached:
- Managed — Attach deliverables to the listing (
setDeliverables()orcreateComplete()). ListBee auto-delivers them to buyers on payment.order.has_deliverablesistrue. - External — No deliverables attached. ListBee fires
order.paidwebhook and/or POSTs tofulfillmentUrlif set. Your app handles delivery.order.has_deliverablesisfalse.
import { Deliverable, CheckoutField } from 'listbee';
// Managed — attach deliverables at listing creation time
const listing = await client.listings.createComplete({
name: 'SEO Playbook',
price: 2900,
coverBlur: 'auto',
deliverables: [
Deliverable.url('https://example.com/seo-playbook.pdf'),
],
});
await client.listings.publish(listing.id);
// Add or remove individual deliverables after creation
await client.listings.addDeliverable(listing.id, Deliverable.text('Bonus: license key XXXX-XXXX'));
await client.listings.removeDeliverable(listing.id, 'del_4hR9nK2mQ7tV5wX1');
// External — AI-generated content, collect input at checkout, push on payment
const generated = await client.listings.create({
name: 'Custom SEO Report',
price: 4900,
coverBlur: 'auto',
fulfillmentUrl: 'https://my-agent.com/fulfill',
checkoutSchema: [
CheckoutField.text('website_url', { label: 'Your website URL', sortOrder: 0 }),
CheckoutField.select('urgency', { label: 'How urgent?', options: ['Low', 'Medium', 'High'], required: false, sortOrder: 1 }),
],
});
await client.listings.publish(generated.id);
// On order.paid webhook — push generated content to the buyer via ListBee
await client.orders.fulfill(order.id, {
deliverables: [
Deliverable.text(generatedReport),
],
});Helper methods
Price formatting
import { formatPrice, toMinor, fromMinor } from 'listbee';
// Format for display
formatPrice(2900); // "$29.00"
formatPrice(100); // "$1.00"
// Convert to/from cents
toMinor('29.00'); // 2900
fromMinor(2900); // "29.00"Order state helpers
import {
isPaid,
isRefunded,
isDisputed,
needsFulfillment,
isTerminal,
} from 'listbee';
const order = await client.orders.get('ord_...');
if (isPaid(order)) {
console.log('Payment received');
}
if (needsFulfillment(order)) {
console.log('Call fulfill() to deliver content');
}
if (isTerminal(order)) {
console.log('Order is in final state — no action needed');
}Listing state helpers
import {
isDraft,
isPublished,
isInStock,
hasDeliverables,
} from 'listbee';
const listing = await client.listings.get('lst_...');
if (isDraft(listing)) {
await client.listings.publish(listing.id);
}
if (hasDeliverables(listing)) {
console.log('Listing has pre-attached content');
}Readiness helpers
import { nextAction, actionsByKind } from 'listbee';
const account = await client.account.get();
// Get the next high-priority action
const action = nextAction(account.readiness);
if (action?.kind === 'api') {
console.log(`Call ${action.resolve.endpoint}`);
} else if (action) {
console.log(`Open ${action.resolve.url} in browser`);
}
// Separate API actions from manual steps
const apiActions = actionsByKind(account.readiness, 'api');
const manualSteps = actionsByKind(account.readiness, 'human');Readiness system
Every listing and account includes a readiness field.
const account = await client.account.get();
if (!account.readiness.operational) {
for (const action of account.readiness.actions) {
if (action.kind === 'api') {
console.log(`API action: ${action.code} -> ${action.resolve.endpoint}`);
} else {
console.log(`Manual step: ${action.code} -> ${action.resolve.url}`);
}
}
}Idempotency
Pass idempotencyKey to any mutating request. Same key within 24h returns the cached response:
await client.listings.create(
{ name: 'SEO Playbook', price: 2900 },
{ idempotencyKey: 'create-listing-campaign-2026' },
);Pagination
const page = await client.listings.list({ limit: 10 });
console.log(page.data); // ListingResponse[]
console.log(page.has_more); // true if more pages exist
console.log(page.cursor); // pass to next call
console.log(page.total_count); // total matching items
if (page.has_more) {
const nextPage = await client.listings.list({ limit: 10, cursor: page.cursor });
}Collecting all pages
Use autoPagingToArray() to collect all results into a single array:
import { ListBee } from 'listbee';
const client = new ListBee({ apiKey: 'lb_...' });
// Fetch all listings (up to 1000)
const allListings = await client.listings.list().autoPagingToArray({ limit: 1000 });
console.log(allListings.length); // total listingsOr use async iteration:
for await (const page of client.listings.list()) {
for (const listing of page.data) {
console.log(listing.id, listing.name);
}
}Utility
Verify API connectivity and that your API key is valid:
const ping = await client.utility.ping();
console.log(ping.status); // "ok"Error handling
ListBeeError
├── APIConnectionError network error — request never reached the server
├── APITimeoutError request timed out
└── APIStatusError server returned 4xx/5xx
├── BadRequestError 400
├── AuthenticationError 401
├── ForbiddenError 403
├── NotFoundError 404
├── ConflictError 409
├── PayloadTooLargeError 413
├── ValidationError 422
├── RateLimitError 429
└── InternalServerError 500+import { NotFoundError, AuthenticationError, RateLimitError } from 'listbee';
try {
await client.listings.get('lst_does-not-exist');
} catch (e) {
if (e instanceof NotFoundError) {
console.log(e.status); // 404
console.log(e.code); // machine-readable error code
console.log(e.detail); // human-readable explanation
console.log(e.requestId); // unique request identifier
} else if (e instanceof RateLimitError) {
console.log(`Rate limited. Resets at ${e.reset}`);
}
}All APIStatusError subclasses expose: status, code, detail, title, type, param, requestId.
Configuration
Client options
const client = new ListBee({
apiKey: 'lb_...',
timeoutMs: 60_000, // default: 30000
maxRetries: 5, // default: 3; retries on 429/500/502/503/504
baseUrl: 'https://api.listbee.so', // override for testing
fetch: customFetchFn, // provide custom fetch (node-fetch, undici, etc.)
});Per-call options
Override client defaults for individual requests:
// Override timeout and max retries for a single request
const listing = await client.listings.create(
{ name: 'SEO Playbook', price: 2900 },
{
timeoutMs: 90_000,
maxRetries: 5,
},
);
// Use a different API key
const other = await client.listings.list(
{ limit: 10 },
{ apiKey: 'lb_other_key' },
);
// Idempotency key for safe retries
await client.listings.create(
{ name: 'SEO Playbook', price: 2900 },
{ idempotencyKey: 'create-unique-id' },
);Raw responses
Access raw HTTP metadata (headers, status, request ID, rate limit info):
const { data, response } = await client.listings.list().withRawResponse();
console.log(response.status); // 200
console.log(response.headers); // Record<string, string>
console.log(response.requestId); // unique request identifierMigration from 0.6.x
CamelCase parameters
All request parameters are now camelCase (matching JavaScript convention), and the SDK automatically translates to snake_case for the API:
// Old (0.6.x)
await client.listings.list({ buyer_email: 'user@example.com' });
// New (0.7.0)
await client.listings.list({ buyerEmail: 'user@example.com' });Webhook verification
webhooks.verify() has been removed. Use the standalone verifyWebhookSignature() instead:
// Old (0.6.x)
const isValid = await client.webhooks.verify({
payload: rawBody,
signature: headers['listbee-signature'],
secret: webhook.secret,
});
// New (0.7.0)
import { verifyWebhookSignature, parseWebhookEvent } from 'listbee';
const isValid = await verifyWebhookSignature({
payload: rawBody,
signature: headers['listbee-signature'],
secret: webhook.secret,
});
// Or use parseWebhookEvent to verify + parse in one call
const event = await parseWebhookEvent(rawBody, {
signature: headers['listbee-signature'],
secret: webhook.secret,
});Webhook event filtering
webhooks.listEvents() filter parameter changed from delivered: boolean to status: string:
// Old (0.6.x)
const events = await client.webhooks.listEvents(whId, { delivered: false });
// New (0.7.0)
const events = await client.webhooks.listEvents(whId, { status: 'failed' });
// status: 'pending' | 'failed' | 'delivered'Error code handling
ErrorCode enum has been removed. Use the code field directly on caught exceptions:
// Old (0.6.x)
import { ErrorCode } from 'listbee';
if (e.code === ErrorCode.LISTING_NOT_FOUND) { ... }
// New (0.7.0)
if (e.code === 'LISTING_NOT_FOUND') { ... }
// e.code is now a string, use for string comparisonTypes
All types are importable from listbee:
import {
ListBee,
CursorPage,
Deliverable, // input class: .file() | .url() | .text() | .fromToken()
CheckoutField, // input builder: .text() | .select() | .date()
PartialCreationError, // thrown when listing is created but deliverable attachment fails
// Response types
type ListingResponse,
type OrderResponse,
type CustomerResponse,
type FileResponse,
type WebhookResponse,
type AccountResponse,
type StoreResponse,
type StoreReadiness,
type BootstrapResponse,
type BootstrapVerifyResponse,
type WebhookEventResponse,
type WebhookTestResponse,
// Enums / literal types
type ActionPriority, // "required" | "suggested"
DeliverableType, // "file" | "url" | "text"
ListingStatus, // "draft" | "published"
OrderStatus, // "pending" | "paid" | "fulfilled" | "canceled" | "failed"
WebhookEventType,
ActionCode,
ActionKind,
// Errors
ListBeeError,
APIStatusError,
APIConnectionError,
APITimeoutError,
BadRequestError,
AuthenticationError,
ForbiddenError,
NotFoundError,
ConflictError,
PayloadTooLargeError,
ValidationError,
RateLimitError,
InternalServerError,
} from 'listbee';Requirements
- Node.js >= 18
License
Apache-2.0. See LICENSE.
Contributing
Bug reports and feature requests welcome — open an issue on GitHub.