npm package: @lumbu/client
Description: Typed fetch client for the Lumbu salon management API. Zero dependencies — works in Node.js, edge runtimes, and browsers.
Latest version: 0.2.1

# @lumbu/client

Typed fetch client for the Lumbu salon management API. Zero dependencies — works in Node.js, edge runtimes, and browsers.

## Install

```bash
npm install @lumbu/client
```

## Setup

```ts
import { createLumbuClient } from "@lumbu/client";

const lumbu = createLumbuClient({
  baseUrl: "https://app.lumbu.pt",
  token: process.env.LUMBU_API_TOKEN!,
});
```

## Read endpoints

All read endpoints return cursor-paginated responses: `{ data, has_more, next_cursor }`.

```ts
// Single page
const page = await lumbu.appointments.list({
  status: "confirmed",
  start_at_gte: "2026-04-01T00:00:00Z",
  limit: 50,
});

// Auto-paginate (async generator)
for await (const page of lumbu.appointments.listAll({ status: "confirmed" })) {
  for (const apt of page.data) {
    console.log(apt.id, apt.start_at);
  }
}
```

### Available read endpoints

| Endpoint | Method | Returns |
|----------|--------|---------|
| `lumbu.appointments.list(params?)` | GET | `Appointment[]` |
| `lumbu.services.list(params?)` | GET | `Service[]` |
| `lumbu.staff.list(params?)` | GET | `Staff[]` |
| `lumbu.locations.list(params?)` | GET | `Location[]` |
| `lumbu.waitlist.list(params?)` | GET | `WaitlistEntry[]` |
| `lumbu.staffHours.list(params?)` | GET | `StaffHoursEntry[]` |
| `lumbu.teamMembers.list(params?)` | GET | `TeamMember[]` |
| `lumbu.staffTimeOff.list(params?)` | GET | `StaffTimeOffEntry[]` |
| `lumbu.appointmentFeedback.list(params?)` | GET | `AppointmentFeedbackEntry[]` |
| `lumbu.serviceCategories.list()` | GET | `ServiceCategory[]` |

All list endpoints also have a `.listAll(params?)` method that returns an `AsyncGenerator` for auto-pagination.

## Booking flow

```ts
// 1. Get available slots
const { data: slots } = await lumbu.booking.getSlots({
  service_id: "svc-123",
  from: "2026-04-05",
  days: 7,
});

// 2. Book an appointment
const result = await lumbu.booking.book({
  service_id: "svc-123",
  staff_id: "staff-456",
  start_at: "2026-04-05T10:00:00Z",
  customer_name: "Maria Silva",
  customer_email: "maria@example.com",
});
// result: { appointment_id, access_token, token_expiry }

// 3. Cancel (requires the access token from booking)
await lumbu.booking.cancel({
  appointment_id: result.appointment_id,
  access_token: result.access_token,
});

// 4. Reschedule
const rescheduled = await lumbu.booking.reschedule({
  appointment_id: result.appointment_id,
  access_token: result.access_token,
  new_start_at: "2026-04-06T14:00:00Z",
});
// rescheduled: { new_end_at, access_token, token_expiry }
```

## Customer portal

Look up a customer's appointments using their access token:

```ts
const { appointment, history } = await lumbu.customer.getAppointments({
  access_token: "token-from-booking-confirmation-email",
});
// appointment: current appointment with service/staff names
// history: up to 20 past appointments for the same customer
```

## Error handling

```ts
import { createLumbuClient, LumbuApiError } from "@lumbu/client";

try {
  await lumbu.booking.book({ ... });
} catch (err) {
  if (err instanceof LumbuApiError) {
    console.log(err.status);  // 401, 400, 500, etc.
    console.log(err.code);    // "UNAUTHORIZED", "INVALID_REQUEST", etc.
    console.log(err.message); // Human-readable message

    if (err.isUnauthorized) { /* re-authenticate */ }
    if (err.isRateLimited)  { /* back off */ }
  }
}
```

## Custom fetch

Inject a custom `fetch` function for testing or non-standard environments:

```ts
const lumbu = createLumbuClient({
  baseUrl: "https://app.lumbu.pt",
  token: "test-token",
  fetch: myCustomFetch,
});
```

## API reference

### Configuration

```ts
interface LumbuClientConfig {
  baseUrl: string;           // Lumbu API base URL
  token: string;             // Bearer token (PUBLIC_API_TOKEN)
  fetch?: typeof fetch;      // Optional custom fetch function
}
```

### Pagination

```ts
interface PaginatedResponse<T> {
  data: T[];
  has_more: boolean;
  next_cursor: string | null;
}
```

Pass `cursor` from a previous response to get the next page:

```ts
const page1 = await lumbu.appointments.list({ limit: 50 });
if (page1.has_more) {
  const page2 = await lumbu.appointments.list({ limit: 50, cursor: page1.next_cursor! });
}
```

## License

MIT