LedeWire SDK

Node.js — Getting Started

@ledewire/node provides the full LedeWire API surface — merchant authentication, content management, sales reporting, buyer flows, wallet, and purchases — in a single typed npm package.

Who this is for: Developers building merchant stores, admin tools, or server-side integrations with the LedeWire API.

Installation

npm install @ledewire/node
# or
pnpm add @ledewire/node

Authentication modes

createClient() supports two authentication strategies that can be combined or used independently:

Mode Use case Config
Store API key + secret Automated content publishing, sales dashboards via client.merchant.* Pass apiKey / apiSecret to createClient(), or call client.auth.loginWithApiKey()
Email / password Merchant operations: user management, store config, content moderation Call client.merchant.auth.loginWithEmail()
Buyer session Server-side buyer flows on behalf of a logged-in end user Call client.auth.loginWithEmail()

Quick start — store API key

import { createClient } from '@ledewire/node'

const client = createClient({
  onAuthExpired: () => { throw new Error('Session expired') },
})
// The login resolves with the key's store_id: pass it to client.merchant.*
const { store_id } = await client.auth.loginWithApiKey({
  key: process.env.LEDEWIRE_API_KEY,
  secret: process.env.LEDEWIRE_API_SECRET,
})

const summary = await client.merchant.sales.summary(store_id)
const { data } = await client.merchant.content.list(store_id)
console.log(summary, data)

Passing apiKey and apiSecret (omit the secret for a view key) to createClient() logs in automatically. Construction sends nothing; the client logs in with the key before its first authenticated request when no token is stored, and concurrent first requests share one login. When a refresh fails later, it logs in with the key again and retries the request once. onAuthExpired fires only when an automatic login fails, once per failed attempt. A refused key rejects the request with an AuthError carrying the API's message.

On a client created with apiKey, clearing the stored tokens doesn't drop the credentials: the next request logs in again. To stop for good, create a client without apiKey. Don't mix another login (email, Google, merchant) on an apiKey client: if that session's refresh fails, the client falls back to the store key.

To get the key's store_id, call loginWithApiKey() once yourself, as above. The client then holds a token, so it doesn't log in again.

const client = createClient({
  apiKey: process.env.LEDEWIRE_API_KEY,
  apiSecret: process.env.LEDEWIRE_API_SECRET,
})
const { store_id } = await client.auth.loginWithApiKey({
  key: process.env.LEDEWIRE_API_KEY,
  secret: process.env.LEDEWIRE_API_SECRET,
}) // the only login: the client already holds a token for its requests
const summary = await client.merchant.sales.summary(store_id)

Full and view keys

