Referensi API

Konvensi base URL, envelope respons ok/data/meta, kode error, versioning, lalu endpoint per-resource Termilo: Services, Availability, Bookings, dan Customers.

Base URL & konvensi#

Worker melayani path dashboard/public secara langsung serta partner API versioned pada /v1. Semua path berikut live:

MethodPathCatatan
POST/book/*Booking publik: config, availability, create.
POST/booking/manage/*Self-service customer via manage token.
GET/api/*Dashboard — butuh sesi (Google login) + RBAC.
GET/api/integrations/*OAuth & aksi provider (Google, Zoom).
GET/v1/*Partner API — Bearer key + X-Termilo-Tenant-Id.
POST/mcp/:tenantIdBranding MCP — Streamable HTTP + Bearer key.
GET/healthHealth check.
Host API publik api.termilo.id
Route /v1/* mewajibkan Authorization: Bearer dan X-Termilo-Tenant-Id. Branding MCP memakai tenant ID pada path dan didokumentasikan di /docs/mcp.

Envelope respons#

Setiap respons dibungkus envelope yang sama. Ini adalah bentuk nyata di kode (lib/http/response.ts): sukses membawa data, error membawa error, dan keduanya selalu membawa meta.requestId.

Sukses

200 OKapplication/json
{
"ok": true,
"data": { … },
"meta": { "requestId": "a1b2c3" }
}

Error

400 Bad Requestapplication/json
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Body gagal validasi.",
"details": [{ "path": "durationMin", "message": "Required" }]
},
"meta": { "requestId": "a1b2c3" }
}
requestId
requestId juga dikembalikan di header respons x-request-id — ia meneruskan x-request-id masuk (≤128 karakter) atau membuat UUID baru. Sertakan dalam laporan masalah.

Kode error#

Error validasi memakai code VALIDATION_ERROR (status 400) dengan details berisi [{path, message}] dari Zod. Kode berikut sudah dipancarkan di kode:

codeHTTPArti
VALIDATION_ERROR400Body/query gagal validasi Zod. details = [{path, message}].
AUTH_REQUIRED401Tidak ada sesi pada endpoint dashboard.
SESSION_EXPIRED401Sesi dashboard sudah lewat masa berlaku.
CSRF_INVALID403Header X-Termilo-CSRF tidak cocok pada request yang mengubah data.
FORBIDDEN403Role tidak punya izin (RBAC).
NOT_FOUND404Resource tidak ditemukan pada tenant ini.
TENANT_INACTIVE403Tenant disabled atau suspended.
SERVICE_INACTIVE403Layanan tidak active untuk dibooking.
BOOKING_SLOT_UNAVAILABLE409Slot sudah terisi saat dicek ulang di bawah lock.
BOOKING_POLICY_BLOCKED409 / 503Transisi lifecycle tidak diizinkan, atau lock tak tersedia.
OAUTH_STATE_INVALID403State OAuth provider tidak valid.
OAUTH_TOKEN_EXCHANGE_FAILED403Penukaran token provider gagal.
CONFIG_MISSING500Konfigurasi server wajib tidak ada.
INTERNAL_ERROR500Galat tak terduga.
Kode error cadangan
Tidak tersediaKode berikut dipesan oleh kontrak API sebagai kosakata error maju, tetapi tidak dipancarkan: ORIGIN_NOT_ALLOWED, BOOKING_IDEMPOTENCY_CONFLICT, PAYMENT_REQUIRED, PROVIDER_NOT_CONNECTED, PROVIDER_NEEDS_REAUTH, PROVIDER_ACTION_PENDING, PROVIDER_ACTION_FAILED, RATE_LIMITED.

Versioning#

Selama MVP, route internal boleh tanpa versi karena frontend dan backend dirilis bersamaan. Aturan yang mengikat:

  • Payload API publik dan webhook membawa versi payload sejak awal.
  • Prefix /v1 dipakai saat partner eksternal mengonsumsi route langsung lewat api.termilo.id.
  • Satu versi maju saja. Tidak ada /v2.

Services#

Layanan (event type) adalah hal yang dipesan. Endpoint dashboard, butuh sesi + RBAC. Dalam standar penamaan Termilo istilahnya event type; di skema dan admin API resource-nya secara harfiah bernama services.

Sudah aktif

MethodPathCatatan
GET/api/servicesList layanan. Role baca.
POST/api/servicesBuat (201). Wajib slug, name, durationMin. Tulis + CSRF.
PATCH/api/services/:serviceIdUpdate sebagian (≥1 field). Tulis + CSRF.

Field body (POST /api/services)

PARAMETERTIPEWAJIBKETERANGAN
slugstringwajibPengenal URL-aman, unik per tenant.
namestringwajibNama layanan yang tampil ke pelanggan.
durationMinnumberwajibDurasi sesi dalam menit.
bufferBeforeMinnumberopsionalJeda sebelum sesi (menit). Bawaan 0.
bufferAfterMinnumberopsionalJeda sesudah sesi (menit). Bawaan 0.
minNoticeMinnumberopsionalBatas pesan sebelum slot mulai (menit, 0–43200). Bawaan 0.
priceAmountnumber | nullopsionalHarga dalam minor units (mis. sen). Nullable.
priceCurrencystringopsionalKode mata uang harga.

Record ServiceAdminRecord mengembalikan: serviceId, tenantId, categoryId, slug, name, description, durationMin, bufferBeforeMin, bufferAfterMin, priceAmount, priceCurrency, status ("active" | "hidden" | "disabled"), createdAtMs, updatedAtMs. Buffer default 0; status default active.

Staff & Resources (modul yang sama)

MethodPathCatatan
GET/api/staffList staf. POST membuat (wajib displayName).
PATCH/api/staff/:staffIdUpdate sebagian. Tulis + CSRF.
GET/api/resourcesList resource. POST membuat (wajib type, name; capacity default 1).
PATCH/api/resources/:resourceIdUpdate sebagian. Tulis + CSRF.

StaffAdminRecord: staffId, tenantId, userId, displayName, email, timezone, status, createdAtMs, updatedAtMs. ResourceAdminRecord: resourceId, tenantId, type, name, locationLabel, capacity, status, ….

Availability#

Slot dihitung dari aturan dikurangi hari libur, blokir waktu, dan booking yang ada. Admin menulis jam operasional, blokir, dan hari libur; pembaca publik membaca slot tanpa auth. Sudah aktif

Admin (sesi + RBAC)

MethodPathCatatan
GET/api/availability-rulesList jam operasional. POST membuat (validasi startMinute < endMinute).
PATCH/api/availability-rules/:ruleIdUpdate aturan mingguan.
GET/api/blocked-timesList blokir waktu. POST membuat.
PATCH/api/blocked-times/:blockIdUpdate blokir.
GET/api/holidaysList hari libur. POST membuat.
PATCH/api/holidays/:holidayIdUpdate hari libur.

AvailabilityRuleAdminRecord: ruleId, tenantId, ownerType ("tenant" | "staff" | "resource"), ownerId, weekday (0-6), startMinute, endMinute, timezone, effectiveFromAtMs, effectiveUntilAtMs, status. Waktu-hari adalah menit integer; weekday 0–6.

Baca publik (tanpa auth)

MethodPathCatatan
GET/book/:tenantSlug/availabilityBaca slot publik (tanpa auth). Query serviceSlug, fromMs, toMs. Jendela maks 31 hari.
curl "https://booking.termilo.id/book/acme/availability\ ?serviceSlug=konsultasi&fromMs=1782864000000&toMs=1783468800000"
200 OKapplication/json
{
"service": { "serviceId": "svc_1", "slug": "konsultasi", "durationMin": 30 },
"finalAuthority": "booking_create_recheck_under_lock",
"slots": [{
"startsAtMs": 1782900000000, "endsAtMs": 1782901800000,
"staffId": "stf_andi", "resourceId": null, "capacityRemaining": 1
}]
}
Slot bukan otoritas final
Respons availability membawa penanda harfiah "finalAuthority": "booking_create_recheck_under_lock". Slot adalah kandidat untuk ditampilkan; ketersediaan final ditentukan ulang di bawah lock saat booking dibuat. Jendela query dibatasi 31 hari.

Bookings#

Tiga jalur: buat booking publik, kelola via manage token customer, dan lifecycle dashboard. Sudah aktif

Config & create publik (tanpa auth)

MethodPathCatatan
GET/book/:tenantSlug/configTenant + daftar layanan publik (tanpa auth).
POST/book/:tenantSlug/bookingsBuat booking. Header Idempotency-Key wajib. 201 baru / 200 replay.

Field body untuk POST /book/:tenantSlug/bookings (email ATAU phone wajib):

PARAMETERTIPEWAJIBKETERANGAN
serviceSlugstringwajibSlug layanan yang dibooking.
startsAtMsnumberwajibWaktu mulai, UTC epoch milidetik.
source"public_page" | "embed"wajibAsal booking.
customer.namestringwajibNama pemesan.
customer.primaryEmailstringopsionalEmail — wajib jika phone kosong.
customer.phonestringopsionalTelepon — wajib jika email kosong.
Idempotency-KeyheaderwajibHeader. Mencegah booking ganda saat retry.
curl -X POST "https://booking.termilo.id/book/acme/bookings" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 4f9a-7c21" \ -d '{ "serviceSlug": "konsultasi", "startsAtMs": 1782900000000, "source": "public_page", "customer": { "name": "Sari", "primaryEmail": "sari@contoh.id" } }'
201 Createdapplication/json
{
"booking": {
"bookingId": "bkg_8sJ2", "status": "held",
"startsAtMs": 1782900000000, "holdExpiresAtMs": 1782900600000
},
"idempotentReplay": false,
"manageToken": "tm_m1.…",
"manageUrl": "https://…/booking/manage/tm_m1.…"
}
Cek ulang slot di bawah lock
Booking baru dibuat berstatus held dengan holdExpiresAtMs = now + 10 menit. Slot dicek ulang di bawah Durable Object lock; jika tidak cocok → BOOKING_SLOT_UNAVAILABLE (409). Replay idempoten mengembalikan 200 (bukan 201).

Manage customer (token bertanda tangan, tanpa sesi)

MethodPathCatatan
GET/booking/manage/:tokenDetail booking + actions {canCancel, canReschedule}. cache-control: private, no-store.
POST/booking/manage/:token/cancelBatalkan via manage token bertanda tangan.
POST/booking/manage/:token/rescheduleBody startsAtMs. Re-lock + cek tumpang-tindih.

manageToken dan manageUrl dikembalikan saat booking dibuat. Token tidak dapat menjangkau /api/* dan tidak membuat sesi.

Lifecycle dashboard (sesi + RBAC)

MethodPathCatatan
GET/api/bookingsList. Query status, fromMs, toMs, limit (default 50, maks 100).
GET/api/bookings/:bookingIdDetail satu booking.
POST/api/bookings/:bookingId/confirmheld → confirmed.
POST/api/bookings/:bookingId/cancelBody reason. held | confirmed → cancelled.
POST/api/bookings/:bookingId/completeconfirmed → completed.
POST/api/bookings/:bookingId/no-showBody reason. confirmed → no_show.
POST/api/bookings/:bookingId/rescheduleBody startsAtMs.

Status: held, confirmed, cancelled, completed, no_show. Terminal: cancelled, completed, no_show. Transisi yang tidak diizinkan mengembalikan BOOKING_POLICY_BLOCKED.

Customers#

Internal dashboard API. Route /api/customers sudah aktif untuk sesi dashboard dan memakai D1 tenant aktif. API publik partner/server-to-server tetap menunggu host /v1.
MethodPathCatatan
GET/api/customersList customer tenant. Query q, limit, cursor.
GET/api/customers/:customerIdDetail customer tenant.
POST/api/customersBuat customer manual. Wajib name; email atau phone opsional sesuai data tenant.
PATCH/api/customers/:customerIdUpdate sebagian customer. Tulis + CSRF.

Field tabel customers yang dipetakan ke dashboard: customerId, tenantId, primaryEmail (nullable), phone (nullable), name, locale, timezone. Pembuatan booking mensyaratkan email ATAU phone.