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.
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.