JSPM

  • Created
  • Published
  • Downloads 80
  • Score
    100M100P100Q33953F
  • License Apache-2.0

Official TypeScript SDK for the ListBee API — one API call to sell and deliver digital content.

Package Exports

  • listbee

Readme

listbee

npm CI License

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 listbee
pnpm add listbee

Quick 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/r7kq2xy9

Or read from the environment:

export LISTBEE_API_KEY="lb_..."
const client = new ListBee(); // reads LISTBEE_API_KEY automatically

Authentication

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_KEY

API 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 complete
import { 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 invalidated

Bootstrap

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 a deliverable to the listing. ListBee auto-delivers it to buyers on payment. No code needed after publish.
  • async — No deliverable on the listing. ListBee fires order.paid to your agentCallbackUrl (if set) and/or your account's events_callback_url. Your app handles delivery by calling orders.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'); // 29

Order 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 ListingResponse

Types

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.