Autentikasi

Termilo punya empat konteks autentikasi yang nyata dan terpisah: sesi dashboard, token manage customer, endpoint booking publik tanpa login, serta API key untuk partner dan MCP.

Lihat .md

Empat konteks auth#

Pakai konteks yang tepat untuk tiap permukaan. Keempatnya berdiri sendiri — tidak ada yang bisa dipertukarkan dengan yang lain.

KonteksPembawa kredensialUntukStatus
Sesi dashboard__Host-termilo_session + X-Termilo-CSRF/api/* (dashboard, RBAC)Tersedia
Token managetm_m1.<base64url>.<hmac>/booking/manage/* (self-service)Tersedia
Booking publikTanpa auth · Idempotency-Key pada POST/book/:tenantSlug/*Tersedia
API key / BearerAuthorization: Bearer sk_live_…Partner / server-to-serverTersedia
Pemisahan yang ketat
Token manage tidak membuat sesi dan tidak bisa menjangkau /api/*. Endpoint publik tidak menerima cookie sesi sebagai otorisasi. Pilih satu konteks per permintaan.

Sesi dashboard (login Google)#

Hanya Google OIDC (openid email profile, PKCE S256 + nonce). Setelah login, sesi dibawa oleh cookie HttpOnly __Host-termilo_session (Secure, SameSite=Lax, TTL 30 hari) — server hanya menyimpan hash SHA-256 dari token, bukan token mentahnya. Setiap request /api/* yang mengubah data wajib menyertakan header CSRF.

PARAMETERTIPEWAJIBKETERANGAN
Cookie: __Host-termilo_sessionHttpOnlywajibCookie sesi (Secure, SameSite=Lax, TTL 30 hari). Hanya hash SHA-256 token yang disimpan server. Dikirim otomatis oleh browser.
X-Termilo-CSRFstringwajibWajib untuk semua request /api/* yang mengubah data. HMAC atas csrf:{sessionId}:{sessionHash}. Diambil dari field csrfToken pada GET /api/session.

Ambil csrfToken dari sesi aktif, lalu kirim sebagai header pada setiap request menulis:

# 1. Sesi dibawa oleh cookie __Host-termilo_session (browser, otomatis). # 2. Ambil csrfToken dari sesi aktif: curl https://app.termilo.id/api/session \ --cookie "__Host-termilo_session=..." # → { "ok": true, "data": { "csrfToken": "..." }, "meta": { "requestId": "..." } } # 3. Kirim header CSRF pada setiap request yang mengubah data: curl -X POST https://app.termilo.id/api/services \ --cookie "__Host-termilo_session=..." \ -H "X-Termilo-CSRF: <csrfToken>" \ -H "Content-Type: application/json" \ -d '{ "slug": "konsultasi", "name": "Konsultasi", "durationMin": 30 }'
Shard tenant tidak ditentukan pemanggil
Shard tenant diambil dari workspace aktif + membership pada sesi, bukan dari tenant id atau binding D1 yang dikirim pemanggil (assertNoCallerSelectedD1). Anda tidak perlu — dan tidak boleh — mengirim id tenant pada request dashboard.

Peran RBAC menentukan akses tulis vs baca:

  • Tulistenant_owner, tenant_admin.
  • Bacatenant_staff, tenant_viewer.

CSRF yang hilang atau salah menghasilkan CSRF_INVALID (403); sesi yang kedaluwarsa menghasilkan SESSION_EXPIRED (401) dan tanpa sesi sama sekali AUTH_REQUIRED (401).

Token manage customer#

Customer mengelola booking-nya sendiri tanpa akun. Saat booking dibuat, respons menyertakan manageToken dan manageUrl. Token berformat tm_m1.<base64url(payload)>.<hmac>, ditandatangani HMAC-SHA256 dengan kunci MANAGE_LINK_SECRET, TTL 30 hari, dan terikat ke satu booking saja.

PARAMETERTIPEWAJIBKETERANGAN
tenantIdstringwajibTenant pemilik booking. Terikat di dalam payload token.
bookingIdstringwajibBooking yang boleh dilihat / dibatalkan / dijadwal ulang.
customerIdstringwajibCustomer pembuat booking. Token tidak berlaku untuk booking lain.
actionssetwajibAksi yang diizinkan: read, cancel, reschedule. Endpoint memeriksa aksi sebelum mengeksekusi.

Pakai token langsung di path — tanpa cookie, tanpa header CSRF. Respons manage selalu cache-control: private, no-store:

# manageToken + manageUrl dikembalikan saat booking dibuat: # POST /book/:tenantSlug/bookings → { ..., "manageToken": "tm_m1.…", "manageUrl": "…" } # Lihat detail booking lewat token (tanpa login, tanpa CSRF): curl https://app.termilo.id/booking/manage/tm_m1.<base64url>.<hmac> # Batalkan / jadwal ulang dengan token yang sama: curl -X POST https://app.termilo.id/booking/manage/tm_m1.<base64url>.<hmac>/cancel curl -X POST https://app.termilo.id/booking/manage/tm_m1.<base64url>.<hmac>/reschedule \ -H "Content-Type: application/json" \ -d '{ "startsAtMs": 1782345600000 }'
Hanya tiga aksi
Token membawa himpunan actionsread, cancel, reschedule. GET mengembalikan booking termasuk actions: { canCancel, canReschedule }. Reschedule mengunci ulang slot dan mengecek tumpang-tindih. Token tidak bisa membuat sesi atau menyentuh /api/*.

Booking publik#

Halaman booking dan widget embed memanggil endpoint publik. Endpoint ini tidak diautentikasi, tetapi dilindungi policy serta batas rate/penyalahgunaan. Membuat booking wajib menyertakan header Idempotency-Key agar percobaan ulang aman.

# Endpoint booking publik tidak butuh auth, tapi POST butuh Idempotency-Key. curl -X POST https://app.termilo.id/book/studio-anda/bookings \ -H "Idempotency-Key: 6f1c2a90-3b7d-4e8a-9c11-2f0a5d7e1b34" \ -H "Content-Type: application/json" \ -d '{ "serviceSlug": "konsultasi", "startsAtMs": 1782345600000, "source": "public_page", "customer": { "name": "Sari", "primaryEmail": "sari@contoh.id" } }'

Respons berisi booking yang dibuat dengan status held beserta token manage untuk customer. 201 untuk booking baru, 200 untuk replay idempoten dari kunci yang sama:

201 Createdapplication/json
{
"ok": true,
"data": {
"booking": { "bookingId": "bkg_...", "status": "held" },
"idempotentReplay": false,
"manageToken": "tm_m1.…",
"manageUrl": "https://app.termilo.id/booking/manage/tm_m1.…"
},
"meta": { "requestId": "..." }
}
Slot dicek ulang di bawah lock
Slot dari availability hanya kandidat tampilan (finalAuthority: "booking_create_recheck_under_lock"). Saat create, slot dicek ulang di bawah lock Durable Object; jika sudah terisi, respons BOOKING_SLOT_UNAVAILABLE (409).

API key / Bearer#

Tersedia
Secret hanya ditampilkan sekali
Buat, rotate, atau revoke key dari Dashboard → API keys. Termilo hanya menyimpan hash key; salin secret saat dialog reveal terbuka dan rotate segera jika key terekspos.

Kunci sk_live_… dikirim sebagai bearer. Route /v1/* juga mewajibkan X-Termilo-Tenant-Id; route MCP menaruh tenant ID pada /mcp/:tenantId. Scope tersedia untuk booking, customer, dan branding read/write; wildcard memberi seluruh scope.

# Partner API /v1: Bearer key + tenant header. curl -H "Authorization: Bearer sk_live_..." -H "X-Termilo-Tenant-Id: <TENANT_ID>" https://api.termilo.id/v1/bookings # Branding MCP: tenant ID is part of the endpoint. curl -H "Authorization: Bearer sk_live_..." https://api.termilo.id/mcp/<TENANT_ID>

Untuk setup per client, lihat Hubungkan MCP Termilo.