TypeScript SDK
Installation
npm install rheo-sdkpnpm add rheo-sdkyarn add rheo-sdkRequires Node.js 18 or later.
Setup
import { RheoClient } from 'rheo-sdk'
const rheo = new RheoClient({ apiKey: process.env.RHEO_API_KEY!, webhookSecret: process.env.RHEO_WEBHOOK_SECRET!, // optional, required for webhook verification})| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | required | API key from the Rheo partner dashboard |
webhookSecret | string | — | whsec_... secret for webhook signature verification |
baseUrl | string | https://market.rheo.se | Override for staging or self-hosted |
timeout | number (ms) | 30000 | Per-request timeout |
maxRetries | number | 3 | Max retries on 429 / 5xx |
partnerAccount | string | — | Reseller member ID — scopes all item calls to that account |
Items
Push a single item
const result = await rheo.items.upsert('ERP_PART_12345', { title: 'Volvo XC90 bromsok fram vänster — 2018', description: 'Testad och fungerande. Skick B.', price: 950, shippingCost: 149, weightKg: 3.2, imageUrls: [ 'https://cdn.example.com/parts/12345/1.jpg', 'https://cdn.example.com/parts/12345/2.jpg', ], domain: { domain: 'auto_parts', vehicle: { manufacturer: 'Volvo', model: 'XC90', year: 2018, vehicleType: 'Bil', }, part: { name: 'Bromsok fram vänster', oemNumber: '31400452', catalogCodes: { recopart: '7118' }, }, conditionGrade: 'B', }, autoPublishTradera: true, useAiEnhancement: true,})// result.success === true// result.itemId — Rheo's internal UUIDupsert creates or updates by externalId. The same call works for both new items and updates — Rheo resolves the existing record by your ID.
Get item status
const item = await rheo.items.get('ERP_PART_12345')// item.rheoStatus — 'draft' | 'active' | 'sold' | 'not_listed' | 'archived'// item.traderaAdUrl — direct link to the live Tradera auction// item.price — current price in SEKUpdate price
Updates the price and queues a Tradera bid-start update if the item is live.
await rheo.items.updatePrice('ERP_PART_12345', { price: 850 })Update status
await rheo.items.updateStatus('ERP_PART_12345', { status: 'not_listed' })// or:await rheo.items.updateStatus('ERP_PART_12345', { status: 'sold' })Delete
Removes the item. If a Rheo order is in progress the item is archived instead of deleted.
await rheo.items.delete('ERP_PART_12345')Batch operations
Push up to 500 items in a single call. Background processing is identical to single upserts.
const parts = await dms.getPartsForVehicle('YV1CZ714581234567')
const result = await rheo.items.batchUpsert({ items: parts.map((part) => ({ externalId: `VIN-YV1CZ714581234567-${part.partNumber}`, parentExternalId: 'VIN-YV1CZ714581234567', title: part.name, description: part.notes, price: part.askingPrice, shippingCost: 149, imageUrls: part.photos.map((p) => p.url), domain: { domain: 'auto_parts', vehicle: { manufacturer: 'Volvo', vehicleType: 'Bil' }, part: { name: part.name, oemNumber: part.oemNumber }, conditionGrade: part.grade, }, metadata: { locationBin: part.binLocation, dismantledBy: part.technicianId, }, autoPublishTradera: part.grade !== 'C', })),})
console.log(`Accepted: ${result.accepted}, rejected: ${result.rejected}`)result.errors.forEach((e) => console.error(`${e.externalId}: ${e.reason}`))Querying items
List items with optional filters. Uses cursor pagination.
// All active parts for a vehicleconst page = await rheo.items.list({ parentExternalId: 'VIN-YV1CZ714581234567', status: 'active', limit: 100,})
// Incremental sync — items changed in the last 24 hoursconst updated = await rheo.items.list({ updatedSince: new Date(Date.now() - 86_400_000),})
// Paginatelet cursor: string | undefineddo { const page = await rheo.items.list({ limit: 100, cursor }) // process page.items ... cursor = page.nextCursor} while (cursor)Container summary
Aggregate view across all children of a container (e.g. a donor vehicle).
const summary = await rheo.items.summary('VIN-YV1CZ714581234567')// summary.childCount — total part count// summary.activeCount — parts currently live on Tradera// summary.soldCount — parts sold// summary.draftCount — parts not yet listed// summary.listedValue — sum of active listing prices (SEK)// summary.soldValue — gross value of sold parts (SEK)List children
The parts under a container.
const { children, count } = await rheo.items.children('VIN-YV1CZ714581234567')children.forEach((c) => console.log(`${c.externalId} — ${c.status} — ${c.price} SEK`))Item history
Lifecycle milestones for an item, newest first.
const { events } = await rheo.items.history('ERP_PART_12345')events.forEach((e) => console.log(`${e.createdAt} — ${e.eventType}`, e.data))Vehicle hierarchy
Model donor vehicles as container items. Parts point to their vehicle via parentExternalId.
// 1. Register the donor vehicleawait rheo.items.upsert('VIN-YV1CZ714581234567', { type: 'container', title: 'Volvo XC90 II 2018 — VIN YV1CZ714581234567', metadata: { vin: 'YV1CZ714581234567', make: 'Volvo', model: 'XC90 II', year: 2018, receivedAt: new Date().toISOString(), },})
// 2. Push all parts in one batchawait rheo.items.batchUpsert({ items: parts.map((p) => ({ externalId: `VIN-YV1CZ714581234567-${p.partNumber}`, parentExternalId: 'VIN-YV1CZ714581234567', type: 'item', // ... rest of fields })),})Container items are not listed for sale — they aggregate metadata and revenue from their children.
Metadata
Attach arbitrary key-value pairs to any item. Rheo stores and returns them verbatim — they are never processed. Use them for internal data (bin locations, technician IDs, cost prices) that you want available in webhook payloads and query responses.
await rheo.items.upsert('ERP_PART_12345', { // ... metadata: { locationBin: 'A-12-3', dismantledBy: 'erik', dismantledAt: '2026-05-27T09:15:00Z', internalCost: 200, },})Limits: 50 keys per item, 1 KB per value.
Reseller routing
If you manage several Rheo accounts under one reseller API key (e.g. a chain of
dismantling yards), pass partnerAccount as a per-call option to route a single
request to a specific member. The value is the reseller’s external_id for that
member — the reference you set in the business Managed accounts UI. It overrides
any client-level partnerAccount:
// One client + key, many yardsawait rheo.items.upsert('10-PART-001', data, { partnerAccount: '10' })await rheo.items.updatePrice('10-PART-001', { price: 800 }, { partnerAccount: '10' })const yard = await rheo.items.list({ status: 'active' }, { partnerAccount: '10' })Every item method takes the optional { partnerAccount } argument. Set
partnerAccount on RheoClient instead to scope every call to a single account.
See the Reseller model for how membership and billing work.
Webhooks
Express middleware
import express from 'express'import { RheoClient } from 'rheo-sdk'
const rheo = new RheoClient({ apiKey: process.env.RHEO_API_KEY!, webhookSecret: process.env.RHEO_WEBHOOK_SECRET!,})
const app = express()
// Mount before any body-parser — the middleware buffers the raw body itselfapp.post('/webhooks/rheo', rheo.webhooks.middleware(), (req, res) => { const event = rheo.webhooks.fromRequest(req)
switch (event.eventType) { case 'item.sold': { const { externalId, salePrice, account } = event.data // account?.memberExternalId tells you which yard (reseller endpoints) — update your DMS break } case 'listing.created': { const { externalId, traderaAdUrl } = event.data // record Tradera link break } case 'listing.failed': { const { externalId, error } = event.data // alert operations team break } }
res.sendStatus(200)})fromRequest(req, secret?) accepts an optional secret (or array of secrets) to verify
with the receiving endpoint’s secret — see per-endpoint secrets.
Manual verification
If you manage your own body parsing:
app.post( '/webhooks/rheo', express.raw({ type: 'application/json' }), (req, res) => { const event = rheo.webhooks.verify( req.body, // Buffer — raw bytes, not parsed JSON req.headers['x-rheo-signature'] as string, ) // event is typed as RheoEvent res.sendStatus(200) },)Always pass the raw request body bytes to verify(). Re-serializing a parsed JSON object will produce a different byte sequence and fail the HMAC check.
Per-endpoint secrets
Each webhook endpoint has its own signing secret. Verify with the secret of the
endpoint that received the event. Pass it as the third argument to verify() (it
overrides the client-level webhookSecret), or pass an array to accept any of several
secrets during rotation:
const event = rheo.webhooks.verify(rawBody, signature, endpointSecret)
// During secret rotation — succeeds if either matches:const rotating = rheo.webhooks.verify(rawBody, signature, [oldSecret, newSecret])See Webhooks for managing multiple endpoints, event-type
filters, and the account field added to every payload for reseller routing.
Error handling
import { RheoApiError, RheoRateLimitError, RheoWebhookSignatureError } from 'rheo-sdk'
try { await rheo.items.upsert('PART_001', { title: 'Test', price: 100, shippingCost: 50 })} catch (err) { if (err instanceof RheoRateLimitError) { console.log(`Rate limited. Retry after ${err.retryAfter ?? 'unknown'} seconds`) } else if (err instanceof RheoApiError) { console.error(`API error ${err.status}: ${err.message}`) // err.code — machine-readable error code (when available) // err.requestId — x-request-id header, useful for support } else { throw err }}The SDK retries 429 and 5xx responses automatically (up to maxRetries). RheoRateLimitError is only thrown if retries are exhausted.
TypeScript types
The SDK exports all types. Import them for use in your own type definitions.
import type { RheoEvent, ItemSoldEvent, DomainObject, DomainObjectAutoParts, RheoItemStatus, BatchUpsertItem,} from 'rheo-sdk'
function handleSale(event: ItemSoldEvent) { // event.data.externalId, event.data.salePrice, event.data.platform}
function isAutoPart( domain: DomainObject,): domain is DomainObjectAutoParts { return domain.domain === 'auto_parts'}
// Narrowing gives full typing on the nested attributes:function oemNumberOf(domain: DomainObject): string | undefined { return isAutoPart(domain) ? domain.part?.oemNumber : undefined}RheoEvent discriminated union
Every event is { eventId, timestamp, eventType, data }. The data object always
carries the shared fields below plus an additive account; per-event fields are merged in.
interface EventAccount { rheoUserId: string; memberExternalId?: string }
interface EventData { externalId: string rheoItemId: string platform: string // 'tradera' | 'rheo' | 'rheo_stripe' | 'rheo_swish' salePrice: number // 0 for non-sale events currency: string account?: EventAccount // memberExternalId present on reseller (members) endpoints}
type RheoEvent = | { eventType: 'item.created'; timestamp: string; eventId: string; data: EventData & { type?: 'item' | 'container' } } | { eventType: 'item.images_ready'; timestamp: string; eventId: string; data: EventData & { imagesProcessed?: number } } | { eventType: 'listing.created'; timestamp: string; eventId: string; data: EventData & { traderaAdId?: string; traderaAdUrl?: string } } | { eventType: 'listing.ended'; timestamp: string; eventId: string; data: EventData & { traderaAdId?: string } } | { eventType: 'listing.failed'; timestamp: string; eventId: string; data: EventData & { error?: string } } | { eventType: 'item.sold'; timestamp: string; eventId: string; data: EventData & { traderaAdId?: string } }The TypeScript compiler narrows event.data automatically in switch (event.eventType) blocks.
CommonJS / ESM
The package ships both CommonJS (dist/index.js) and ESM (dist/index.mjs) builds. It works in Node.js CJS projects without any configuration.
// CommonJSconst { RheoClient } = require('rheo-sdk')
// ESM / Bun / Denoimport { RheoClient } from 'rheo-sdk'