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.
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:
-
merchant.content.create,update,delete(anis_authormember may also write, but only their own items) -
merchant.users.list,invite,update,remove merchant.pricingRules.create,deactivatemerchant.domains.add,verify,remove
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/ .