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 } from 'listbee';
const client = new ListBee({ apiKey: 'lb_...' });
// Create a listing with a managed deliverable and publish it
const listing = await client.listings.create({
name: 'SEO Playbook',
price: 2900, // $29.00 in cents
description: 'A comprehensive guide to modern SEO.',
deliverable: Deliverable.url('https://example.com/seo-playbook.pdf'),
fulfillmentMode: 'static',
});
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 two-step bootstrap flow:
1. start() — submit email → OTP sent, bootstrap_token returned
2. verify() — submit bootstrap_token + OTP code → account created, api_key issued
3. (poll) — optional: poll readiness until Stripe Connect onboarding is completeimport { ListBee } from 'listbee';
// Bootstrap endpoints are public — no API key needed
const client = new ListBee({ apiKey: '' });
// Step 1: Submit email — sends a one-time code, returns a bootstrap token
const { bootstrap_token } = await client.bootstrap.start({ email: 'seller@example.com' });
// Step 2: Verify the OTP from email — account is created, API key is issued (shown once!)
const result = await client.bootstrap.verify({
bootstrapToken: bootstrap_token,
otpCode: '123456',
});
const apiKey = result.api_key; // "lb_prod_abc123..." — save this securely!
const accountId = result.account_id; // "acc_..."
const onboardingUrl = result.stripe_onboarding_url; // Stripe Connect link to complete payout setup
// Step 3 (optional): Poll readiness until Stripe Connect onboarding is done
const authedClient = new ListBee({ apiKey });
let poll;
do {
await new Promise(r => setTimeout(r, 2000));
poll = await authedClient.bootstrap.poll(accountId);
} while (!poll.ready);
// Now the account is fully operational — create listings
const listing = await authedClient.listings.create({ name: 'My First Listing', price: 2900 });Using the run() helper
bootstrap.run() orchestrates the full flow. Provide an onHumanAction callback that prompts the user for their OTP code:
const client = new ListBee({ apiKey: '' });
const result = await client.bootstrap.run({
email: 'seller@example.com',
onHumanAction: async (bootstrapToken) => {
// Your app collects OTP from the user and returns it
return promptUserForOtp();
},
});
console.log(result.api_key); // "lb_prod_abc123..." — save this!
console.log(result.account_id); // "acc_..."Resources
| Resource | Methods |
|---|---|
listings |
create, get, list, update, delete, publish, unpublish, archive, setCover |
orders |
get, list, fulfill, redeliver, refund |
events |
list |
files |
upload |
plans |
list |
account |
get, update, delete |
apiKeys |
selfRevoke |
bootstrap |
start, verify, poll, run |
stripe |
connect, disconnect |
utility |
ping |
Listings
import { Deliverable, CheckoutField } from 'listbee';
// Create — managed fulfillment (ListBee delivers on payment)
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',
deliverable: Deliverable.url('https://example.com/seo-playbook.pdf'),
fulfillmentMode: 'static',
});
console.log(listing.id); // lst_r7kq2xy9m3pR5tW1
// Create — external (async) fulfillment — your code handles delivery
const generated = await client.listings.create({
name: 'Custom SEO Report',
price: 4900,
coverBlur: 'auto',
agentCallbackUrl: 'https://my-agent.com/fulfill',
fulfillmentMode: 'async',
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,
}),
],
});
// Publish (make live)
const published = await client.listings.publish(listing.id);
console.log(published.url); // https://buy.listbee.so/r7kq2xy9
// Unpublish (return to draft)
const draft = await client.listings.unpublish(listing.id);
console.log(draft.status); // "draft"
// Archive (end-of-life — no purchases allowed)
const archived = await client.listings.archive(listing.id);
console.log(archived.status); // "archived"
// Get by ID
const fetched = await client.listings.get('lst_r7kq2xy9m3pR5tW1');
// List — returns ListingSummary objects; use get() for full detail
const page = await client.listings.list({ status: 'published', limit: 20 });
for (const l of page.data) {
console.log(l.id, l.name, l.status, l.price);
}
console.log(page.has_more); // true if more pages exist
// Update fields (including rotating the signing secret)
await client.listings.update('lst_r7kq2xy9m3pR5tW1', { price: 3900 });
await client.listings.update('lst_r7kq2xy9m3pR5tW1', { signingSecret: 'rotate' });
// Delete
await client.listings.delete('lst_r7kq2xy9m3pR5tW1');Setting the cover image
Use setCover() to handle upload-and-attach in one call:
import fs from 'fs';
// From a URL — SDK fetches the image, uploads it (purpose=cover), and attaches it
const listing = await client.listings.setCover('lst_abc123', 'https://example.com/hero.png');
console.log(listing.has_cover); // true
// From a local file (Buffer)
const listing = await client.listings.setCover('lst_abc123', fs.readFileSync('hero.png'));
// From an existing file token (skip upload)
const listing = await client.listings.setCover('lst_abc123', 'file_7kQ2xY9mN3pR5tW1vB8a01');
// Manual two-step: upload first, then pass the token to update()
const file = await client.files.upload(fs.readFileSync('hero.png'), { purpose: 'cover' });
await client.listings.update('lst_abc123', { cover: file.id });URL inputs must return an image MIME type (JPEG, PNG, WebP, or GIF). The API enforces a 5 MB size limit for cover images.
Orders
// List — returns OrderSummary objects; use get() for full detail
const page = await client.orders.list({
status: 'paid',
buyerEmail: 'buyer@example.com',
createdAfter: new Date('2026-03-01'),
createdBefore: '2026-03-31T23:59:59Z',
});
console.log(page.has_more);
// Get by ID
const order = await client.orders.get('ord_9xM4kP7nR2qT5wY1');
console.log(order.status); // "paid" | "fulfilled"
console.log(order.payment_status); // "unpaid" | "paid" | "refunded"
console.log(order.checkout_data); // custom checkout field values
console.log(order.deliverable); // DeliverableShape | null (if static delivery)
console.log(order.unlock_url); // buyer-facing delivery URL (if applicable)
console.log(order.paid_at); // ISO 8601 timestamp
// Fulfill — close out an async order, optionally pushing generated content
import { Deliverable } from 'listbee';
// No content — marks fulfilled without delivery (e.g. physical goods)
await client.orders.fulfill('ord_9xM4kP7nR2qT5wY1');
// Push a URL delivery to the buyer
await client.orders.fulfill('ord_9xM4kP7nR2qT5wY1', {
deliverable: Deliverable.url('https://example.com/report.pdf'),
});
// Push generated text to the buyer
const fulfilled = await client.orders.fulfill('ord_9xM4kP7nR2qT5wY1', {
deliverable: Deliverable.text('Your AI-generated report is ready.\n\n' + reportContent),
metadata: { generatedAt: new Date().toISOString() },
});
console.log(fulfilled.status); // "fulfilled"
// Redeliver — re-send the deliverable to the buyer
const ack = await client.orders.redeliver('ord_9xM4kP7nR2qT5wY1');
console.log(ack.queued); // true
// Refund
const refunded = await client.orders.refund('ord_9xM4kP7nR2qT5wY1');
console.log(refunded.payment_status); // "refunded"Events
The event log records all delivery attempts for events fired to your events_callback_url. Use it to reconcile missed deliveries, audit event history, or replay past events.
// List all recent events
const events = await client.events.list({ limit: 20 });
for (const event of events.data) {
console.log(event.event_type, event.status, event.created_at);
}
// Filter by event type
const orderPaidEvents = await client.events.list({ eventType: 'order.paid' });
// Paginate
if (events.has_more && events.cursor) {
const next = await client.events.list({ cursor: events.cursor });
}Files
Files are uploaded first to receive a token, then the token is passed to create/update calls. The purpose parameter controls size and MIME type limits.
| Purpose | Max size | Accepted types |
|---|---|---|
cover |
5 MB | JPEG, PNG, WebP, GIF |
avatar |
2 MB | JPEG, PNG, WebP, GIF |
import fs from 'fs';
// Upload a cover image
const cover = await client.files.upload(fs.readFileSync('hero.png'), { purpose: 'cover' });
console.log(cover.id); // "file_7kQ2xY9mN3pR5tW1vB8a01" — use this token
// Upload an avatar image
const avatar = await client.files.upload(fs.readFileSync('avatar.png'), { purpose: 'avatar' });
// Attach the cover to a listing
await client.listings.update('lst_abc123', { cover: cover.id });For cover images, you can skip the manual upload step using setCover() — see the Listings section above.
Deliverable content (PDFs, reports, etc.) is hosted externally and referenced by URL. Use Deliverable.url() or Deliverable.text() directly — no file upload needed.
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.fee_rate); // "0.10" (platform fee)
console.log(account.events_callback_url); // URL receiving event POSTs (if set)
console.log(account.notify_orders); // true if order email notifications are enabled
console.log(account.readiness.operational); // true once Stripe is connected
// Update analytics, notification settings, or callback URL
await client.account.update({ gaMeasurementId: 'G-XXXXXXXXXX' });
await client.account.update({ notifyOrders: false });
await client.account.update({ eventsCallbackUrl: 'https://my-app.com/events' });
// Delete account
await client.account.delete();API Keys
// Revoke the key used for this request (self-destruct — irreversible)
const ack = await client.apiKeys.selfRevoke();
console.log(ack.revoked); // true — key is now invalidatedBootstrap
See the Authentication section for the full bootstrap flow.
const client = new ListBee({ apiKey: '' }); // no key needed for start/verify
// Step 1
const { bootstrap_token } = await client.bootstrap.start({ email: 'seller@example.com' });
// Step 2
const result = await client.bootstrap.verify({
bootstrapToken: bootstrap_token,
otpCode: '123456',
});
console.log(result.api_key); // "lb_prod_..." — save this!
console.log(result.stripe_onboarding_url); // Stripe Connect link
// Step 3 (poll until ready)
const authedClient = new ListBee({ apiKey: result.api_key });
let poll;
do {
await new Promise(r => setTimeout(r, 2000));
poll = await authedClient.bootstrap.poll(result.account_id);
} while (!poll.ready);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();
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 supports two fulfillment modes, set at listing creation via fulfillmentMode:
static— Attach adeliverableto the listing. ListBee auto-delivers it to buyers on payment. No code needed after publish.async— No deliverable on the listing. ListBee firesorder.paidto youragentCallbackUrl(if set) and/or your account'sevents_callback_url. Your app handles delivery by callingorders.fulfill().
import { Deliverable, CheckoutField } from 'listbee';
// Static — ListBee delivers automatically
const ebook = await client.listings.create({
name: 'SEO Playbook',
price: 2900,
deliverable: Deliverable.url('https://example.com/seo-playbook.pdf'),
fulfillmentMode: 'static',
});
await client.listings.publish(ebook.id);
// Async — AI-generated content, collect buyer input at checkout
const custom = await client.listings.create({
name: 'Custom SEO Report',
price: 4900,
agentCallbackUrl: 'https://my-agent.com/fulfill',
fulfillmentMode: 'async',
checkoutSchema: [
CheckoutField.text('website_url', { label: 'Your website URL', sortOrder: 0 }),
CheckoutField.select('report_type', {
label: 'Report type',
options: ['Quick audit', 'Full analysis'],
sortOrder: 1,
}),
],
});
await client.listings.publish(custom.id);
// On order.paid — push generated content to buyer via ListBee
await client.orders.fulfill(order.id, {
deliverable: Deliverable.text(generatedReport),
});Helper methods
Price formatting
import { formatPrice, toMinor, fromMinor } from 'listbee';
formatPrice(2900, 'USD'); // "$29.00"
formatPrice(3000, 'JPY'); // "¥3000"
toMinor(29.00, 'USD'); // 2900
fromMinor(2900, 'USD'); // 29Order state helpers
import {
isPaid,
isRefunded,
isDisputed,
needsFulfillment,
isTerminal,
} from 'listbee';
const order = await client.orders.get('ord_...');
if (needsFulfillment(order)) {
// order.status === 'paid' — call fulfill() to deliver content
await client.orders.fulfill(order.id, { deliverable: Deliverable.text('...') });
}
if (isTerminal(order)) {
// order.status === 'fulfilled' — no further action needed
}Listing state helpers
import { isDraft, isPublished, isArchived, hasDeliverable } from 'listbee';
const listing = await client.listings.get('lst_...');
if (isDraft(listing)) {
await client.listings.publish(listing.id);
}
if (isArchived(listing)) {
// listing is retired — republish to make it live again
await client.listings.publish(listing.id);
}
if (hasDeliverable(listing)) {
console.log('Listing has pre-attached content:', listing.deliverable);
}Readiness helpers
import { nextAction, actionsByKind, resolveAction } 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(`Automated: call ${action.resolve.endpoint}`);
} else if (action) {
console.log(`Manual step: open ${action.resolve.url} in browser`);
}
// Separate API actions from manual steps
const apiActions = actionsByKind(account.readiness, 'api');
const manualSteps = actionsByKind(account.readiness, 'human');
// Execute all automated actions in sequence
for (const a of apiActions) {
await resolveAction(client, a);
}Readiness system
Every account and listing includes a readiness field that tells you what actions are needed before you can transact.
const account = await client.account.get();
if (!account.readiness.operational) {
for (const action of account.readiness.actions) {
if (action.kind === 'api') {
// Automated — call the resolve endpoint
console.log(`API action: ${action.code} -> ${action.resolve.endpoint}`);
} else {
// Human step — open URL in browser
console.log(`Manual step: ${action.code} -> ${action.resolve.url}`);
}
}
}Webhook verification
Verify HMAC-SHA256 signatures on incoming webhook requests:
import { verifyWebhookSignature, parseWebhookEvent } from 'listbee';
// Verify only
const isValid = await verifyWebhookSignature(rawBodyString, signature, secret);
// Verify and parse in one call
try {
const event = await parseWebhookEvent(rawBodyString, signature, secret);
console.log(event.type); // 'order.paid' | 'order.fulfilled' | etc.
console.log(event.data); // typed event payload
} catch (e) {
// WebhookVerificationError — invalid signature
}Idempotency
Pass idempotencyKey to any mutating request. Same key within 24 hours returns the cached response — safe to retry:
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); // ListingSummary[] — slim; call get(id) for full detail
console.log(page.has_more); // true if more pages exist
console.log(page.cursor); // pass to next call
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:
const allListings = await client.listings.list().autoPagingToArray({ limit: 1000 });Or use async iteration:
for await (const page of client.listings.list()) {
for (const listing of page.data) {
console.log(listing.id, listing.name);
}
}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. Retry after ${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 retries
const listing = await client.listings.create(
{ name: 'SEO Playbook', price: 2900 },
{ timeoutMs: 90_000, maxRetries: 5 },
);
// 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 code, request ID, rate limit info):
const raw = await client.listings.withRawResponse.get('lst_abc123');
console.log(raw.statusCode); // 200
console.log(raw.requestId); // "req_abc123" | null
console.log(raw.headers['x-ratelimit-remaining']); // "98"
const listing = raw.parse(); // typed ListingResponseTypes
All types are importable from listbee:
import {
ListBee,
CursorPage,
Deliverable, // builder: .url(content) | .text(content)
CheckoutField, // builder: .text() | .select() | .date()
// Response types
type AccountResponse,
type ApiKeyRevokeAck,
type BootstrapStartResponse,
type BootstrapVerifyResponse,
type BootstrapPollResponse,
type DeliverableShape,
type EventResponse,
type ListingResponse,
type ListingSummary, // slim listing shape returned by listings.list()
type OrderResponse,
type OrderSummary, // slim order shape returned by orders.list()
type PlanResponse,
type RedeliveryAck,
type StripeConnectSessionResponse,
// Enums / literal types
ActionCode,
ActionKind,
ActionPriority, // "required" | "suggested"
DeliverableType, // "url" | "text"
FulfillmentMode, // "static" | "async"
ListingStatus, // "draft" | "published" | "archived"
OrderStatus, // "paid" | "fulfilled"
// Errors
ListBeeError,
APIStatusError,
APIConnectionError,
APITimeoutError,
BadRequestError,
AuthenticationError,
ForbiddenError,
NotFoundError,
ConflictError,
PayloadTooLargeError,
ValidationError,
RateLimitError,
InternalServerError,
WebhookVerificationError,
} from 'listbee';Requirements
- Node.js >= 18
License
Apache-2.0. See LICENSE.
Contributing
Bug reports and feature requests welcome — open an issue on GitHub.