DEV Community

Choirunnisa Hapsari
Choirunnisa Hapsari

Posted on • Originally published at medminutes.io

Integrating Indonesian Hospitals with SATUSEHAT: A Developer's Guide to HL7 FHIR

Indonesia is building a national health data exchange called SATUSEHAT, and every hospital in the country needs to integrate with it. If you're a developer tasked with this integration, this guide covers the architecture, FHIR resource types, common gotchas, and a realistic timeline for going live.

What is SATUSEHAT?

SATUSEHAT is Indonesia's national health data platform, managed by the Ministry of Health (Kemenkes). Think of it as Indonesia's answer to nationwide health interoperability — a centralized FHIR R4 server where hospitals push clinical encounter data.

The mandate is clear: hospitals that fail to integrate face sanctions from the Directorate General of Health Services (Dirjen Yankes). As of 2026, over 1,200 hospitals have been flagged for non-compliance, with a June 2026 deadline looming.

The platform runs on HL7 FHIR R4 and exposes RESTful APIs for data submission. The developer portal is at satusehat.kemkes.go.id.

FHIR Resources You Need to Implement

SATUSEHAT doesn't require every FHIR resource — just a specific subset relevant to Indonesian clinical workflows. Here's what you'll need:

Resource Purpose Required?
Organization Your hospital entity Yes
Location Departments, rooms, beds Yes
Practitioner Doctors, nurses Yes
Patient Patient demographics (linked to NIK) Yes
Encounter Clinical visits/admissions Yes
Condition Diagnoses (ICD-10) Yes
MedicationRequest Prescriptions Yes
MedicationDispense Pharmacy dispensing Yes
Observation Vital signs, lab results Conditional
Specimen Lab specimens (SNOMED-CT) Conditional
ImagingStudy Radiology (DICOM + NIDR) Conditional

The "conditional" resources depend on your hospital's service scope. A hospital with a radiology department needs ImagingStudy; a small hospital without a lab may skip Specimen initially.

Authentication Flow

SATUSEHAT uses OAuth 2.0 Client Credentials flow. You register your application in the developer portal to get a client_id and client_secret, then exchange them for an access token:

# Get access token
curl -X POST https://api-satusehat.kemkes.go.id/oauth2/v1/accesstoken?grant_type=client_credentials \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"
Enter fullscreen mode Exit fullscreen mode
{
  "access_token": "eyJ...",
  "token_type": "BearerToken",
  "expires_in": "3599",
  "scope": ""
}
Enter fullscreen mode Exit fullscreen mode

Tokens expire in 1 hour. Build token refresh into your integration layer — don't fetch a new token per request.

The Integration Architecture

Here's the pattern we've found works well for Indonesian hospitals:

┌──────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   SIMRS /    │     │   Integration    │     │   SATUSEHAT     │
│   RME        │────▶│   Gateway        │────▶│   FHIR Server   │
│   Database   │     │                  │     │                 │
└──────────────┘     │  - FHIR mapper   │     └─────────────────┘
                     │  - Queue system  │
                     │  - Retry logic   │
                     │  - Audit log     │
                     └──────────────────┘
Enter fullscreen mode Exit fullscreen mode

The Integration Gateway sits between your hospital's SIMRS and SATUSEHAT. It handles:

  1. Data mapping: Transform SIMRS-specific data models to FHIR R4 resources
  2. Queueing: Buffer submissions during network issues (hospital internet in Indonesia can be unreliable)
  3. Retry with backoff: SATUSEHAT API has rate limits and occasional downtime
  4. Audit logging: Track every submission for compliance reporting

Example: Mapping an Encounter

