JavaScript/TypeScript toolkit for the Booqable API.
Booqable is a rental management platform that helps businesses manage their rental inventory, customers, and orders. This library provides a JavaScript interface to interact with all Booqable API endpoints.
It is a faithful port of the booqable.rb Ruby gem with zero runtime dependencies: it is built on the global fetch and Web Crypto, so it runs in Node.js (≥ 20), browsers, and edge runtimes such as Cloudflare Workers.
- Installation
- Making requests
- Authentication
- Configuration
- Pagination
- Rate limiting
- Resources
- Parsing JSON:API payloads
- Strict attribute reads
- Error handling
- Development
npm install @booqable/client
# or
bun add @booqable/clientAPI methods are available on the module-level default export (consuming module-level configuration) or on a client instance.
import Booqable from '@booqable/client'
// Provide authentication credentials
Booqable.configure({
apiKey: 'your_api_key_here',
companyId: 'your_company_id',
})
// Fetch orders
const orders = await Booqable.orders.list()or
import { BooqableClient } from '@booqable/client'
// Create a client instance
const client = new BooqableClient({
apiKey: 'your_api_key_here',
companyId: 'your_company_id',
})
// Fetch orders
const orders = await client.orders.list()Booqable supports several authentication methods to suit different use cases:
The simplest way to authenticate is with an API key:
Booqable.configure({
apiKey: 'your_api_key_here',
companyId: 'your_company_id',
})Generate your API key from your Booqable account settings. Learn more about API keys.
For applications that need to act on behalf of multiple users:
const client = new BooqableClient({
clientId: 'your_oauth_client_id',
clientSecret: 'your_oauth_client_secret',
companyId: 'your_company_id',
readToken: async () => {
// Return the stored token hash
return JSON.parse(await fs.readFile('token.json', 'utf8'))
},
writeToken: async (token) => {
// Store the token hash
await fs.writeFile('token.json', JSON.stringify(token))
},
})
// Complete the OAuth flow (e.g. in your callback handler)
await client.authenticateWithCode(params.code)Access tokens are refreshed automatically when they are expired or about to
expire (a 60-second buffer absorbs network latency and clock skew), and the
refreshed token is persisted through writeToken.
When multiple processes share the same OAuth token (e.g. the same installation
serving concurrent requests), pass an aroundRefreshToken callable to
serialize the read + expiry-check + refresh sequence. The client awaits the
callable once per request; the host application decides how to lock.
new BooqableClient({
// ...other oauth options...
aroundRefreshToken: async (refresh) => {
await installation.withAdvisoryLock(async () => {
await installation.reload()
await refresh()
})
},
})The library itself has no locking dependency — aroundRefreshToken is just an
async function that receives the refresh sequence to run.
For server-to-server communication requiring enhanced security (request signing — every request carries a unique JWT bound to its method, path, and body):
const client = new BooqableClient({
singleUseToken: 'your_token_id',
singleUseTokenAlgorithm: 'HS256',
singleUseTokenSecret: 'your_signing_secret',
singleUseTokenCompanyId: 'company_uuid',
singleUseTokenUserId: 'user_uuid',
companyId: 'your_company_id',
})Supports HS256 (HMAC), RS256 (RSA), and ES256 (ECDSA) algorithms. RS256/ES256
private keys must be PEM-encoded PKCS#8 (-----BEGIN PRIVATE KEY-----).
Learn more about request signing.
For anything the built-in methods don't cover (e.g. attaching a
session-specific header when talking to the API through a proxy), pass a
custom auth strategy — the JavaScript analog of booqable.rb's custom
middleware support. It takes precedence over the built-in methods and is
called before every request (and every retry attempt):
const client = new BooqableClient({
apiEndpoint: '/api/booqable/proxy', // e.g. an authenticated same-origin proxy
auth: {
apply(request) {
request.headers['Authorization'] = `Bearer ${mySessionHandle}`
},
},
})In Node.js, all authentication options can be configured via environment variables:
export BOOQABLE_API_KEY="your_api_key_here"
export BOOQABLE_COMPANY_ID="your_company_id"
export BOOQABLE_CLIENT_ID="your_oauth_client_id"
export BOOQABLE_CLIENT_SECRET="your_oauth_client_secret"Booqable.configure({
apiKey: 'your_api_key_here',
companyId: 'your_company_id',
apiDomain: 'booqable.com', // Default
apiVersion: 4, // Default
perPage: 25, // Default pagination size
autoPaginate: true, // Auto-fetch all pages
})Options are camelCase; the snake_case spellings used by booqable.rb
(api_key, company_id, …) are accepted as aliases. Unknown options throw
an error with a "did you mean" suggestion.
const client = new BooqableClient({
apiKey: 'your_api_key_here',
companyId: 'your_company_id',
perPage: 50,
autoPaginate: false,
})// Fetch first page
const orders = await Booqable.orders.list({ page: { size: 25, number: 1 } })
// Fetch next page
const nextOrders = await Booqable.orders.list({ page: { size: 25, number: 2 } })Booqable.configure({ autoPaginate: true })
// This will automatically fetch ALL orders across all pages
const allOrders = await Booqable.orders.list()Rate limit information is tracked from every response:
await Booqable.orders.list()
const rateLimit = Booqable.rateLimit()
console.log(`Remaining requests: ${rateLimit.remaining}`)
console.log(`Limit: ${rateLimit.limit}`)
console.log(`Resets in: ${rateLimit.resetsIn} seconds`)When the API rate-limits a request, the thrown TooManyRequests error carries
the same information on its context property.
Every idempotent request is retried up to two times when it fails with a server error or a network error, with exponential backoff.
Booqable.configure({ noRetries: true }) // Disable automatic retriesAll Booqable API resources are available through a consistent interface:
// List orders with filtering and includes
const orders = await Booqable.orders.list({
include: 'customer,items',
filter: { status: 'reserved' },
sort: '-created_at',
})
// `all` is an alias for `list`
const sameOrders = await Booqable.orders.all({ filter: { status: 'reserved' } })
// Find a specific order
const order = await Booqable.orders.find('order_id', { include: 'customer,items' })
order.customer.name
// Create an order
const newOrder = await Booqable.orders.create({
starts_at: '2024-01-01T00:00:00Z',
stops_at: '2024-01-02T00:00:00Z',
status: 'draft',
})
newOrder.status // => 'draft'
// Update an order
const updatedOrder = await Booqable.orders.update('order_id', { stops_at: '2024-01-03T00:00:00Z' })
// Delete an order
const deletedOrder = await Booqable.orders.delete('order_id')
deletedOrder.id // => 'order_id'const customers = await Booqable.customers.list({ filter: { name: 'John' }, sort: 'created_at' })
const customer = await Booqable.customers.create({ name: 'John Doe', email: 'john@example.com' })
await Booqable.customers.update('customer_id', { name: 'Jane Doe' })
await Booqable.customers.delete('customer_id')Core Resources:
orders,customers,products,itemsemployees,companies,locationspayments,documents
Inventory Management:
inventory_levels,stock_items,stock_adjustmentstransfers,plannings,clusters
Configuration:
settings,properties,tax_ratespayment_methods,email_templates
And many more... See the full resource list for all available endpoints. Resources can also be reached by name via client.resource('orders'), and lower-level access is available through client.get/post/put/patch/delete/request/paginate.
JSON:API formatted data (such as webhook payloads or raw API responses) can be parsed into plain objects with flattened attributes and populated relationships:
const payload = {
data: {
id: 'abc-123',
type: 'customers',
attributes: {
name: 'John Doe',
email: 'john@example.com',
created_at: '2024-01-15T10:30:00Z',
},
},
}
const customer = Booqable.parseResource(payload)
customer.id // => "abc-123"
customer.name // => "John Doe"
customer.email // => "john@example.com"
customer.created_at // => Date object (2024-01-15T10:30:00Z)const payload = {
data: {
id: 'order-123',
type: 'orders',
attributes: { status: 'reserved' },
relationships: {
customer: { data: { id: 'customer-456', type: 'customers' } },
},
},
included: [
{ id: 'customer-456', type: 'customers', attributes: { name: 'Jane Smith' } },
],
}
const order = Booqable.parseResource(payload)
order.status // => "reserved"
order.customer.name // => "Jane Smith"parseResource accepts a JSON string or an already-parsed object, and is also
available on client instances and as a named export. deserializeResource is
an alias.
Reading an attribute that is absent from the API payload throws
MissingAttribute instead of silently returning undefined, so typos and
renamed API fields fail loudly:
customer.name // => "John Doe"
customer.full_name // throws MissingAttribute (key absent from payload)An attribute that is present in the payload with a null value still returns null — only absent keys throw:
order.customer // => null when the payload contains "customer": nullMissingAttribute subclasses TypeError. To probe for an attribute that may
be absent, use the in operator or the raw attributes:
'customer' in order // => false when the key is absent (lenient probe)
order.$attrs // => the underlying plain objectStrictness can be disabled per client with strictAttributes: false.
API failures throw typed errors, all subclasses of BooqableError:
import Booqable, { BooqableError, NotFound, TooManyRequests } from '@booqable/client'
try {
await Booqable.orders.find('invalid_id')
} catch (error) {
if (error instanceof NotFound) {
// 404
} else if (error instanceof TooManyRequests) {
console.log(error.context?.resetsIn) // rate limit info
} else if (error instanceof BooqableError) {
console.log(error.message) // "GET https://…: 422 - …"
console.log(error.responseStatus) // HTTP status
console.log(error.errors) // JSON:API validation errors
}
}The full hierarchy mirrors booqable.rb, including body-based refinements such
as UnknownAttribute, InvalidFilter, CompanyNotFound, FeatureNotEnabled,
TokenRevoked, RefreshTokenRevoked, and ReadOnlyMode.
After checking out the repo, run npm install to install dependencies.
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Lint
npm run lint
# Build (emits ESM + type declarations to dist/)
npm run buildBug reports and pull requests are welcome on GitHub at https://github.com/booqable/booqable.js.
This library aims to support and is tested against Node.js 20, 22, and 24,
and targets any runtime with fetch and Web Crypto (browsers, Cloudflare
Workers, Deno, Bun).
The library is available as open source under the terms of the MIT License.
Questions? Check out the Booqable API documentation or open an issue.