Skip to content

Honeyframe Data Integration

Read-only BI data source for Hermina 360 dashboards.

Official API contract: https://docs.honeyframe.io/webapp-data-apiBase URL: https://hospital.hubstudio.idMinimum version: Platform v0.2.62+ Token: set in .env.local as VITE_HF_TOKEN — never commit to source control


Quick Start

bash
# .env.local (not committed) — DEV ONLY, see Authentication below
VITE_HF_TOKEN=hf_xxxx...
ts
import { useWebappConfig, usePageBatch } from '@/hooks';
import { useHoneyframeStore } from '@/stores';

// 1. Load config (cached)
useWebappConfig('hermina360');
const app = useHoneyframeStore((s) => s.apps['hermina360']);

// 2. Find page and card refs
const page = app?.config.pages.find((p) => p.key === 'executive');
const cardRefs = (page?.card_refs ?? []).filter(
  (b): b is HFCardRef => b.kind === undefined || b.kind === 'card',
);

// 3. Fetch batch with optional params
const { data: batch } = usePageBatch('executive', cardRefs, app?.asset_id, {
  period_start: '2025-06-01',
  period_end: '2025-06-30',
});

// 4. Read results — ALWAYS check error first
const cardResult = batch?.[String(cardId)];
if (cardResult?.error) console.error(cardResult.error);
const rows = cardResult?.rows ?? [];

Webapps

App Keyasset_idDescriptionDashboards
hermina3601200Main executive + operational244, 280, 291–299
hermina360_financial1207CFO / financial (richer data)284, 285, 286, 287

Authentication

The app logs the user into Honeyframe and uses the returned JWT for every call.

Login

POST {HF_BASE}/api/auth/login
Body: { "username": "...", "password": "..." }

200 → { "access_token": "...", "token_type": "bearer", "must_reset_password": false }
401 → bad credentials

HF_BASE is the hfApi base — /hf-api in dev (Vite proxy) and in production (nginx proxy). useAuthStore.login() (src/stores/useAuthStore.ts) calls it, persists access_token to localStorage under hermina360-auth, and exposes logout() to clear it. must_reset_password: true raises a sticky notice after login and is kept on the store as mustResetPassword.

All subsequent requests carry that JWT:

Authorization: Bearer <access_token>

A hfApi request interceptor injects it; a response interceptor clears auth and redirects to /login on any 401 except the login call itself. PrivateRoute gates on the presence of that token.

VITE_HF_TOKEN is a dev-only fallback

When no user is logged in, the interceptor falls back to VITE_HF_TOKEN only if import.meta.env.DEV. That branch is statically false in npm run build, so the PAT is dropped from the production bundle. Never set VITE_HF_TOKEN in a production build — a token baked into the bundle is handed to every visitor.

Token Permissions

  • project.view — config, block execute, filter options
  • dashboard.view — card execute
  • Recommended scope ceiling: ["project.view", "dashboard.view"]

Failure Modes

StatusMeaningAction
401Bad, revoked, or expired tokenTerminal — do not retry
403Permission denied or token scoped to different org/projectTerminal — do not retry
404Not found or not visible (deliberately indistinguishable)Check key/ID
429Rate limitedBack off and retry

Platform Tier Note: Unlicensed orgs receive 403 with license_required body. App tier does not enforce this.


Endpoints

#MethodPathPurposeLimits
1GET/api/webapps/{key}Fetch config (pages, nav, card_refs)Cache — changes only on republish
2POST/api/dashboards/{id}/cards/{card_id}/execute?asset_id=NExecute single card1000 rows, 30s timeout
3POST/api/dashboards/{id}/cards/execute-batch?asset_id=NRun multiple cards per dashboard1000 rows per card, 30s timeout
4POST/api/webapps/{key}/pages/{pageKey}/blocks/{idx}/executeRun HTML/code blocks with data_sql5000 rows, 8s timeout
5POST/api/webapps/preview-filter-optionsResolve options_sql to dropdown values5000 rows, 8s timeout

Webapp Config (Endpoint 1)

Response contains pages, nav, card_refs, filters, parameters.

Block shapes in card_refs:

KindDescriptionExecute via
"card" or absentSQL-backed cardEndpoint 2/3
"code"Code blockEndpoint 4
"html"Sandboxed markup; if has data_sqlEndpoint 4
"html" (no data_sql)Static markupNo data call needed