// Transform SIMRS visit data to FHIR Encounter
function mapToFHIREncounter(visit) {
  return {
    resourceType: "Encounter",
    status: mapVisitStatus(visit.status),
    class: {
      system: "http://terminology.hl7.org/CodeSystem/v3-ActCode",
      code: visit.type === "inpatient" ? "IMP" : "AMB",
      display:
        visit.type === "inpatient" ? "inpatient encounter" : "ambulatory",
    },
    subject: {
      reference: `Patient/${visit.patient_satusehat_id}`,
      display: visit.patient_name,
    },
    participant: [
      {
        individual: {
          reference: `Practitioner/${visit.doctor_satusehat_id}`,
          display: visit.doctor_name,
        },
      },
    ],
    period: {
      start: formatToFHIRDateTime(visit.admission_date),
      end: visit.discharge_date
        ? formatToFHIRDateTime(visit.discharge_date)
        : undefined,
    },
    location: [
      {
        location: {
          reference: `Location/${visit.department_satusehat_id}`,
          display: visit.department_name,
        },
      },
    ],
    serviceProvider: {
      reference: `Organization/${HOSPITAL_ORG_ID}`,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Common Gotchas

After integrating multiple hospitals, here are the pitfalls that catch most teams:

1. Patient NIK Matching

SATUSEHAT links patients via NIK (national ID number). The problem: many hospital databases have inconsistent NIK data — missing digits, typos, or placeholder values. You'll need a data cleanup step before integration.

def validate_nik(nik: str) -> bool:
    """Indonesian NIK is exactly 16 digits."""
    if not nik or not nik.isdigit():
        return False
    if len(nik) != 16:
        return False
    # First 6 digits = region code (validate against known codes)
    region = nik[:6]
    return region in VALID_REGION_CODES
Enter fullscreen mode Exit fullscreen mode

2. SATUSEHAT ID Mapping

Every entity (patient, practitioner, location) needs a SATUSEHAT-assigned ID. You need to:

  1. Search SATUSEHAT first to find existing records
  2. Create only if not found
  3. Store the mapping in your local database

Never hardcode SATUSEHAT IDs. They can change during data migrations.

3. Sandbox vs. Production Differences

The sandbox environment at api-satusehat-stg.kemkes.go.id doesn't perfectly mirror production. Some things we've seen:

  • Sandbox accepts malformed resources that production rejects
  • Rate limits are more generous in sandbox
  • Some code systems have different validation rules

Always do a pilot run in production with real (consented) data before declaring integration complete.

4. FHIR DateTime Formatting

SATUSEHAT is strict about datetime formats. Use ISO 8601 with timezone:

// Correct
"2026-04-04T10:30:00+07:00"

// Wrong (will be rejected)
"2026-04-04 10:30:00"
"04/04/2026"
Enter fullscreen mode Exit fullscreen mode

5. ICD-10 Code Validation

SATUSEHAT validates ICD-10 codes against the ICD-10 WHO version, not ICD-10-CM. Some codes that exist in ICD-10-CM don't exist in the WHO version. Make sure your mapping uses the correct codeset.

Realistic Timeline

Based on our experience across multiple hospital integrations:

Phase Duration Activities
Registration 1-2 weeks Portal registration, credential setup, team onboarding
Data Audit 2-3 weeks NIK cleanup, master data mapping, gap analysis
Development 4-6 weeks FHIR mapper, gateway, queue system, retry logic
Sandbox Testing 2-3 weeks Submit test data, fix validation errors, load testing
Production Pilot 2-3 weeks Limited department rollout, monitor success rates
Full Go-Live 1-2 weeks All departments, monitoring, staff training

Total: 12-19 weeks for a typical hospital. Smaller hospitals with simpler SIMRS can be faster; large hospitals with complex legacy systems take longer.

Monitoring Post-Integration

Once live, track these metrics:

  • Submission success rate — aim for >98%. Below that, investigate validation errors.
  • Retry rate — high retries suggest network or rate limit issues.
  • Data completeness — are all departments submitting? Some may quietly drop off.
  • Latency — SATUSEHAT API response times can spike during peak hours (morning clinic times).
# Simple monitoring check
def check_submission_health(last_24h_stats):
    success_rate = stats.success / stats.total
    if success_rate < 0.98:
        alert(f"SATUSEHAT submission rate dropped to {success_rate:.1%}")
    if stats.avg_retry > 2:
        alert(f"High retry rate: {stats.avg_retry:.1f} per submission")
Enter fullscreen mode Exit fullscreen mode

Resources


MedMinutes provides managed SATUSEHAT integration for hospitals that want to comply without building the entire pipeline in-house. We handle the FHIR mapping, queue management, and ongoing API change monitoring. Currently serving 50+ hospitals across Indonesia.

Top comments (0)