JSPM

  • Created
  • Published
  • Downloads 79
  • Score
    100M100P100Q33822F
  • 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, 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/r7kq2xy9

Or read from the environment:

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

Authentication

Pass your API key explicitly or via the LISTBEE_API_KEY environment variable.

const client = new ListBee({ apiKey: 'lb_...' });
const client = new ListBee(); // env var

The key is validated lazily — an AuthenticationError is raised only when you make the first API call. API keys start with lb_. Get yours at console.listbee.so.

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
apiKeys list, create, delete
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" | "processing" | "fulfilled" | "handed_off" | "canceled" | "failed"
console.log(order.content_type);  // "static" | "generated" | "webhook"
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 generated content_type orders)
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);

await client.account.update({ ga_measurement_id: 'G-XXXXXXXXXX' });
await client.account.update({ notify_orders: false }); // disable order email notifications
await client.account.delete();

API keys

const keys = await client.apiKeys.list();

const newKey = await client.apiKeys.create({ name: 'CI pipeline' });
console.log(newKey.key); // lb_... (save this immediately)

await client.apiKeys.delete('lbk_7kQ2xY9mN3pR5tW1');

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`);
}

Content types

ListBee listings have three content types that determine how orders are fulfilled:

  • static — ListBee delivers pre-attached digital content automatically on payment. Call setDeliverables(), then publish().
  • generated — Your app generates content per-order. ListBee fires order.paid, your agent generates content and calls orders.fulfill() with deliverables. Order enters processing status until fulfilled.
  • webhook — Your external system handles delivery entirely. ListBee fires order.paid and stays out of delivery. Order status becomes handed_off.
import { Deliverable, CheckoutField } from 'listbee';

// Static — attach deliverables at listing creation time
const listing = await client.listings.createComplete({
  name: 'SEO Playbook',
  price: 2900,
  content_type: 'static',
  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');

// Generated — AI-generated content, collect input at checkout
const generated = await client.listings.create({
  name: 'Custom SEO Report',
  price: 4900,
  content_type: 'generated',
  checkout_schema: [
    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
await client.orders.fulfill(order.id, {
  deliverables: [
    Deliverable.text(generatedReport),
  ],
});

// Webhook — your system handles delivery, just close out
await client.orders.fulfill(order.id);

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 listings

Or 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 identifier

Migration 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 comparison

Types

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 ApiKeyResponse,
  type WebhookEventResponse,
  type WebhookTestResponse,

  // Enums / literal types
  type ContentType,  // "static" | "generated" | "webhook"
  type PaymentStatus, // "unpaid" | "paid" | "refunded"
  DeliverableType,   // "file" | "url" | "text"
  ListingStatus,     // "draft" | "published"
  OrderStatus,       // "pending" | "paid" | "processing" | "fulfilled" | "handed_off" | "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.