Card block fields (server-hydrated, usable before execution):

  • card_type: bar | line | pie | donut | kpi | text
  • card_title: Display title
  • card_config: Chart config (xField, yField, nameField, valueField, etc.)
  • card_config_overrides: Apply on top of card_config when present

Block indexing: block_idx = position in card_refs array. Public redactor blanks hidden blocks in place (indexes stable). Never filter array before deriving index.

Execute Single Card (Endpoint 2)

POST /api/dashboards/{dashboard_id}/cards/{card_id}/execute?asset_id={asset_id}
Body: { "params": { "key": "value" } }

Execute Batch (Endpoint 3 — main data path)

POST /api/dashboards/{dashboardId}/cards/execute-batch?asset_id={assetId}
Body: { "card_ids": [number], "params": { "key": "value" } }

Response: Record<string, CardResult> — keyed by card_id (string, not integer).

ts
type CardResult = {
  card_id: number;
  columns: string[];
  rows: Record<string, unknown>[];
  row_count: number;
  execution_ms: number;
  error: string | null;       // ← ALWAYS CHECK THIS
  bound_params?: string[];    // confirms which params each card responds to
};

Row limit: 1000 rows appended as LIMIT on connector-backed and parameterless warehouse paths. Parameterized warehouse queries (common for card execution with params) run without appended LIMIT — only 30s timeout bounds them. Size clients accordingly.

Critical: Failed queries return HTTP 200 with error set and rows: []. Always check error before reading rows.

Block Execute (Endpoint 4)

POST /api/webapps/{key}/pages/{pageKey}/blocks/{blockIdx}/execute
Body: { "filters": { "filter_name": "value" } }
ts
type BlockResult = {
  columns: string[];
  rows: Record<string, unknown>[];
  row_count: number;
  execution_ms: number;
  error: string | null;
  truncated: boolean;         // true if exceeded 5000-row limit — surface to user
};

blockIdx = index in card_refs array (before any filtering). Max 5000 rows, 8s timeout. Honor truncated flag.

Filter Options (Endpoint 5)

POST /api/webapps/preview-filter-options
Body: { "sql": "SELECT code AS value, name AS label FROM ..." }
ts
type FilterOption = {
  value: string | number;
  label: string;
  code?: string;              // present only when query returns it
};

Send options_sql from config verbatim — modified SQL returns 403. Read-only tokens cannot run ad-hoc SQL.

Anonymous/Public Endpoints

MethodPathNotes
GET/api/public/webapps/{key}?token=SQL redacted, PII masked, no filter options
POST/api/public/webapps/{key}/cards/{dashboard_id}/{card_id}/execute?token=No auth header needed

Public paths redact data_sql, mask PII columns, and blank public_hidden blocks in place (indexes stable).


Filter Parameter Contract

Filter state travels as ONE flat dict in the params field of execute-batch. Build it with buildFilterParams() (src/lib/filterParams.ts) — do not assemble the keys by hand.

1. Period (filters.period, type preset_date_range)

The config's params object names the bind keys; on both hermina360 apps they are:

jsonc
{ "period_start": "2026-07-01", "period_end": "2026-07-31", "period_label": "Bln Lalu" }

period_label is the preset key, or 'custom' for a hand-picked range.

2. Select / pill filters (branch_select, pill_multi)

The config declares param (e.g. branch_id, kelas, regional). The card SQL binds the COMPANION key <param>_codes — a CSV of the selected options' code values (pill options with no code use their value):

sql
(COALESCE(:branch_id_codes,'ALL') = 'ALL'
 OR afya_code = ANY(string_to_array(:branch_id_codes, ',')))
Selection<param><param>_codes
nothingemptyValue ('ALL')emptyValue ('ALL')
onethe raw valuethat option's code
manyarray of raw valuescodes joined with ,

kelas_codes matches criteria_band (A/B/C/D); regional_codes matches regional. Never send branch_keys — server-side cards COALESCE it away.

