@balima/booking-sdk
v1.0.2
Published
TypeScript SDK for the Booking API
Readme
Booking SDK (@balima/booking-sdk)
TypeScript SDK for the Booking API
Features
- Dual backend — pick the target version at initialization:
version: "v2"version: "v1"
- Typed end-to-end — every request/response has type inference.
- Auth — automatically sends the
x-api-keyheader for/api/*routes. - Isomorphic — works in the browser (React/Next.js client) and server (Node 18+).
- Structured errors —
BookingEngineErrorcarriesstatusCode,path,message. - Per-folder structure — each resource has its own folder with an
index.ts(class) and a<name>Type.ts(types) file.
Installation
npm install @balima/booking-sdkRequires Node 18+ (global fetch), or react as an optional peer dependency.
Quick Start
import { BookingSDK } from "@balima/booking-sdk";
const bookingClient = new BookingSDK({
apiKey: process.env.API_KEY,
version:// "v1" or "v2"
});Server-only Singleton (Next.js)
Security rule: the
apiKeyis a secret and must only live on the server. Never prefix it withNEXT_PUBLIC_and never import it from a Client Component.
Create a shared server-only singleton that is initialized once per server process, then reused by every Server Action / Route Handler.
/**
* Server-only singleton for the Booking SDK.
*
* IMPORTANT:
* - Place this file under `src/lib/booking/` in your Next.js app.
* - It must ONLY be imported from Server Components, Server Actions,
* Route Handlers, or Server-only utilities.
* - The `apiKey` (secret) lives in `process.env` and is NEVER exposed
* to the browser.
*
* Use "server-only" (npm: server-only) or a `.server.ts` suffix to enforce
* server-only usage. The classic `"use server"` directive is NOT correct here;
* that directive is for Server Actions (async functions), not for objects.
*/
import "server-only";
import { BookingSDK } from "@balima/booking-sdk";
/**
* Single shared instance — initialized once per server process.
* Reused by every route handler / server action in the app.
*/
export const bookingServer = new BookingSDK({
apiKey: process.env.API_KEY,
version: "v2",
});
/**
* Optional: expose a lazily-created singleton to avoid double-render
* issues and to keep a single reference across hot reloads in dev.
*/
export function getBookingServer(): BookingSDK {
return bookingServer;
}Files in
src/lib/booking/are executed on the server. To make Next.js fail fast if a Client Component ever imports from them, runnpm install server-onlyand keep theimport "server-only"line above.
Calling it from a Server Action
// src/app/actions/booking.ts
"use server";
import { bookingServer } from "@/lib/booking/server";
export async function checkAvailability(input: {
roomId: number;
start: string;
end: string;
adult?: number;
child?: number;
}) {
return bookingServer.availability.check(input);
}Calling it from a Client Component
// src/components/AvailabilityForm.tsx
"use client";
import { checkAvailability } from "@/app/actions/booking";
export function AvailabilityForm() {
async function handleCheck(formData: FormData) {
const res = await checkAvailability({
roomId: Number(formData.get("roomId")),
start: String(formData.get("start")),
end: String(formData.get("end")),
adult: 1,
});
// res is a plain serializable object — safe to render/setState
console.log(res);
}
return (
<form action={handleCheck}>
<input name="roomId" placeholder="527948" />
<input name="start" type="date" />
<input name="end" type="date" />
<button type="submit">Check availability</button>
</form>
);
}Server Actions must return plain objects — never return a raw
fetch(...)Response(Next.js cannot serialize it back to a Client Component). Have the Server Action call the SDK and return its result.
Usage Examples
Check availability
const res = await bookingClient.availability.check({
roomId: 458628,
start: "2026-08-01",
end: "2026-08-05",
adult: 2,
});
if (res.success && res.data) {
console.log(res.data.name, res.data.usd, res.data.idr);
}Check availability for multiple rooms
const res = await bookingClient.availability.checkMultiple({
roomId: [458628, 458629],
start: "2026-08-01",
end: "2026-08-05",
adult: 2,
});Validate a coupon
const res = await bookingClient.coupons.check({
code: "PROMO20",
roomId: 458628,
usd: 120,
start: "2026-08-01",
end: "2026-08-05",
email: "[email protected]",
});
// res.data.discount.usd, res.data.finalPrice.idrCreate a payment
const res = await bookingClient.payment.create({
roomId: 458628,
firstname: "John",
lastname: "Doe",
email: "[email protected]",
phone: "+628123456789",
start: "2026-08-01",
end: "2026-08-05",
adult: 2,
child: 0,
usd: 480,
});
// res.data => payment link URLDynamic payment per tenant
// v2:
const res = await bookingClient.payment.createDynamic("stripe", {
/* ... */
});
// v1 — uses `offer` instead of `provider`:
const res1 = await bookingClient.payment.createDynamic("stripe", {
/* ... */
offer: { offerId: 1, offerName: "Standard Rate" },
});Checkout (v2 only)
const checkout = await bookingClient.checkout?.get("encryptedSessionId", {
ref: "...",
sig: "...",
});Error Handling
import { BookingSDK, BookingEngineError } from "@balima/booking-sdk";
try {
await bookingClient.availability.check({
/* ... */
});
} catch (err) {
if (err instanceof BookingEngineError) {
console.error(err.statusCode, err.message, err.path);
}
}A { success: false } response may not throw — check the success and message
fields on the response.
Configuration
| Option | Type | Default | Description |
| --------- | -------------- | ------------------- | -------------------------------------- |
| apiKey | string | — | x-api-key header for /api/* routes |
| version | "v1" \| "v2" | "v2" | Target backend version |
| baseUrl | string | per-version default | Override the API base URL |
| timeout | number | 15000 | Timeout in ms |
| fetch | fn | global | Custom fetch (for testing) |
Development
npm install
npm run typecheck # tsc --noEmit
npm run build # tsup → dist (esm + cjs + d.ts)