Pengembang
API publik dan webhook
127 operasi · 30 event webhook · 7 scope
POST /oauth/token
Authorization: Basic ‹client_id:client_secret›
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&scope=patients:read encounters:read
→ 200 { "token_type": "Bearer", "expires_in": 3600 }Autentikasi
Dari klien API ke panggilan pertama
- 01
Buat klien API
Admin klinik membuka Pengaturan, API dan webhook, lalu memilih scope, cabang, dan batas laju. Client secret hanya ditampilkan sekali.
- 02
Tukar menjadi token
POST /oauth/token dengan Basic auth dan grant_type=client_credentials. Token opaque berlaku 3.600 detik; minta token baru sebelum kedaluwarsa.
- 03
Panggil endpoint
Kirim Authorization: Bearer ‹token›. Pilih cabang dengan X-Branch-ID bila klien berlaku di beberapa cabang.
- 04
Cabut kapan saja
Mengubah scope, merotasi secret, atau mencabut klien langsung membatalkan semua token aktifnya. Setiap akses tercatat di audit log klinik.
Autentikasi dan tanda tangan
OAuth2 client credentials
Admin klinik membuat klien API; integrator menukar kredensial menjadi token Bearer berumur 1 jam.
Scope = izin
Scope dipetakan ke izin RBAC, jadi endpoint menegakkan hak akses yang sama dengan pengguna aplikasi.
Webhook bertanda tangan
Header X-Medivana-Signature: t=‹unix›,v1=HMAC-SHA256(secret, "‹t›." + body); tolak bila selisih waktu > 5 menit.
Mulai cepat
Tanpa SDK: curl, JavaScript, Python
Dua permintaan cukup untuk membaca data pertama. Simpan kredensial di variabel lingkungan dan jangan pernah menaruh client secret di aplikasi peramban atau ponsel.
1. Minta access token
curl -X POST "https://api.medivana.cloud/api/v1/oauth/token" \
-u "$MEDIVANA_CLIENT_ID:$MEDIVANA_CLIENT_SECRET" \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=patients:read encounters:read'const basic = Buffer.from(`${process.env.MEDIVANA_CLIENT_ID}:${process.env.MEDIVANA_CLIENT_SECRET}`).toString("base64");
const res = await fetch("https://api.medivana.cloud/api/v1/oauth/token", {
method: "POST",
headers: { Authorization: `Basic ${basic}` },
body: new URLSearchParams({
grant_type: "client_credentials",
scope: "patients:read encounters:read",
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const data = await res.json();import os
import requests
resp = requests.post(
"https://api.medivana.cloud/api/v1/oauth/token",
auth=(os.environ["MEDIVANA_CLIENT_ID"], os.environ["MEDIVANA_CLIENT_SECRET"]),
data={
"grant_type": "client_credentials",
"scope": "patients:read encounters:read",
},
timeout=30,
)
resp.raise_for_status()
data = resp.json()2. Cari pasien dengan token
curl -X GET "https://api.medivana.cloud/api/v1/patients?q=RM-000123" \
-H "Authorization: Bearer $MEDIVANA_TOKEN"const res = await fetch("https://api.medivana.cloud/api/v1/patients?q=RM-000123", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MEDIVANA_TOKEN}`,
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const data = await res.json();import os
import requests
resp = requests.get(
"https://api.medivana.cloud/api/v1/patients?q=RM-000123",
headers={"Authorization": f"Bearer {os.environ['MEDIVANA_TOKEN']}"},
timeout=30,
)
resp.raise_for_status()
data = resp.json()3. Ambil halaman berikutnya
Daftar berhalaman mengembalikan next_cursor. Kirim kembali sebagai ?cursor= sampai next_cursor tidak ada.
curl "https://api.medivana.cloud/api/v1/patients?limit=100&cursor=$NEXT_CURSOR" \
-H "Authorization: Bearer $MEDIVANA_TOKEN"let cursor;
do {
const url = new URL("https://api.medivana.cloud/api/v1/patients");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.MEDIVANA_TOKEN}` } });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const page = await res.json();
for (const patient of page.items) console.log(patient.mrn, patient.name);
cursor = page.next_cursor;
} while (cursor);import os
import requests
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['MEDIVANA_TOKEN']}"
params = {"limit": 100}
while True:
resp = session.get("https://api.medivana.cloud/api/v1/patients", params=params, timeout=30)
resp.raise_for_status()
page = resp.json()
for patient in page["items"]:
print(patient["mrn"], patient["name"])
if not page.get("next_cursor"):
break
params["cursor"] = page["next_cursor"]Otorisasi
Scope dan izin RBAC
Minta scope sesedikit mungkin. Operasi yang tidak diizinkan scope token menjawab 403 forbidden; endpoint konteks (cabang, unit, tenaga kesehatan, terminologi) bisa dipanggil token apa pun.
| Scope | Izin RBAC | Operasi |
|---|---|---|
| patients:read | patient.read | 9 operasi |
| patients:write | patient.read, patient.write | 20 operasi |
| encounters:read | encounter.read | 62 operasi |
| appointments:read | appointment.read | 5 operasi |
| appointments:write | appointment.read, appointment.manage | 16 operasi |
| invoices:read | invoice.read | 19 operasi |
| webhooks:manage | webhook.manage | 9 operasi |
Konvensi
Aturan yang berlaku di semua endpoint
Cabang aktif
Header X-Branch-ID memilih cabang. Operasi tulis per cabang menolak permintaan tanpa cabang aktif (400); klien satu cabang memakai cabangnya otomatis.
Paginasi cursor
Daftar berbentuk items dan next_cursor. Ukuran halaman diatur limit; cursor bersifat opaque dan tidak boleh disusun sendiri.
Batas laju
Per klien sesuai rate_limit_per_min (default 120 per menit) dan 60 permintaan per menit per IP di endpoint OAuth. Melebihi batas menjawab 429 rate-limited.
Konkurensi
Detail membawa ETag berisi versi. Kirim If-Match saat PATCH agar perubahan ditolak 412 bila data sudah diubah pihak lain.
Pengulangan aman
Duplikat ditolak 409 (NIK pasien yang sama, slot booking yang sama), jadi permintaan yang gagal di jaringan aman diulang lalu dibaca kembali.
Versi dan bahasa
Versi mayor ada di path /api/v1. Field, enum, event, dan endpoint baru ditambahkan tanpa versi baru, jadi abaikan field yang tidak dikenal. Galat mengikuti Accept-Language (id atau en).
Galat
Format galat RFC 9457
Galat berformat application/problem+json. Gunakan code dan errors[].field untuk logika program; title dan detail diterjemahkan sesuai Accept-Language. Sertakan request_id saat menghubungi dukungan.
| HTTP | code | Arti |
|---|---|---|
| 400 | bad-request | Format atau parameter salah, atau cabang aktif belum dipilih |
| 401 | unauthorized | Token tidak ada, salah, kedaluwarsa, atau dicabut |
| 402 | module-locked | Modul yang melayani endpoint tidak ada di paket klinik |
| 403 | forbidden | Scope token tidak memberi izin untuk aksi ini |
| 403 | confidential-record | Rekam medis ditandai rahasia; klien API tidak bisa membukanya |
| 404 | not-found | Data tidak ada atau di luar cakupan klien |
| 409 | conflict | Duplikat atau status data tidak mengizinkan aksi |
| 412 | stale | If-Match tidak cocok dengan versi terbaru |
| 422 | validation | Validasi gagal; rincian per field di errors |
| 429 | rate-limited | Batas laju terlampaui; coba lagi pada menit berikutnya |
Endpoint OAuth memakai format RFC 6749, misalnya error invalid_client atau invalid_scope.
application/problem+json
{
"type": "https://medivana.cloud/errors/forbidden",
"title": "Akses ditolak",
"status": 403,
"code": "forbidden",
"detail": "Peran Anda tidak memiliki izin untuk aksi ini.",
"instance": "/api/v1/billing/invoices",
"request_id": "01JC9Z3Y8M4N2P5Q7R9S0T1V2W"
}Webhook
Event bertanda tangan HMAC-SHA256
Langgan event dengan POST /webhooks. Medivana mengirim POST JSON ke URL https Anda dengan header X-Medivana-Event, X-Medivana-Event-ID, X-Medivana-Delivery, dan X-Medivana-Signature.
Verifikasi setiap pengiriman
- Baca body mentah sebelum di-parse, lalu ambil t dan v1 dari X-Medivana-Signature.
- Tolak bila selisih waktu sekarang dan t lebih dari 300 detik (mencegah replay).
- Hitung HEX(HMAC-SHA256(secret, t + "." + body)) dan bandingkan dengan v1 secara konstan-waktu.
- Deduplikasi memakai id event; pengiriman ulang membawa id yang sama.
- Balas 2xx dalam 10 detik, lalu proses secara asinkron.
Pengiriman ulang
Selain 2xx atau timeout dicoba ulang 1, 5, 15, dan 60 menit, lalu tiap 6 jam selama 3 hari. Jawaban 410 Gone menonaktifkan langganan. Riwayat dan kirim ulang manual tersedia lewat API.
Contoh verifikasi
import crypto from "node:crypto";
export function verifyMedivanaSignature(rawBody, header, secret, toleranceSec = 300) {
let t = 0;
const sigs = [];
for (const part of header.split(",")) {
const [k, v] = part.trim().split("=", 2);
if (k === "t") t = Number(v);
if (k === "v1") sigs.push(v);
}
if (!t || sigs.length === 0) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
return sigs.some((s) => {
const got = Buffer.from(s, "hex");
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}import hashlib
import hmac
import time
def verify_medivana_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
t, sigs = 0, []
for part in header.split(","):
k, _, v = part.strip().partition("=")
if k == "t" and v.isdigit():
t = int(v)
elif k == "v1":
sigs.append(v)
if not t or not sigs or abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in sigs)Spesifikasi OpenAPI 3.1
Impor ke Postman, Insomnia, atau generator klien. Spesifikasi diuji otomatis terhadap router dan peta scope Medivana di setiap perubahan.
Langkah berikutnya
Lihat Medivana bekerja untuk klinik Anda
Coba demo mandiri dengan data contoh, atau minta tim kami menunjukkan alur yang paling relevan untuk klinik Anda.
Demo mandiri berisi data contoh sesuai jenis klinik Anda dan berlaku 7 hari.