ts
const params = buildFilterParams({
  period: { start: '2026-07-01', end: '2026-07-31', label: 'Bln Lalu', keys: periodKeys },
  selects: [
    { param: 'branch_id', options: branchOptions, selected: ['12', '31'] },
    { param: 'kelas', options: kelasOptions, selected: kelas === 'Semua' ? [] : [kelas], mode: 'single' },
  ],
});
// → { period_start, period_end, period_label,
//     branch_id: ['12','31'], branch_id_codes: 'RSHB,RSHK',
//     kelas: 'ALL', kelas_codes: 'ALL' }

Bind names and option codes are read from the app config at runtime (resolvePeriodKeys, resolveSelectFilter); the page's hardcoded pill values are only a fallback for when the config does not declare that filter.

3. Card badges (bound_params)

Each batch result carries bound_params — the bind names that card's SQL actually references. deriveCardBadge() (src/lib/cardBadges.ts) turns that into a chip:

  • a branch-scoping filter is set AND neither <param> nor <param>_codes is in bound_params"Tidak per cabang"
  • else the period moved off the default AND none of period_start / period_end / period_label is in bound_params"Tidak per periode"

Branch wins; the two never stack. The chip only renders once the card has loaded cleanly (no error, not loading) and the server reported bound_params — an absent bound_params badges nothing rather than guessing.


Integration Pattern

Standard page pattern used across all dashboard pages:

useWebappConfig → find page by key → filter card_refs (kind=card) → usePageBatch(pageKey, cardRefs, asset_id, params?)

executePageBatch groups cards by dashboard_id and fires parallel batches.

Per-page block loop:

ts
for (const [idx, blk] of page.card_refs.entries()) {
  if (blk.kind === 'code' || (blk.kind === 'html' && blk.data_sql)) {
    await executeBlock(key, page.key, idx, filters);
  } else if (blk.kind === undefined || blk.kind === 'card') {
    // Collect by dashboard_id, batch per dashboard
  } else if (blk.kind === 'html') {
    // Static markup—render blk.html, no data call
  }
}

hermina360 — Card Inventory (v132, 34 pages)

executive (dashboard 244)

Card IDTitleNotes
2016Pasien Unik
2017Dokter Aktif
2026Tingkat Pembatalan
2010Rawat jalan totalKunjungan, per hari
2014Admisi Rawat Inap (Jaringan)Rawat Inap (Episode), NO Hari Rawat field
2039Operasi · JaringanNOT period-bound (YTD always)
2037Lahir · JaringanNOT period-bound (YTD always)
2225Tren Kinerja Jaringan — 6 Bulan
2226Prioritas Tindakan Direksi
2246Bulan Berjalan vs Bulan LaluHas Δ MoM (%) per metric
2002Padma vs Kamala — Bauran Layanan

operational (dashboard 244) — superset of executive

All executive cards plus:

Card IDTitleNotes
2312Fisioterapi
2313KTK — Tumbuh Kembang
2363Tren Operasional Jaringan — 12 Bulan
2362Pembatalan per Cabang
2364Ringkasan Operasional per Cabang
2365BOR BulananBOR % trend, last value = current BOR
2366Volume Operasi Bulanan — Jaringan

financial (dashboard 244)

Card IDTitleNotes
2669Pendapatan (Bruto) — JaringanGross revenue (≠ net from 2598)
2670Revenue Rawat InapGross, period-bound
2671Revenue Rawat JalanGross, period-bound
2672Pendapatan IGDGross, period-bound
2675Pendapatan Bruto per Regional (I–VI)Regional bar chart data
2676Yield per EpisodeARPIP/ARPOP per episode type
2227Penerimaan / Collections
2228Klaim (Settlement)
2229Bayar Langsung (Payment)
2230Deposit Diterima
2231Komposisi Penerimaan (Metode)
2357Akrual vs Collections per Cabang
2358Catatan Data Keuangan
2359Tren Penerimaan & Deposit — 12 Bulan
2360Penerimaan per Cabang (Top 15)
2361Komposisi Kunjungan BPJS vs Non-BPJS
2387Tren Pendapatan Bruto (GL) — Padma vs Kamala
2388Pendapatan Akrual per Penjamin (GL)
2389Pendapatan Akrual per Cabang (GL) — Top 15

Cards 4425–4442 (18 new cards added in v132, dashboard 280).

financial-gl (dashboard 280)

