@broberg/complimenta-sdk
v0.3.0
Published
Typed SDK for the Complimenta (ComplimentaWork) booking API — read therapists/services/clinics/calendar-slots and create/cancel bookings. Generated from Complimenta's OpenAPI spec; OAuth2 client-credentials; runs in Next.js and Bun.
Readme
@broberg/complimenta-sdk
Typed SDK for the Complimenta (ComplimentaWork) booking system — read
therapists / services / clinics / calendar-slots and create + cancel bookings.
Types are generated from Complimenta's own OpenAPI spec, so the contract is
exact and complete. Zero runtime dependencies, native fetch → runs unchanged
in Next.js and Bun.
Built in the fdaa monorepo (packages/complimenta), published to npm as
@broberg/complimenta-sdk. One SDK, consumed across the FysioDanmark Aalborg
fleet: the fdaa platform, the future fdaa-sundhed repo, and the native apps.
Auth
OAuth2 client-credentials: the SDK exchanges clientId+clientSecret at the
auth host's /oauth2/token for a Bearer JWT (cached + auto-refreshed) and sends
it on every /api/v1/* call.
import { createComplimentaClient } from "@broberg/complimenta-sdk";
const cam = createComplimentaClient({
baseUrl: process.env.COMPLIMENTA_BASE_URL!, // scheme+host, no path
clientId: process.env.COMPLIMENTA_CLIENT_ID!,
clientSecret: process.env.COMPLIMENTA_CLIENT_SECRET!,
});
const therapists = await cam.listUsers(); // GET /api/v1/users
const services = await cam.listClinicServices(); // GET /api/v1/clinic-services
const slots = await cam.listUserCalendarSlots( // GET /api/v1/users/{userId}/calendar-slots
therapists.items[0].id,
{ endsOnOrAfter: "2026-06-16T08:00:00", endsBefore: "2026-06-23T17:00:00" }, // end-time window (required)
);
const bookings = await cam.listBookings({ // GET /api/v1/bookings
startsAtOrAfter: "2026-06-16T00:00:00", // from-date window (required, ≤ 3 months)
startsBefore: "2026-06-23T00:00:00",
});
// also: listClinics/getClinic · listPublicServices · getBooking ·
// createBooking(body) · cancelBooking(id)cam.request(method, path, { query, body }) is the typed escape hatch for any of
the ~40 endpoints not yet wrapped in a convenience method.
Three traps the types can't express
Timestamps are UTC — always. fromTime/toTime are naive (no zone) but the
API reads them as UTC, which the spec now says outright ("Must be UTC").
A Danish 09:00 is 07:00 in summer and 08:00 in winter. Send local time and
the booking silently lands two hours off.
confirmationSmsMessage bypasses the clinic's template completely — nothing
is merged in, so the text must be COMPLETE on its own (clinic, therapist, date,
time). maxLength: 160 is enforced server-side, but it counts characters, not
messages — and that is the whole trap. 160 characters is one SMS only if every
character is in the GSM 03.38 basic alphabet. æ, ø and å are — but a
typographic dash (—), a curly apostrophe (’) or an ellipsis (…) flips the entire
message to UCS-2 at 67 characters per part, so a 160-character string sails
through Complimenta's check and arrives as three SMS. Nothing on the server
side can catch that.
The SDK deliberately does NOT validate it either: this is a pure generated
transport, and a hand-written check here would be deleted by the next
pnpm generate — a safeguard that disappears silently is worse than none. The
check belongs where the text is composed. Validate before you call; it is the
only validation that exists.
notes is deprecated in favour of bookingDescription (marked
@deprecated in the generated types). Both still send; only one has a future.
Types generated from the spec
The full contract lives in openapi/complimenta-external.json, vendored from
production — GET https://api.complimentawork.dk/v3/api-docs/external.
Vendor production, not demo. The file was originally taken from api-demo, and
demo lags: for a while confirmationSmsMessage existed in production and not in
demo, so the generated types simply did not have it and no consumer could see
it. (The two specs are byte-identical today apart from the auth host — but that
is a coincidence of timing, not a guarantee.)
Regenerate whenever Complimenta updates their API:
pnpm generate # openapi-typescript → src/schema.ts (paths · components · operations)src/schema.ts is generated — do not hand-edit. The convenience methods
derive their request/response types from it, so a spec change surfaces as a
type error at the call-site.
Smoke test (proves the live connection)
cp .env.example .env # COMPLIMENTA_BASE_URL + COMPLIMENTA_CLIENT_ID + COMPLIMENTA_CLIENT_SECRET
bun scripts/smoke.ts # token exchange + GET /calendar-slots