A store API key logged in with its secret has full permission; logged in with the key alone it has view permission. Reads work with either. Content writes, member admin and x402 setup need a full key (or an owner's merchant login), and a view key gets a 403 ForbiddenError:

Store invitations

merchant.users.invite() emails an invitation_token to someone without an account. A signup page can show the invitation first with the public client.auth.getInviteInfo(token), which sends no credentials. The invitee then joins through client.auth.signup() or client.auth.loginWithGoogle() with invitation_token, or, already a merchant, through client.merchant.auth.acceptInvitation(). A refused invitation carries err.type === 'invitation_not_accepted' and the reason in err.details.reason.

const invite = await client.auth.getInviteInfo(token) // 404 unknown, 409 used, 410 expired
console.log(`Join ${invite.store_name}`)

await client.merchant.auth.loginWithEmail({ email, password })
const membership = await client.merchant.auth.acceptInvitation({ invitation_token: token })

Migrating from client.seller

client.seller has been removed: the API retired /v1/seller/*, and client.merchant.* accepts store API-key tokens. Every replacement takes the store's id as its first argument, and loginWithApiKey resolves with it:

const { store_id } = await client.auth.loginWithApiKey({ key, secret })
const summary = await client.merchant.sales.summary(store_id)
Before After
seller.sales.summary() merchant.sales.summary(storeId)
seller.sales.list() merchant.sales.list(storeId) (paginated, one row per content item)
seller.buyers.list() merchant.buyers.list(storeId)
seller.config.get() merchant.config.get(storeId)
seller.content.*(storeId, …) merchant.content.*(storeId, …)

The seller-shaped search request is now only MerchantContentSearchRequest; ContentSearchRequest names the buyer search request that client.content.search() takes. In @ledewire/browser, lw.seller (including lw.seller.loginWithApiKey and lw.seller.content.*) has been removed: the browser SDK is buyer-only, and a store API key does not belong in a browser.

Quick start — merchant (email login)

import { createClient } from '@ledewire/node'

const client = createClient()

// Authenticate and list stores in one call
const { stores } = await client.merchant.auth.loginWithEmailAndListStores({
  email: process.env.LEDEWIRE_EMAIL,
  password: process.env.LEDEWIRE_PASSWORD,
})
const storeId = stores[0].id

// Sales summary
const summary = await client.merchant.sales.summary(storeId)
console.log(`Revenue: ${summary.total_revenue_cents / 100} USD`)

Error handling

All SDK errors extend LedewireError and carry a statusCode and structured code / message from the API. Import specific error classes to handle individual cases:

import { createClient, LedewireError, NotFoundError, AuthError } from '@ledewire/node'

try {
  const content = await client.merchant.content.get(storeId, 'some-id')
} catch (err) {
  if (err instanceof NotFoundError) {
    console.log('Content not found')
  } else if (err instanceof AuthError) {
    console.log('Not authenticated — call loginWithEmail first')
  } else if (err instanceof LedewireError) {
    console.error(`API error ${err.statusCode}: ${err.message}`)
  } else {
    throw err  // unexpected — rethrow
  }
}

client.purchases.create(), the x402 content gate, and client.acquisitions.authorize() can also throw SpendCapReachedError (402) when the buyer's daily spend cap is reached. Funding the wallet does not clear it — the cap resets at err.resetsAt, or can be raised via client.user.spendCap.update().

import { SpendCapReachedError } from '@ledewire/node'

try {
  await client.purchases.create({ content_id, price_cents })
} catch (err) {
  if (err instanceof SpendCapReachedError) {
    console.error(`Spend cap reached; resets at ${err.resetsAt}`)
  }
}

Full namespace reference

Namespace Methods
client.auth.* signup, loginWithEmail, loginWithGoogle, loginWithBuyerApiKey, loginWithApiKey, requestPasswordReset, resetPassword, getInviteInfo
client.merchant.auth.* loginWithEmail, loginWithGoogle, listStores, loginWithEmailAndListStores, loginWithGoogleAndListStores, requestPasswordReset, resetPassword, acceptInvitation
client.merchant.users.* list, invite, remove, update
client.merchant.sales.* summary, list, get
client.merchant.config.* get
client.merchant.content.* create, list, get, update, delete, search
client.wallet.* balance, transactions, createPaymentSession, getPaymentStatus
client.purchases.* create, list, get (single-use delivery — see below), verify, licenseDocument
client.content.* getWithAccess (buyer token only; a store credential gets 403), search
client.checkout.* state
client.user.spendCap.* get, update
client.user.mcpKeys.* list, create, revoke
client.user.paymentKeys.* list, create, revoke (Ed25519 Payment keys for x402; no secret)
client.company.* membership, invitations, members, machineUsers (+ buyerKeys, mcpKeys, paymentKeys), wallet, purchases, spend
client.publications.* list, get, listWorks
client.acquisitions.* create, get, requote, cancel, acknowledgeExclusions, authorize, listWorks, getCorpus, buildCorpus, downloadCorpus, getManifest, signingKeyHistory
client.x402.* discoverResources, verifyOrigin, getJwks

Buyer purchases — single-use delivery

Purchases are single-use: client.purchases.create() delivers the content directly in its response (content_body as plain UTF-8, or content_uri). Neither field is ever present on list() or get() — persist the delivery immediately, there is no route that serves it a second time.

// price_cents is the price the buyer was shown; a stale one is refused (422)
const { price_cents } = await client.content.getWithAccess('content-id')
const purchase = await client.purchases.create({ content_id: 'content-id', price_cents })
await saveDeliveredContent(purchase) // one-time delivery

if (purchase.content_body) {
  // Always plain UTF-8 — never base64-encoded.
} else if (purchase.content_uri) {
  // Check the scheme before following or linking to it.
}

Purchase licences

Every purchase carries license: a PurchaseLicense (type, permissions, document_url) exactly as the work's source granted it, or null when none was recorded (every Store work, and older brokered purchases). null means “not recorded”, never “no rights granted”. license.type is the source's own value, such as ON_DEMAND_LICENSE, ON_DEMAND_FULL_USE_LICENSE or public-domain for an SEC filing. content.publication names a brokered work's Publication, and content.filing describes an SEC filing; both are null otherwise.

client.purchases.licenseDocument(id) downloads the document license.document_url points at. It is not JSON: it resolves to { body: ArrayBuffer, contentType }, usually application/pdf, or text/markdown for an SEC filing's terms. A missing, incomplete or uncaptured purchase throws NotFoundError; a non-buyer credential throws ForbiddenError.

import { writeFile } from 'node:fs/promises'

const purchase = await client.purchases.get(purchaseId)
if (purchase.license?.document_url) {
  const { body, contentType } = await client.purchases.licenseDocument(purchase.id)
  const ext = contentType.startsWith('application/pdf')
    ? 'pdf'
    : contentType.startsWith('text/markdown')
      ? 'md'
      : 'bin'
  const file = `licence.${ext}`
  await writeFile(file, Buffer.from(body))
}

Buyer content search

client.content.search() takes a ContentSearchRequest and searches every Store and brokered Publication. Page with next_cursor, sending the same filters, until it is null. publication keeps one Publication's works (its id, or its exact name). form_type, ticker and cik return only SEC EDGAR filings, and query may then be left out to list them newest first. Each result's filing gives the filer, ticker, CIK, form, filing date, period of report and accession number, and is null for every other work. Filings are shown to every signed-in Buyer, up to that Buyer's limit on SEC requests; over it, the search returns what it could with partial_results: true.

// Apple's annual reports, amendments (10-K/A) included.
let cursor
do {
  const page = await client.content.search({ ticker: 'AAPL', form_type: '10-K', cursor })
  for (const result of page.data) {
    console.log(result.filing?.form_type, result.filing?.filing_date, result.title)
  }
  cursor = page.next_cursor
} while (cursor)

A cursor lasts 24 hours and fits only the search that issued it. One that is expired, invalid, or sent with different filters throws InvalidCursorError (statusCode 422, type 'invalid_cursor'), and nothing is searched. Drop the cursor and restart the search from the first page: the same cursor never succeeds on retry.

import { InvalidCursorError } from '@ledewire/node'

try {
  page = await client.content.search({ ticker: 'AAPL', form_type: '10-K', cursor })
} catch (err) {
  if (!(err instanceof InvalidCursorError)) throw err
  page = await client.content.search({ ticker: 'AAPL', form_type: '10-K' }) // restart
}

client.publications.list() and get() are sent without credentials, so they return the public catalog. The API shows SEC EDGAR there only to a signed-in Buyer; search filings with the filters above instead.

Bulk licensing (agents)

License a publication's entire catalog in one transaction via client.publications and client.acquisitions: submit a Selection of work URLs, poll the quote, acknowledge exclusions, authorize the hold, then download the corpus once the run settles.

Poll at the interval the server sends in poll_after_seconds, for as long as that field is present.

const wait = (s) => new Promise((r) => setTimeout(r, s * 1000))
let acquisition = await client.acquisitions.create({ urls })

while (acquisition.poll_after_seconds !== undefined) {
  await wait(acquisition.poll_after_seconds)
  acquisition = await client.acquisitions.get(acquisition.id)
}

acquisition = await client.acquisitions.acknowledgeExclusions(acquisition.id)
acquisition = await client.acquisitions.authorize(acquisition.id) // may throw SpendCapReachedError

while (acquisition.poll_after_seconds !== undefined) {
  await wait(acquisition.poll_after_seconds)
  acquisition = await client.acquisitions.get(acquisition.id)
}

const result = await client.acquisitions.downloadCorpus(acquisition.id)
// result.ready === true → stream result.body to a file

createAgentClient() exposes company, publications, acquisitions, and x402 alongside the buyer namespaces, for autonomous agents.

Company wallets

client.company manages a shared wallet that pays for its members' purchases: invitations, members and their daily Spend caps, Machine users (agent identities with no login) and their keys, Company top-ups, and spend reports. A Company member never sees the Company balance — wallet.balance() returns balance_cents: null with remaining_cents and company_name instead, so handle null before doing arithmetic on a balance.

For full parameter and return type documentation, see the API Reference. For a complete runnable example, see examples/node-merchant/ .