GL-based P&L data. Cards 3130–3176 covering:

  • P&L Summary (3130–3137): Revenue RS, COGS, GP, Margin, Opex, EBIT
  • Revenue breakdown (3138–3149): IP/OP/IGD, BPJS vs Non-BPJS, per cabang
  • COGS structure (3151–3161): drug, salary, per line item, ratio trends
  • Opex (3163–3172): salary BUA, insurance, per line item
  • Profitability (3174–3176): drug revenue

ringkasan-eksekutif (dashboard 244)

Card IDTitleNotes
2598Kinerja Keuangan HEAL (Metrik × Periode)Revenue, EBITDA, NP, NPM, Padma Mix — NET revenue
2581Laporan Laba Rugi (P&L Cascade)Full P&L table
2574Prioritas Tindakan DireksiCabang, Kls, EBITDA Mgn %, Net Profit, NPM %, % Padma
2572Komposisi Revenue — Padma vs Kamala
2599Pendapatan Net per Segmen (GL Live)
2665–2668Metric heroes (Revenue, EBITDA, NP, GP)Trend data, 12-month series

tren-periodik (dashboard 244)

Card IDTitleNotes
2665Revenue Total — metric_heroMonthly series with Δ%
2666EBITDA — metric_hero
2667Net Profit — metric_hero
2668Gross Profit — metric_hero
2582Tren — Revenue Total · RS · Non-RS
2583Tren — EBITDA · NP · GP
2584Tren — Margin: EBITDA · NPM · GPM (%)
2585Tren — Revenue Padma vs Kamala
2586Tren — Biaya: Obat · TK · Pemeliharaan · Utilitas
2587Tren — Rasio Biaya / Revenue (%)

clinical (dashboard 244)

Card IDTitle
2376LOS Rata-rata Rawat Inap
2025Hari rawat ICU
2028Pemakaian ventilator
2377Komposisi Encounter (RJ/RI/Darurat)
2378Top 12 Diagnosa (ICD-10)
2365BOR Bulanan
2441Top 10 Diagnosa — Jaringan AFYA
2442ALOS per RS — Jaringan AFYA

pharmacy (dashboard 244)

Card IDTitle
2328Belanja Farmasi & Supply
2420Pendapatan Obat · Jaringan
2329SKU Diterima
2330Vendor Aktif
2331Batch ≤90 Hari Kadaluarsa
2419Analisis ABC — Pareto Pengadaan
2332Top Item — Belanja Procurement
2334Belanja Procurement per Bulan
2335Batch Mendekati Kadaluarsa

branches (dashboard 244)

Card IDTitle
2421Statistik Cabang — Cabang vs Region vs Jaringan
2340Ranking Cabang — Volume Kunjungan
2422Ranking — Penerimaan (Rp juta)
2424Semua Metrik — 54 Cabang
2336Branch Explorer — Semua Cabang

branch-focus (dashboard 244)

Card IDTitle
2606Perbandingan Cabang header
2603Perbandingan Revenue per Cabang — Bulanan
2605Profitabilitas per Cabang — Bulan Terkini
2607Struktur Biaya per Cabang — Bulan Terkini

likuiditas (dashboard 291)

Card IDTitle
4188Kas & Setara Kas
4189Piutang Usaha (AR)
4190Persediaan
4191Utang Usaha (Trade AP)
4192CCC (Hari)
4193CCC Komposit — DSO + DIO − DPO
4194Piutang per Penjamin
4195Tren Modal Kerja — 6 Bulan
4196AR Aging — Piutang Pasien Pribadi
4197Umur Utang Usaha (AP Aging)
4198AR Days (DSO) — Top 15 Cabang
4386Likuiditas — additional metric

heatmap-kpi (dashboard 297)

Card IDTitle
4182Heatmap KPI Cabang — 8 Indikator
4183Ranking Worst-first — Komposit KPI

lini-layanan (dashboard 293)

Cards 4339–4364 covering Poliklinik, IGD, Rawat Inap, Kelahiran metrics with trends and worst-first rankings.

kinerja-penjamin (dashboard 295)

Cards 4397–4405: Asuransi, Perusahaan, BPJS TK, OOP, target realization, payer mix.

pelayanan-jkn (dashboard 296)

Cards 4406–4415: BPJS revenue/volume, ARPIP/ARPOP BPJS, selisih koding.

vaksinasi (dashboard 298)

Cards 4170–4180: Vaccine doses, antigen trends, funnel, projection.

