@bkbnlab/api
v0.11.0
Published
JavaScript/TypeScript client for the bkbn AI Lab API
Readme
@bkbnlab/api
JavaScript/TypeScript client for the bkbn AI Lab API — AI photo editing for real estate: HDR develop, perspective, signature tone, privacy blur, sky replacement, declutter, staging, magic eraser.
- One WebSocket per client —
order.wait(),batch.wait()andclient.watch()all multiplex a single connection (ticket auth + reconnect hidden). NativeWebSocketon browsers and Node ≥ 22;npm i wson older Node. - Zero required dependencies — native
fetchfor the rest. - Typed errors — branch on
RateLimitError.retryAfterMs, not on strings.
npm install @bkbnlab/apiQuickstart
import { BkbnClient } from '@bkbnlab/api'
const bkbn = new BkbnClient() // reads BKBNLAB_API_KEY
const photo = await bkbn.upload('living-room.jpg') // 3-step upload, hidden
const order = await bkbn.order({
inputs: [photo], // 1-5 brackets of ONE scene
perspective: true, // or { strength: 0.8 }
tonecraft: true,
privacy: true,
sky: 'high_cloud', // clear | low_cloud | neutral | high_cloud
})
const result = await order.wait({
onProgress: (p) => console.log(p.stage, p.percent),
})
await result.save('enhanced.jpg')Batches — one shoot, one call
Shared defaults, per-scene overrides (undefined inherits), atomic
validation server-side: a bad scene refuses the whole batch before anything
runs. One SSE connection follows the whole shoot.
const batch = await bkbn.batch({
name: 'shoot-12-rue-vaugirard',
perspective: true,
tonecraft: true,
scenes: [
{ inputs: [a1, a2, a3] }, // HDR brackets, inherits
{ inputs: [b1], sky: 'high_cloud' }, // override
{ inputs: [c1], genEdit: 'declutter_local' },
],
})
const results = await batch.wait({
onProgress: (p) => console.log(`${p.completed}/${p.total}`),
onOrderDone: (o) => console.log('done', o.orderUuid),
})
await results.saveAll('out/')
results.failed // a failed scene does NOT throwMagic eraser
The mask is an asset like any other (white-on-transparent PNG):
const done = await (await bkbn.order({
inputs: [photo],
genEdit: 'magic_eraser',
mask: await bkbn.upload(maskPng),
})).wait()Exports — other versions of a delivery
Know the format up front? Pass it to order(). Need another version later —
a webp for the listing page, a 1024 px thumbnail, a lighter jpeg — export it:
no re-run, no re-billing of the pipeline.
const webp = await result.export({ format: 'webp', quality: 80 })
await webp.save('room.webp') // node
img.src = webp.url // browser — the link needs no headerAn agency's three formats at once, straight off one delivery:
const [print, web, thumb] = await Promise.all([
result.export({ format: 'jpeg', quality: 95 }),
result.export({ format: 'webp', quality: 80 }),
result.export({ format: 'jpeg', longEdge: 1024 }),
])export() resolves when the file is ready — it listens on the client's one
WebSocket, so you never write a polling loop. Asking twice for the same version
costs one packaging: the second call returns the stored file immediately.
Any asset you own works, not just the freshest order — use
bkbn.export(assetId, options) with an upload id, or with the
outputAssetId of an order from three weeks ago.
Watch the account
Everything that happens on your key — progress, completions, intermediate
frames (command.resultAssetId), asset events — off the ONE shared
WebSocket. Infinite, reconnects by itself. order.wait() and batch.wait()
filter this same connection, so a client holds exactly one socket no matter
how many orders it follows at once.
for await (const ev of bkbn.watch()) {
if (ev.type === 'order' && ev.data.command?.resultAssetId) {
console.log('intermediate frame:', ev.data.command.resultAssetId)
}
}The feature catalog
Features are versioned (semver of the delivered recipe):
const features = await bkbn.features()
// [{ key: 'magic_eraser', version: '4.1.0', kind: 'tool', status: 'ga', … }]Errors
import { RateLimitError, OrderFailedError, IngestError } from '@bkbnlab/api'Short 429 waits are absorbed automatically (Retry-After honoured); long ones
surface as RateLimitError with retryAfterMs. wait() falls back to
polling when the WebSocket can't connect (Upgrade-stripping proxies) — it
does not break on hostile networks.
Key rotation
const { key } = await bkbn.rotateKey() // new secret, returned once —
// the old one dies immediatelyConfiguration
| | |
|---|---|
| BKBNLAB_API_KEY | your sk-… key (or new BkbnClient({ apiKey })) |
| BKBNLAB_API_BASE | HTTP API origin override (default: hosted API) |
| BKBNLAB_WS_BASE | WebSocket origin override (default: hosted WS tier) |
API reference
| Method | Returns | Notes |
|---|---|---|
| new BkbnClient({ apiKey?, base?, fetch? }) | client | env fallbacks: BKBNLAB_API_KEY, BKBNLAB_API_BASE |
| bkbn.upload(input, { filename?, contentType? }?) | Promise<assetId> | input: path (node) / Uint8Array / Blob/File; resolves when ingest is ready, throws IngestError |
| bkbn.order(options) | Promise<OrderHandle> | submits one scene; format/quality pick the delivery container |
| bkbn.orderHandle(uuid) | OrderHandle | reattach by id (no request) |
| bkbn.getOrder(uuid) | Promise<OrderSummary> | current state (one poll) |
| order.wait({ onProgress?, timeoutMs?, signal? }?) | Promise<OrderResult> | SSE, poll fallback; throws OrderFailedError |
| order.events({ signal? }?) | AsyncGenerator<FeedEvent> | raw events for this order, off the shared WS feed |
| order.get() | Promise<OrderSummary> | one poll |
| result.bytes() / result.save(path) | | save is node-only |
| result.export({ format, quality?, longEdge?, timeoutMs?, signal? }) | Promise<ExportResult> | another version of this delivery; resolves when ready (feed, poll fallback) |
| bkbn.export(assetId, options) | Promise<ExportResult> | the same, by asset id — an upload, or any past delivery |
| export.url / export.bytes() / export.save(path) | | url needs no header; save is node-only |
| bkbn.batch(options) | Promise<BatchHandle> | atomic server-side validation |
| bkbn.batchHandle(uuid) | BatchHandle | reattach by id |
| bkbn.getBatch(uuid) | Promise<BatchState> | current aggregate (one poll) |
| batch.wait({ onProgress?, onOrderDone?, timeoutMs?, signal? }?) | Promise<BatchResults> | one SSE for the shoot |
| batch.events() / batch.get() | | raw stream / aggregate poll |
| results.completed / results.failed / results.saveAll(dir) | | a failed scene never throws |
| bkbn.watch({ signal? }?) | AsyncGenerator<FeedEvent> | infinite account feed |
| bkbn.listOrders({ status?, limit? }?) | Promise<OrderSummary[]> | snapshot of my queue |
| bkbn.features() | Promise<FeatureSpec[]> | versioned catalog |
| bkbn.rotateKey() | Promise<{ key, … }> | new secret, returned once |
Runnable examples: examples/ (quickstart.mjs, batch.mjs, watch.mjs).
Smoke — verify a real stack
BKBNLAB_API_KEY=sk-… npm run smokeSelf-contained conformance run against a live stack (sample image included): catalog, upload, order with realtime proof (feed events counted during the run), download byte-check, batch with a per-scene override, typed errors. Exits non-zero on any failure — usable as a deploy gate.