varians-biaya (dashboard 294)

Cards 4365–4385: Cost ratios, worst-first rankings, honor dokter.

retensi-pasien (dashboard 299)

Cards 4416–4424: Retention cohorts, new vs returning patients.

wasdal (dashboards 244, 292)

Card IDTitle
2236Wasdal Prioritas — Scorecard 54 Cabang
4199Wasdal Prioritas — Heatmap 8 Indikator
4200Ranking Worst-first — Komposit Wasdal

review-heal (dashboard 244)

HEAL network review. 12 cards, shares many with ringkasan-eksekutif and tren-periodik.

Card IDTitle
2562Review HEAL — Summary
2564Review HEAL — Detail
2572Komposisi Revenue — Padma vs Kamala
2574Prioritas Tindakan Direksi
2581Laporan Laba Rugi (P&L Cascade)
2599Pendapatan Net per Segmen (GL Live)
2656Metric Hero — Revenue
2657Metric Hero — EBITDA
2665Revenue Total — metric_hero
2666EBITDA — metric_hero
2667Net Profit — metric_hero
2668Gross Profit — metric_hero

review-rs (dashboard 244)

Per-hospital review. 8 cards.

Card IDTitle
2563Review per RS — Detail (names NOT masked)
2564Review HEAL — Detail
2597Revenue per RS
2656Metric Hero — Revenue
2665Revenue Total — metric_hero
2666EBITDA — metric_hero
2667Net Profit — metric_hero
2668Gross Profit — metric_hero

dwh-gross (dashboard 244)

DWH gross revenue analysis. 9 cards (2589–2597).

Card IDTitle
2589–2596DWH Gross Revenue breakdowns
2597Revenue per RS

patients (dashboard 244)

Patient analytics. 13 cards (7 new: 4443–4450).

Card IDTitle
2002Padma vs Kamala — Bauran Layanan
2010Rawat jalan total
2011Rawat Jalan — Detail
2014Admisi Rawat Inap (Jaringan)
2015Rawat Inap — Detail
4443–4450Patient analytics (new in v132)

Other pages

  • doctors (244): 2234 — Doctor listing
  • specialists (244): 2235 — Specialist listing
  • catalog (244): 2239 — Catalog/reference data
  • builder (244): 2240 — Dashboard builder data
  • wiki (244): 2241 — Wiki content
  • episode (no dashboard): 1 code block — Episode analysis
  • sdm (244): 2234, 2380–2383 + 2 code blocks — Doctor headcount, specialization distribution
  • diagnostic (244): 2031–2035, 2232, 2417 — Imaging modality volumes
  • staffing (244): 2042–2043 — SP1/SP2 doctor counts
  • assets (244): 2004–2006, 2386, 2431–2433 — Bed inventory
  • data-trust (244): 2485–2497 — Patient identity quality metrics

hermina360_financial — Card & Block Inventory

ringkasan (dashboard 284)

HTML Blocks (block-execute):

BlockContentKey Columns
block[0]Data freshnesskind, a (timestamp), b (business date), n (count)
block[1]P&L monthly trendkind=m: label (YYYY-MM), a (revenue), b, c, d
block[2]Revenue breakdown (exact)rev_ip, ip_p, ip_k, rev_op, op_p, op_k, rev_igd, d (IP days), v (OP visits) — all in Rp
block[3]P&L totalsrev, ebitda, laba, gp, padma, kamala, p_* (previous period)
block[4]Volume & BOR based, v, bed_days, beds — BOR = d/bed_days×100
block[8]Intra-month previewDaily billing, NOT comparable with GL data

Cards:

Card IDTitleKey Fields
3786Rincian Lini Layanan × KelasMetrik, Total/Blended, Padma, Kamala
3787Revenue per RegionalRegional, Revenue (Rp M)
3788Prioritas Direksi (richer than 2574)Cabang, Reg, Kelas, Revenue (Rp M), EBITDA Mgn %, NPM %, BOR* %, % Padma, Quick Assessment

review-konsolidasi (dashboard 285)

Card IDTitle
3790Revenue Padma
3791Revenue Kamala
3792Rasio Obat
3793Rasio Tenaga Kerja
3794Tabel Deret Waktu — YTD · Avg · M-2 · M-1 · M-0
3795Revenue Total — 12 Bulan (anotasi musiman)
3796EBITDA & Margin — 12 Bulan
3797Revenue Padma vs Kamala — 12 Bulan
3798Rasio 4 Penggerak Biaya — 12 Bulan
3799Ranking Biaya — bulan berjalan

review-rs (dashboard 286)

Card IDTitle
3801Peta Panas — seluruh RS × indikator
3802EBITDA Margin — 15 RS terburuk
3803Revenue per RS — Top 15
3804Waterfall Δ EBITDA MoM
3805Rollup per Regional
3806Distribusi Quick Assessment

tren-bulanan (dashboard 287)

Card IDTitle
3808Revenue Bulanan — Total · Padma · Kamala
3809Revenue Bulanan — Rawat Inap vs Rawat Jalan
3810Hari Rawat Inap Bulanan — Padma vs Kamala
3811ARPIP Bulanan — Padma vs Kamala
3812Growth MoM Revenue (%)
3813Penguraian Pertumbuhan IP — Efek Volume vs Efek Tarif
3814CMGR YTD — Revenue & Volume per RS

Data Gotchas

  1. HTTP 200 with error: Failed queries return 200 with error set + empty rows. Always check error before consuming data.
  2. Gross vs Net revenue: Cards 2669–2672 return gross revenue; card 2598 returns net revenue. Different legitimate metrics.
  3. Card 2014 has no Hari Rawat: Only Rawat Inap (Episode). Use hermina360_financial block[2] for real IP days.
  4. Cards 2039/2037 not period-bound: Surgery and birth cards return YTD data regardless of period params.
  5. Hospital names masked in dashboards 293/294/297 — branch_code available in 297.
  6. Card 2563 (review-rs): Names NOT masked, Thn = founding year.
  7. bound_params in execute-batch response confirms which params each card responds to.
  8. block_idx is array index: Use the index in card_refs array BEFORE filtering by kind.
  9. options_sql is read-only: Send verbatim from config — modified SQL returns 403. Read-only tokens cannot run ad-hoc SQL.
  10. Padma/Kamala split: hermina360 card 2598 only has overall Padma Mix % (approximation). hermina360_financial block[2] has exact per-service-line split.
  11. Row cap not guaranteed: 1000-row LIMIT is appended on connector-backed paths, but parameterized warehouse queries run without it — only 30s timeout bounds them.
  12. card_config_overrides: Apply on top of card_config when present in card_refs.

Integration Checklist

  1. Fetch config once per page load; cache it (changes only on republish)
  2. Lay grid from layout and hydrated card_type; do not wait for execution
  3. Render static options immediately; call endpoint 5 for options_sql (send verbatim)
  4. Group cards by dashboard_id; one batch per dashboard (endpoint 3)
  5. Execute code and data-bearing HTML blocks (endpoint 4)
  6. Check error on every result before reading rows
  7. Honor truncated on block path; surface rather than hide
  8. Pass asset_id on card executes
  9. Treat 401/403 as non-retryable stop
  10. Back off on 429 (rate limited)

Pages Using HF Data

Page ComponentHF Page KeyDashboardStatus
ExecSummaryPageringkasan-eksekutif + financial + operational244Live
CfoCockpitPagefinancial244Live
OperationalPageoperational244Live
PeriodicTrendsPagetren-periodik244Live
BranchStatisticsPagebranches244Live
BranchComparisonPagebranch-focus244Live
HeatmapPageheatmap-kpi297Live
ServiceLinesPagelini-layanan293Live
PayerPerformancePagekinerja-penjamin295Live
VaccinationPagevaksinasi298Live
LiquidityPagelikuiditas291Live
SPIPagevarians-biaya294Live
PatientRetentionPageretensi-pasien299Live
JKNPagepelayanan-jkn296Live
ClinicalFlowPageclinical244Live
PharmacyPagepharmacy244Live
ReviewPerRSPagereview-rs244Live
WasdalPagewasdal244Live
ConsolidationPagereview-konsolidasi (hermina360_financial)285Live

Pages with no backing cards

MCUPage, MaternalPage and AccessManagementPage have no cards in either webapp. They render <DemoDataBanner /> ("Data demo — belum terhubung ke sumber data") over placeholder figures. Wire them only once real cards exist — do not invent an endpoint for them.