Dokumentasi SIMAPI

Panduan lengkap — Multi-Database · Refresh Token Otomatis · Semua Platform termasuk IoT

v2.0 Multi-Database Refresh Token All Platforms IoT Ready Updated: 2026

Tentang SIMAPI

SIMAPI (Sistem Informasi Manajemen API) adalah platform manajemen API canggih yang memungkinkan organisasi Anda meng-expose data internal (pegawai, user, absensi, dll) ke aplikasi lain dengan aman, terstruktur, dan teraudit.

Dirancang untuk kebutuhan internal enterprise — di mana aplikasi seperti HRIS, Absensi, Payroll, dan Dashboard perlu mengakses data induk tanpa harus langsung query ke database production.

v2.0 kini mendukung:

  • 6 jenis database — MySQL, MariaDB, PostgreSQL, Oracle, SQL Server, SQLite
  • Refresh token otomatis — client tidak perlu login ulang tiap jam
  • Token expiry per-client — setiap client bisa punya durasi token sendiri
  • Token rotation & reuse detection — keamanan tingkat enterprise
  • Multi-platform — backend, web, mobile (Android/iOS), desktop, IoT (ESP32/ESP8266)
Analogi: SIMAPI seperti pintu resmi ke gudang data Anda. Setiap aplikasi punya kunci unik dengan akses terbatas, dan semua aktivitas tercatat.

Manfaat & Keunggulan

Keamanan Berlapis

JWT access token, refresh token rotation, reuse detection, scope permission, IP whitelist, rate limiting.

Kontrol Granular

Atur akses per aplikasi, per endpoint, bahkan per field (basic/sensitive/protected).

Monitoring Real-time

Dashboard statistik, audit log lengkap, top clients, error tracking, dan usage analytics.

Multi-Database Support

Satu panel untuk MySQL, MariaDB, PostgreSQL, Oracle, SQL Server, dan SQLite.

Refresh Token Otomatis

Access token pendek + refresh token panjang. Client tidak perlu login ulang tiap jam.

Multi-Platform + IoT

Contoh kode untuk backend, web, mobile (Android/iOS), desktop, dan IoT (ESP32/ESP8266).

Database yang Didukung

SIMAPI v2.0 mendukung 6 jenis database — relasional klasik hingga modern. Anda tinggal pilih jenisnya saat menambah koneksi di panel admin.

Siap Pakai
MySQL
Port 3306
Siap Pakai
MariaDB
Port 3306
+ Driver
PostgreSQL
Port 5432
+ Driver
Oracle
Port 1521
+ Driver
SQL Server
Port 1433
Siap Pakai
SQLite
File .db
Catatan: MySQL, MariaDB, dan SQLite siap pakai tanpa instalasi tambahan. PostgreSQL, Oracle, dan SQL Server memerlukan driver Python (dipasang sekali oleh admin sistem).

Cara Memulai

1
Admin Mendaftarkan Anda

Hubungi admin SIMAPI untuk mendaftarkan aplikasi Anda. Admin akan:

  • Menambahkan koneksi database (MySQL / PostgreSQL / Oracle / SQL Server / MariaDB / SQLite)
  • Memilih tabel yang di-expose sebagai API
  • Mendaftarkan aplikasi Anda sebagai Client
  • Memberikan kredensial lewat channel aman
2
Terima Kredensial dari Admin

Admin akan memberikan:

  • client_id — identitas aplikasi Anda
  • client_secret — kunci rahasia (simpan aman!)
  • Daftar scope yang diizinkan
  • URL API endpoint
  • Durasi token (default: access 60 menit, refresh 30 hari)
3
Simpan Kredensial di Tempat Aman

Simpan client_id & client_secret di environment variable (file .env), bukan di source code.

.env
API_CLIENT_ID=app_absensi_abc123
API_CLIENT_SECRET=sk_live_xxxxxxxxxxxxx
API_TOKEN_URL=https://api.anda.com/oauth/token
API_BASE_URL=https://api.anda.com/api/v1
4
Login Pertama — Dapat Access + Refresh Token

Tukar kredensial menjadi access token + refresh token.

5
Akses API

Kirim request dengan header Authorization: Bearer <access_token> ke endpoint yang diizinkan (sesuai scope Anda).

6
Refresh Otomatis

Setelah access token expired, jangan login ulang. Cukup kirim refresh token ke /oauth/token dengan grant_type=refresh_token. Server akan kasih access token baru + refresh token baru (rotation).

Otentikasi (Token)

Ringkasan

SIMAPI menggunakan OAuth2 dengan dua jenis token:

  • Access Token (JWT) — default 1 jam, dipakai untuk akses API
  • Refresh Token (opaque) — default 30 hari, dipakai untuk memperbarui access token

Durasi bisa berbeda per client — diatur admin sesuai kebutuhan aplikasi.

1. Login — Dapat Token Pertama
POST/oauth/token
Request Body
JSON
{
  "client_id": "app_absensi_abc123",
  "client_secret": "sk_live_xxxxxxxxxxxxx",
  "grant_type": "client_credentials"
}
Response Sukses
JSON
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "pegawai:read:basic absensi:read:basic",
  "refresh_token": "rt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "refresh_expires_in": 2592000
}

Nilai expires_in dan refresh_expires_in bisa berbeda per client.

2. Refresh — Dapat Access Token Baru
POST/oauth/token
JSON
{
  "refresh_token": "rt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "grant_type": "refresh_token"
}
Token Rotation: Setiap kali refresh, server mengembalikan refresh_token baru. Simpan yang baru, buang yang lama. Kalau token lama dipakai lagi (tanda pencurian), semua token client akan di-revoke otomatis.
3. Logout — Revoke Refresh Token
POST/oauth/revoke
JSON
{
  "refresh_token": "rt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
Tips: Cache access_token di memory dan refresh 60 detik sebelum expired. Refresh token hanya bisa dipakai sekali (rotation). Kalau request gagal 401, retry 1x dengan token baru.

Endpoint API

Semua endpoint resource mengikuti pola yang sama:

MethodEndpointScopeDeskripsi
GET/api/v1/{resource}{resource}:read:basicList data
GET/api/v1/{resource}/{id}{resource}:read:basicDetail satu record
POST/api/v1/{resource}{resource}:createTambah data
PUT/api/v1/{resource}/{id}{resource}:updateUpdate data
DELETE/api/v1/{resource}/{id}{resource}:deleteHapus data
Query Parameters
ParameterContohDeskripsi
page?page=2Halaman (default: 1)
limit?limit=20Per halaman (default 20, max 100)
search?search=budiCari di field searchable
sort?sort=namaUrutkan berdasarkan field
order?order=descasc / desc
filter[field]?filter[jabatan]=ManagerFilter nilai field

Contoh Penggunaan

Pilih platform Anda di bawah. Semua contoh sudah menggunakan refresh token otomatis — tidak perlu login ulang tiap jam.

Penting untuk Mobile & Web (SPA): Jangan pernah simpan client_secret di aplikasi mobile/SPA. Untuk platform tersebut, gunakan backend proxy yang menyimpan client_secret dan mem-proxy request ke SIMAPI (contoh di tab "Backend Proxy untuk Mobile/SPA").
Backend (Server-to-Server)
Bash
# 1. Login — dapat access + refresh token
RESP=$(curl -s -X POST https://api.anda.com/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "app_absensi_abc123",
    "client_secret": "sk_live_xxxxxxxxxxxxx",
    "grant_type": "client_credentials"
  }')

ACCESS_TOKEN=$(echo "$RESP" | jq -r '.access_token')
REFRESH_TOKEN=$(echo "$RESP" | jq -r '.refresh_token')

# 2. Akses API
curl -X GET https://api.anda.com/api/v1/pegawai \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# 3. Refresh (setelah access token expired)
RESP2=$(curl -s -X POST https://api.anda.com/oauth/token \
  -H "Content-Type: application/json" \
  -d "{
    \"refresh_token\": \"$REFRESH_TOKEN\",
    \"grant_type\": \"refresh_token\"
  }")

ACCESS_TOKEN=$(echo "$RESP2" | jq -r '.access_token')
REFRESH_TOKEN=$(echo "$RESP2" | jq -r '.refresh_token')
Python
import requests
from datetime import datetime, timedelta


class SIMAPIClient:
    """Client SIMAPI dengan auto-refresh token."""

    def __init__(self, client_id, client_secret, base_url):
        self.client_id = client_id
        self.client_secret = client_secret
        self.base_url = base_url.rstrip('/')
        self.access_token = None
        self.refresh_token = None
        self.expires_at = None

    def _login(self):
        resp = requests.post(f"{self.base_url}/oauth/token", json={
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "grant_type": "client_credentials",
        })
        data = resp.json()
        self.access_token = data["access_token"]
        self.refresh_token = data["refresh_token"]
        self.expires_at = datetime.now() + timedelta(seconds=data["expires_in"] - 60)

    def _refresh(self):
        if not self.refresh_token:
            return self._login()
        resp = requests.post(f"{self.base_url}/oauth/token", json={
            "refresh_token": self.refresh_token,
            "grant_type": "refresh_token",
        })
        if resp.status_code != 200:
            return self._login()
        data = resp.json()
        self.access_token = data["access_token"]
        self.refresh_token = data["refresh_token"]  # rotation
        self.expires_at = datetime.now() + timedelta(seconds=data["expires_in"] - 60)

    def _ensure_token(self):
        if not self.access_token or datetime.now() >= self.expires_at:
            self._refresh()
        return self.access_token

    def get(self, path, **params):
        headers = {"Authorization": f"Bearer {self._ensure_token()}"}
        resp = requests.get(f"{self.base_url}{path}", headers=headers, params=params)
        if resp.status_code == 401:
            self._refresh()
            headers = {"Authorization": f"Bearer {self._ensure_token()}"}
            resp = requests.get(f"{self.base_url}{path}", headers=headers, params=params)
        return resp.json()


# Pemakaian
api = SIMAPIClient(
    client_id="app_absensi_abc123",
    client_secret="sk_live_xxxxxxxxxxxxx",
    base_url="https://api.anda.com",
)
data = api.get("/api/v1/pegawai", page=1, limit=10)
print(data)
Node.js
class SIMAPIClient {
  constructor(clientId, clientSecret, baseUrl) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.accessToken = null;
    this.refreshToken = null;
    this.expiresAt = null;
  }

  async _login() {
    const resp = await fetch(`${this.baseUrl}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_id: this.clientId,
        client_secret: this.clientSecret,
        grant_type: 'client_credentials',
      }),
    });
    const data = await resp.json();
    this.accessToken = data.access_token;
    this.refreshToken = data.refresh_token;
    this.expiresAt = Date.now() + (data.expires_in - 60) * 1000;
  }

  async _refresh() {
    if (!this.refreshToken) return this._login();
    const resp = await fetch(`${this.baseUrl}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        refresh_token: this.refreshToken,
        grant_type: 'refresh_token',
      }),
    });
    if (!resp.ok) return this._login();
    const data = await resp.json();
    this.accessToken = data.access_token;
    this.refreshToken = data.refresh_token;
    this.expiresAt = Date.now() + (data.expires_in - 60) * 1000;
  }

  async _ensureToken() {
    if (!this.accessToken || Date.now() >= this.expiresAt) {
      await this._refresh();
    }
    return this.accessToken;
  }

  async get(path, params = {}) {
    const token = await this._ensureToken();
    const qs = new URLSearchParams(params).toString();
    const url = `${this.baseUrl}${path}${qs ? '?' + qs : ''}`;
    let resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
    if (resp.status === 401) {
      await this._refresh();
      resp = await fetch(url, {
        headers: { Authorization: `Bearer ${await this._ensureToken()}` },
      });
    }
    return resp.json();
  }
}

// Pemakaian
(async () => {
  const api = new SIMAPIClient(
    'app_absensi_abc123',
    'sk_live_xxxxxxxxxxxxx',
    'https://api.anda.com'
  );
  const data = await api.get('/api/v1/pegawai', { page: 1, limit: 10 });
  console.log(data);
})();
PHP
<?php
class SIMAPIClient {
    private $clientId, $clientSecret, $baseUrl;
    private $accessToken = null, $refreshToken = null, $expiresAt = 0;

    public function __construct($clientId, $clientSecret, $baseUrl) {
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
        $this->baseUrl = rtrim($baseUrl, '/');
    }

    private function login() {
        $ch = curl_init("{$this->baseUrl}/oauth/token");
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_POSTFIELDS => json_encode([
                'client_id' => $this->clientId,
                'client_secret' => $this->clientSecret,
                'grant_type' => 'client_credentials',
            ]),
        ]);
        $data = json_decode(curl_exec($ch), true);
        $this->accessToken = $data['access_token'];
        $this->refreshToken = $data['refresh_token'];
        $this->expiresAt = time() + $data['expires_in'] - 60;
    }

    private function refresh() {
        if (!$this->refreshToken) return $this->login();
        $ch = curl_init("{$this->baseUrl}/oauth/token");
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_POSTFIELDS => json_encode([
                'refresh_token' => $this->refreshToken,
                'grant_type' => 'refresh_token',
            ]),
        ]);
        $data = json_decode(curl_exec($ch), true);
        if (!isset($data['access_token'])) return $this->login();
        $this->accessToken = $data['access_token'];
        $this->refreshToken = $data['refresh_token'];
        $this->expiresAt = time() + $data['expires_in'] - 60;
    }

    private function ensureToken() {
        if (!$this->accessToken || time() >= $this->expiresAt) {
            $this->refresh();
        }
        return $this->accessToken;
    }

    public function get($path, $params = []) {
        $url = "{$this->baseUrl}{$path}";
        if ($params) $url .= '?' . http_build_query($params);
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ["Authorization: Bearer {$this->ensureToken()}"],
        ]);
        return json_decode(curl_exec($ch), true);
    }
}

$api = new SIMAPIClient(
    'app_absensi_abc123',
    'sk_live_xxxxxxxxxxxxx',
    'https://api.anda.com'
);
$data = $api->get('/api/v1/pegawai', ['page' => 1, 'limit' => 10]);
print_r($data);
Go
package simapi

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
    "time"
)

type SIMAPIClient struct {
    ClientID     string
    ClientSecret string
    BaseURL      string

    mu           sync.Mutex
    accessToken  string
    refreshToken string
    expiresAt    time.Time

    httpClient *http.Client
}

func NewSIMAPIClient(clientID, clientSecret, baseURL string) *SIMAPIClient {
    return &SIMAPIClient{
        ClientID:     clientID,
        ClientSecret: clientSecret,
        BaseURL:      baseURL,
        httpClient:   &http.Client{Timeout: 30 * time.Second},
    }
}

type tokenResp struct {
    AccessToken      string `json:"access_token"`
    RefreshToken     string `json:"refresh_token"`
    ExpiresIn        int    `json:"expires_in"`
    RefreshExpiresIn int    `json:"refresh_expires_in"`
}

func (c *SIMAPIClient) login() error {
    body, _ := json.Marshal(map[string]string{
        "client_id":     c.ClientID,
        "client_secret": c.ClientSecret,
        "grant_type":    "client_credentials",
    })
    resp, err := c.httpClient.Post(c.BaseURL+"/oauth/token", "application/json", bytes.NewBuffer(body))
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    var data tokenResp
    if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
        return err
    }
    c.accessToken = data.AccessToken
    c.refreshToken = data.RefreshToken
    c.expiresAt = time.Now().Add(time.Duration(data.ExpiresIn-60) * time.Second)
    return nil
}

func (c *SIMAPIClient) refresh() error {
    if c.refreshToken == "" {
        return c.login()
    }
    body, _ := json.Marshal(map[string]string{
        "refresh_token": c.refreshToken,
        "grant_type":    "refresh_token",
    })
    resp, err := c.httpClient.Post(c.BaseURL+"/oauth/token", "application/json", bytes.NewBuffer(body))
    if err != nil {
        return c.login()
    }
    defer resp.Body.Close()
    if resp.StatusCode != 200 {
        return c.login()
    }
    var data tokenResp
    json.NewDecoder(resp.Body).Decode(&data)
    c.accessToken = data.AccessToken
    c.refreshToken = data.RefreshToken
    c.expiresAt = time.Now().Add(time.Duration(data.ExpiresIn-60) * time.Second)
    return nil
}

func (c *SIMAPIClient) ensureToken() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.accessToken == "" || time.Now().After(c.expiresAt) {
        return c.refresh()
    }
    return nil
}

func (c *SIMAPIClient) Get(path string) (map[string]interface{}, error) {
    if err := c.ensureToken(); err != nil {
        return nil, err
    }
    req, _ := http.NewRequest("GET", c.BaseURL+path, nil)
    req.Header.Set("Authorization", "Bearer "+c.accessToken)
    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    if resp.StatusCode == 401 {
        c.mu.Lock()
        c.refresh()
        c.mu.Unlock()
        req.Header.Set("Authorization", "Bearer "+c.accessToken)
        resp, _ = c.httpClient.Do(req)
        defer resp.Body.Close()
    }
    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    return result, nil
}

func main() {
    api := NewSIMAPIClient("app_absensi_abc123", "sk_live_xxxxxxxxxxxxx", "https://api.anda.com")
    data, err := api.Get("/api/v1/pegawai?page=1&limit=10")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(data)
}
Java
import java.net.URI;
import java.net.http.*;
import java.time.Instant;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;

public class SIMAPIClient {
    private final String clientId, clientSecret, baseUrl;
    private final HttpClient http;
    private final ObjectMapper mapper = new ObjectMapper();

    private String accessToken;
    private String refreshToken;
    private Instant expiresAt;

    public SIMAPIClient(String clientId, String clientSecret, String baseUrl) {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.baseUrl = baseUrl;
        this.http = HttpClient.newHttpClient();
    }

    private void login() throws Exception {
        String body = mapper.writeValueAsString(Map.of(
            "client_id", clientId,
            "client_secret", clientSecret,
            "grant_type", "client_credentials"
        ));
        var req = HttpRequest.newBuilder()
            .uri(URI.create(baseUrl + "/oauth/token"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        var resp = http.send(req, HttpResponse.BodyHandlers.ofString());
        var data = mapper.readTree(resp.body());
        accessToken = data.get("access_token").asText();
        refreshToken = data.get("refresh_token").asText();
        expiresAt = Instant.now().plusSeconds(data.get("expires_in").asLong() - 60);
    }

    private void refresh() throws Exception {
        if (refreshToken == null) { login(); return; }
        String body = mapper.writeValueAsString(Map.of(
            "refresh_token", refreshToken,
            "grant_type", "refresh_token"
        ));
        var req = HttpRequest.newBuilder()
            .uri(URI.create(baseUrl + "/oauth/token"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        var resp = http.send(req, HttpResponse.BodyHandlers.ofString());
        if (resp.statusCode() != 200) { login(); return; }
        var data = mapper.readTree(resp.body());
        accessToken = data.get("access_token").asText();
        refreshToken = data.get("refresh_token").asText();
        expiresAt = Instant.now().plusSeconds(data.get("expires_in").asLong() - 60);
    }

    private String ensureToken() throws Exception {
        if (accessToken == null || Instant.now().isAfter(expiresAt)) refresh();
        return accessToken;
    }

    public String get(String path) throws Exception {
        var req = HttpRequest.newBuilder()
            .uri(URI.create(baseUrl + path))
            .header("Authorization", "Bearer " + ensureToken())
            .GET()
            .build();
        var resp = http.send(req, HttpResponse.BodyHandlers.ofString());
        if (resp.statusCode() == 401) {
            refresh();
            req = HttpRequest.newBuilder()
                .uri(URI.create(baseUrl + path))
                .header("Authorization", "Bearer " + accessToken)
                .GET()
                .build();
            resp = http.send(req, HttpResponse.BodyHandlers.ofString());
        }
        return resp.body();
    }

    public static void main(String[] args) throws Exception {
        var api = new SIMAPIClient("app_absensi_abc123", "sk_live_xxxxxxxxxxxxx", "https://api.anda.com");
        System.out.println(api.get("/api/v1/pegawai?page=1&limit=10"));
    }
}
C# (.NET)
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;

public class SIMAPIClient
{
    private readonly string _clientId;
    private readonly string _clientSecret;
    private readonly string _baseUrl;
    private readonly HttpClient _http;

    private string _accessToken;
    private string _refreshToken;
    private DateTime _expiresAt;

    public SIMAPIClient(string clientId, string clientSecret, string baseUrl)
    {
        _clientId = clientId;
        _clientSecret = clientSecret;
        _baseUrl = baseUrl.TrimEnd('/');
        _http = new HttpClient();
    }

    private async Task LoginAsync()
    {
        var resp = await _http.PostAsJsonAsync($"{_baseUrl}/oauth/token", new
        {
            client_id = _clientId,
            client_secret = _clientSecret,
            grant_type = "client_credentials"
        });
        var data = await resp.Content.ReadFromJsonAsync();
        _accessToken = data.GetProperty("access_token").GetString();
        _refreshToken = data.GetProperty("refresh_token").GetString();
        var expiresIn = data.GetProperty("expires_in").GetInt32();
        _expiresAt = DateTime.UtcNow.AddSeconds(expiresIn - 60);
    }

    private async Task RefreshAsync()
    {
        if (string.IsNullOrEmpty(_refreshToken))
        {
            await LoginAsync();
            return;
        }
        var resp = await _http.PostAsJsonAsync($"{_baseUrl}/oauth/token", new
        {
            refresh_token = _refreshToken,
            grant_type = "refresh_token"
        });
        if (!resp.IsSuccessStatusCode)
        {
            await LoginAsync();
            return;
        }
        var data = await resp.Content.ReadFromJsonAsync();
        _accessToken = data.GetProperty("access_token").GetString();
        _refreshToken = data.GetProperty("refresh_token").GetString();
        var expiresIn = data.GetProperty("expires_in").GetInt32();
        _expiresAt = DateTime.UtcNow.AddSeconds(expiresIn - 60);
    }

    private async Task EnsureTokenAsync()
    {
        if (string.IsNullOrEmpty(_accessToken) || DateTime.UtcNow >= _expiresAt)
            await RefreshAsync();
        return _accessToken;
    }

    public async Task GetAsync(string path)
    {
        var token = await EnsureTokenAsync();
        var req = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}{path}");
        req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        var resp = await _http.SendAsync(req);

        if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            await RefreshAsync();
            req = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}{path}");
            req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken);
            resp = await _http.SendAsync(req);
        }
        return await resp.Content.ReadAsStringAsync();
    }

    public static async Task Main()
    {
        var api = new SIMAPIClient("app_absensi_abc123", "sk_live_xxxxxxxxxxxxx", "https://api.anda.com");
        Console.WriteLine(await api.GetAsync("/api/v1/pegawai?page=1&limit=10"));
    }
}
Web Frontend (SPA)
JANGAN simpan client_secret di browser. Kode SPA yang dikirim ke browser bisa dilihat siapa saja. Gunakan backend proxy (lihat tab di bawah) yang menyimpan client_secret dan mem-proxy request ke SIMAPI.
React (hooks)
import { useEffect, useState } from 'react';

// REACT — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
// Backend proxy menyimpan client_secret dan forward request ke SIMAPI.

function usePegawai() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/proxy/pegawai?page=1&limit=10')   // backend proxy Anda
      .then(r => {
        if (!r.ok) throw new Error('Gagal fetch');
        return r.json();
      })
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  return { data, loading, error };
}

function PegawaiList() {
  const { data, loading, error } = usePegawai();

  if (loading) return 
Memuat...
; if (error) return
Error: {error.message}
; return (
    {data?.data?.map(p => (
  • {p.nama} - {p.jabatan}
  • ))}
); } export default PegawaiList;
Vue 3 (Composition API)
<script setup>
import { ref, onMounted } from 'vue';

// VUE 3 — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
const pegawai = ref([]);
const loading = ref(true);
const error = ref(null);

onMounted(async () => {
  try {
    const resp = await fetch('/api/proxy/pegawai?page=1&limit=10');  // backend proxy
    if (!resp.ok) throw new Error('Gagal fetch');
    const result = await resp.json();
    pegawai.value = result.data;
  } catch (err) {
    error.value = err.message;
  } finally {
    loading.value = false;
  }
});
</script>

<template>
  
Memuat...
Error: {{ error }}
  • {{ p.nama }} - {{ p.jabatan }}
</template>
Angular
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

// ANGULAR — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)

@Injectable({ providedIn: 'root' })
export class PegawaiService {
  private proxyUrl = '/api/proxy/pegawai';   // backend proxy Anda

  constructor(private http: HttpClient) {}

  listPegawai(page = 1, limit = 10): Observable {
    return this.http.get(`${this.proxyUrl}?page=${page}&limit=${limit}`);
  }
}

// Component
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-pegawai-list',
  template: `
    
Memuat...
Error: {{ error }}
  • {{ p.nama }} - {{ p.jabatan }}
` }) export class PegawaiListComponent implements OnInit { pegawai: any[] = []; loading = true; error: string | null = null; constructor(private service: PegawaiService) {} ngOnInit() { this.service.listPegawai().subscribe({ next: (res) => { this.pegawai = res.data; this.loading = false; }, error: (err) => { this.error = err.message; this.loading = false; } }); } }
Backend Proxy (Flask)
# backend_proxy.py — WAJIB untuk web SPA & mobile
# Menyimpan client_secret, mem-proxy request ke SIMAPI.
# Frontend (React/Vue/Angular) hanya bicara dengan proxy ini.

import os
from datetime import datetime, timedelta
import requests
from flask import Flask, jsonify, request

app = Flask(__name__)

SIMAPI_BASE = os.getenv("SIMAPI_BASE", "https://api.anda.com")
CLIENT_ID = os.getenv("SIMAPI_CLIENT_ID")
CLIENT_SECRET = os.getenv("SIMAPI_CLIENT_SECRET")

# Cache token di memory
_token_cache = {"access_token": None, "refresh_token": None, "expires_at": None}


def _ensure_token():
    if _token_cache["access_token"] and datetime.now() < _token_cache["expires_at"]:
        return _token_cache["access_token"]

    # Coba refresh dulu
    if _token_cache["refresh_token"]:
        resp = requests.post(f"{SIMAPI_BASE}/oauth/token", json={
            "refresh_token": _token_cache["refresh_token"],
            "grant_type": "refresh_token",
        })
        if resp.status_code == 200:
            data = resp.json()
            _token_cache["access_token"] = data["access_token"]
            _token_cache["refresh_token"] = data["refresh_token"]
            _token_cache["expires_at"] = datetime.now() + timedelta(seconds=data["expires_in"] - 60)
            return _token_cache["access_token"]

    # Login ulang
    resp = requests.post(f"{SIMAPI_BASE}/oauth/token", json={
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type": "client_credentials",
    })
    data = resp.json()
    _token_cache["access_token"] = data["access_token"]
    _token_cache["refresh_token"] = data["refresh_token"]
    _token_cache["expires_at"] = datetime.now() + timedelta(seconds=data["expires_in"] - 60)
    return _token_cache["access_token"]


@app.route("/api/proxy/", methods=["GET", "POST", "PUT", "DELETE"])
def proxy(subpath):
    token = _ensure_token()
    headers = {"Authorization": f"Bearer {token}"}

    url = f"{SIMAPI_BASE}/api/v1/{subpath}"
    resp = requests.request(
        method=request.method,
        url=url,
        headers=headers,
        params=request.args,
        json=request.get_json(silent=True) if request.method != "GET" else None,
    )

    # Kalau 401, force refresh & retry
    if resp.status_code == 401:
        _token_cache["refresh_token"] = None
        token = _ensure_token()
        headers["Authorization"] = f"Bearer {token}"
        resp = requests.request(
            method=request.method,
            url=url,
            headers=headers,
            params=request.args,
            json=request.get_json(silent=True) if request.method != "GET" else None,
        )

    return (resp.content, resp.status_code, {"Content-Type": "application/json"})


# ============ Endpoint khusus IoT device ============
# Device pakai X-Device-Key (bukan client_secret) untuk autentikasi.
DEVICE_KEYS = {
    "device-abc123": "esp32-gudang-01",
    "device-def456": "esp8266-suhu-02",
}

@app.route("/api/device/", methods=["GET"])
def device_proxy(subpath):
    """Endpoint khusus IoT device — auth pakai X-Device-Key."""
    device_key = request.headers.get("X-Device-Key", "").strip()

    if device_key not in DEVICE_KEYS:
        return jsonify({"success": False, "error": "invalid_device_key"}), 401

    token = _ensure_token()
    url = f"{SIMAPI_BASE}/api/v1/{subpath}"

    resp = requests.get(
        url,
        headers={"Authorization": f"Bearer {token}"},
        params=request.args,
        timeout=10,
    )

    if resp.status_code == 401:
        _token_cache["refresh_token"] = None
        token = _ensure_token()
        resp = requests.get(
            url,
            headers={"Authorization": f"Bearer {token}"},
            params=request.args,
            timeout=10,
        )

    return (resp.content, resp.status_code, {"Content-Type": "application/json"})


if __name__ == "__main__":
    app.run(port=8000)
Mobile Apps
JANGAN simpan client_secret di mobile. APK/IPA bisa dibongkar. Gunakan backend proxy (tab sebelumnya) atau minta admin membuatkan endpoint khusus per-user.
Kotlin (Retrofit + Coroutines)
// Android — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
// Karena APK bisa dibongkar, client_secret TIDAK AMAN disimpan di mobile.

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

// === Model ===
data class Pegawai(val id: Int, val nama: String, val jabatan: String?)
data class ApiMeta(val pagination: Pagination)
data class Pagination(val page: Int, val limit: Int, val total: Int, val total_pages: Int)
data class ApiResponse(
    val success: Boolean,
    val data: List,
    val meta: ApiMeta?
)

// === Retrofit Interface ===
interface SIMAPIProxyService {
    @GET("api/proxy/pegawai")
    suspend fun listPegawai(
        @Query("page") page: Int = 1,
        @Query("limit") limit: Int = 10
    ): ApiResponse
}

// === Client ===
object ApiClient {
    private const val PROXY_BASE = "https://backend-anda.com/"  // backend proxy Anda

    val service: SIMAPIProxyService by lazy {
        Retrofit.Builder()
            .baseUrl(PROXY_BASE)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(SIMAPIProxyService::class.java)
    }
}

// === Usage di ViewModel ===
class PegawaiViewModel : ViewModel() {
    private val _pegawai = MutableLiveData>()
    val pegawai: LiveData> = _pegawai

    fun loadPegawai() {
        viewModelScope.launch {
            try {
                val resp = withContext(Dispatchers.IO) {
                    ApiClient.service.listPegawai(page = 1, limit = 10)
                }
                _pegawai.value = resp.data
            } catch (e: Exception) {
                Log.e("SIMAPI", "Error: ${e.message}")
            }
        }
    }
}
Swift (URLSession + async/await)
// iOS — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
// Karena IPA bisa dibongkar, client_secret TIDAK AMAN disimpan di iOS app.

import Foundation

// === Model ===
struct Pegawai: Codable {
    let id: Int
    let nama: String
    let jabatan: String?
}

struct Pagination: Codable {
    let page: Int
    let limit: Int
    let total: Int
    let totalPages: Int

    enum CodingKeys: String, CodingKey {
        case page, limit, total
        case totalPages = "total_pages"
    }
}

struct ApiMeta: Codable {
    let pagination: Pagination
}

struct ApiResponse: Codable {
    let success: Bool
    let data: [Pegawai]
    let meta: ApiMeta?
}

// === Client ===
class SIMAPIProxyClient {
    private let baseURL = "https://backend-anda.com"  // backend proxy Anda
    private let session = URLSession.shared

    func listPegawai(page: Int = 1, limit: Int = 10) async throws -> [Pegawai] {
        var components = URLComponents(string: "\(baseURL)/api/proxy/pegawai")!
        components.queryItems = [
            URLQueryItem(name: "page", value: "\(page)"),
            URLQueryItem(name: "limit", value: "\(limit)"),
        ]

        let (data, resp) = try await session.data(from: components.url!)
        guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else {
            throw NSError(domain: "SIMAPI", code: -1,
                          userInfo: [NSLocalizedDescriptionKey: "Gagal fetch"])
        }
        let result = try JSONDecoder().decode(ApiResponse.self, from: data)
        return result.data
    }
}

// === Usage di SwiftUI ===
import SwiftUI

@MainActor
class PegawaiViewModel: ObservableObject {
    @Published var pegawai: [Pegawai] = []
    @Published var isLoading = false
    @Published var errorMessage: String?

    private let client = SIMAPIProxyClient()

    func load() async {
        isLoading = true
        errorMessage = nil
        do {
            pegawai = try await client.listPegawai()
        } catch {
            errorMessage = error.localizedDescription
        }
        isLoading = false
    }
}

struct PegawaiListView: View {
    @StateObject private var viewModel = PegawaiViewModel()

    var body: some View {
        NavigationView {
            Group {
                if viewModel.isLoading {
                    ProgressView("Memuat...")
                } else if let err = viewModel.errorMessage {
                    Text("Error: \(err)")
                } else {
                    List(viewModel.pegawai, id: \.id) { p in
                        VStack(alignment: .leading) {
                            Text(p.nama).font(.headline)
                            Text(p.jabatan ?? "-").font(.subheadline)
                                .foregroundColor(.secondary)
                        }
                    }
                }
            }
            .navigationTitle("Pegawai")
        }
        .task { await viewModel.load() }
    }
}
Flutter (Dart)
// Flutter — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
// Flutter app bisa di-decompile, client_secret TIDAK AMAN.

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';

// === Model ===
class Pegawai {
  final int id;
  final String nama;
  final String? jabatan;

  Pegawai({required this.id, required this.nama, this.jabatan});

  factory Pegawai.fromJson(Map json) {
    return Pegawai(
      id: json['id'],
      nama: json['nama'],
      jabatan: json['jabatan'],
    );
  }
}

// === Client ===
class SIMAPIProxyClient {
  static const String baseURL = 'https://backend-anda.com';  // backend proxy Anda

  Future> listPegawai({int page = 1, int limit = 10}) async {
    final uri = Uri.parse('$baseURL/api/proxy/pegawai')
        .replace(queryParameters: {
      'page': '$page',
      'limit': '$limit',
    });

    final resp = await http.get(uri);
    if (resp.statusCode != 200) {
      throw Exception('Gagal fetch: ${resp.statusCode}');
    }

    final json = jsonDecode(resp.body) as Map;
    final data = json['data'] as List;
    return data.map((e) => Pegawai.fromJson(e)).toList();
  }
}

// === Widget ===
class PegawaiListPage extends StatefulWidget {
  @override
  _PegawaiListPageState createState() => _PegawaiListPageState();
}

class _PegawaiListPageState extends State {
  final _client = SIMAPIProxyClient();
  late Future> _future;

  @override
  void initState() {
    super.initState();
    _future = _client.listPegawai();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Pegawai')),
      body: FutureBuilder>(
        future: _future,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          }
          final list = snapshot.data ?? [];
          return ListView.builder(
            itemCount: list.length,
            itemBuilder: (context, i) {
              final p = list[i];
              return ListTile(
                title: Text(p.nama),
                subtitle: Text(p.jabatan ?? '-'),
              );
            },
          );
        },
      ),
    );
  }
}
React Native
// React Native — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
// Bundle JS bisa dibongkar, client_secret TIDAK AMAN.

import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';

const PROXY_BASE = 'https://backend-anda.com';  // backend proxy Anda

export default function PegawaiListScreen() {
  const [pegawai, setPegawai] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    (async () => {
      try {
        const resp = await fetch(`${PROXY_BASE}/api/proxy/pegawai?page=1&limit=10`);
        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
        const json = await resp.json();
        setPegawai(json.data);
      } catch (e) {
        setError(e.message);
      } finally {
        setLoading(false);
      }
    })();
  }, []);

  if (loading) {
    return (
      
        
        Memuat...
      
    );
  }

  if (error) {
    return (
      
        Error: {error}
      
    );
  }

  return (
     String(item.id)}
      renderItem={({ item }) => (
        
          {item.nama}
          {item.jabatan ?? '-'}
        
      )}
    />
  );
}

const styles = StyleSheet.create({
  center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  error: { color: 'red' },
  item: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' },
  title: { fontSize: 16, fontWeight: 'bold' },
  subtitle: { fontSize: 14, color: '#666' },
});
Desktop
Desktop app (Electron, .NET WPF, JavaFX, Python Tkinter) bisa menyimpan client_secret di backend app-nya (bukan di UI). Tapi lebih aman pakai backend proxy — sama seperti mobile.
Electron (main process)
// Electron main process — client_secret aman di sini
// Renderer (UI) hanya komunikasi via IPC ke main process.

const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

const SIMAPI_BASE = 'https://api.anda.com';
const CLIENT_ID = process.env.SIMAPI_CLIENT_ID;
const CLIENT_SECRET = process.env.SIMAPI_CLIENT_SECRET;

let tokenCache = { access: null, refresh: null, expiresAt: 0 };

async function ensureToken() {
  if (tokenCache.access && Date.now() < tokenCache.expiresAt) {
    return tokenCache.access;
  }

  if (tokenCache.refresh) {
    const resp = await fetch(`${SIMAPI_BASE}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        refresh_token: tokenCache.refresh,
        grant_type: 'refresh_token',
      }),
    });
    if (resp.ok) {
      const data = await resp.json();
      tokenCache.access = data.access_token;
      tokenCache.refresh = data.refresh_token;
      tokenCache.expiresAt = Date.now() + (data.expires_in - 60) * 1000;
      return tokenCache.access;
    }
  }

  const resp = await fetch(`${SIMAPI_BASE}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      grant_type: 'client_credentials',
    }),
  });
  const data = await resp.json();
  tokenCache.access = data.access_token;
  tokenCache.refresh = data.refresh_token;
  tokenCache.expiresAt = Date.now() + (data.expires_in - 60) * 1000;
  return tokenCache.access;
}

ipcMain.handle('simapi:get', async (event, path) => {
  const token = await ensureToken();
  let resp = await fetch(`${SIMAPI_BASE}${path}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (resp.status === 401) {
    tokenCache.refresh = null;
    const token2 = await ensureToken();
    resp = await fetch(`${SIMAPI_BASE}${path}`, {
      headers: { Authorization: `Bearer ${token2}` },
    });
  }
  return await resp.json();
});

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });
  win.loadFile('index.html');
}

app.whenReady().then(createWindow);
Python Tkinter / PyQt
# Python Desktop (Tkinter / PyQt) — client_secret aman di app
# Karena app desktop tidak dikirim ke user (hanya executable), secret relatif aman.

import tkinter as tk
from tkinter import ttk
import threading
from datetime import datetime, timedelta
import requests

SIMAPI_BASE = "https://api.anda.com"
CLIENT_ID = "app_absensi_abc123"
CLIENT_SECRET = "sk_live_xxxxxxxxxxxxx"


class SIMAPIClient:
    def __init__(self):
        self.access_token = None
        self.refresh_token = None
        self.expires_at = None

    def _login(self):
        resp = requests.post(f"{SIMAPI_BASE}/oauth/token", json={
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "grant_type": "client_credentials",
        })
        data = resp.json()
        self.access_token = data["access_token"]
        self.refresh_token = data["refresh_token"]
        self.expires_at = datetime.now() + timedelta(seconds=data["expires_in"] - 60)

    def _refresh(self):
        if not self.refresh_token:
            return self._login()
        resp = requests.post(f"{SIMAPI_BASE}/oauth/token", json={
            "refresh_token": self.refresh_token,
            "grant_type": "refresh_token",
        })
        if resp.status_code != 200:
            return self._login()
        data = resp.json()
        self.access_token = data["access_token"]
        self.refresh_token = data["refresh_token"]
        self.expires_at = datetime.now() + timedelta(seconds=data["expires_in"] - 60)

    def _ensure_token(self):
        if not self.access_token or datetime.now() >= self.expires_at:
            self._refresh()
        return self.access_token

    def get_pegawai(self):
        headers = {"Authorization": f"Bearer {self._ensure_token()}"}
        resp = requests.get(f"{SIMAPI_BASE}/api/v1/pegawai", headers=headers,
                            params={"page": 1, "limit": 10})
        if resp.status_code == 401:
            self._refresh()
            headers["Authorization"] = f"Bearer {self._ensure_token()}"
            resp = requests.get(f"{SIMAPI_BASE}/api/v1/pegawai", headers=headers,
                                params={"page": 1, "limit": 10})
        return resp.json()


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("SIMAPI — Daftar Pegawai")
        self.geometry("600x400")

        self.api = SIMAPIClient()

        self.tree = ttk.Treeview(self, columns=("id", "nama", "jabatan"),
                                 show="headings")
        self.tree.heading("id", text="ID")
        self.tree.heading("nama", text="Nama")
        self.tree.heading("jabatan", text="Jabatan")
        self.tree.pack(fill="both", expand=True, padx=10, pady=10)

        btn = ttk.Button(self, text="Muat Data", command=self.load_data)
        btn.pack(pady=5)

    def load_data(self):
        threading.Thread(target=self._load, daemon=True).start()

    def _load(self):
        try:
            data = self.api.get_pegawai()
            self.tree.delete(*self.tree.get_children())
            for p in data["data"]:
                self.tree.insert("", "end",
                                 values=(p["id"], p["nama"], p.get("jabatan", "-")))
        except Exception as e:
            print(f"Error: {e}")


if __name__ == "__main__":
    App().mainloop()

IoT / Embedded

SIMAPI bisa dipakai dari ESP32 dan ESP8266 untuk membaca data atau mengirim sensor ke database.

Perbandingan Board
BoardWiFiRAMBisa HTTPS?Rekomendasi
ESP32✅ Built-in~320 KB✅ Ya⭐ Terbaik
ESP8266✅ Built-in~80 KB⚠️ Berat✅ Bisa (dengan hati-hati)
Arduino Uno❌ Butuh shield2 KB❌ Tidak realistis❌ Tidak disarankan
Arduino Mega❌ Butuh shield8 KB⚠️ Sulit❌ Tidak disarankan
Raspberry Pi✅ Built-in1–8 GB✅ Ya⭐ Terbaik (kalau perlu komputasi)
JANGAN simpan client_secret di firmware Arduino/ESP! Firmware bisa di-extract dari flash memory dengan esptool.py read_flash. Gunakan backend proxy dengan device key sederhana (contoh di bawah).
Pendekatan yang Direkomendasikan: Backend Proxy

ESP32 cukup bicara HTTP ke backend proxy Anda, yang menyimpan client_secret dengan aman.

Arsitektur
┌─────────────┐     HTTP      ┌──────────────────┐     HTTPS     ┌─────────┐
│   ESP32     │ ────────────▶ │  Backend Proxy   │ ────────────▶ │ SIMAPI  │
│  (sensor)   │  X-Device-Key │  (Flask/Node.js) │ Bearer token  │         │
└─────────────┘               └──────────────────┘               └─────────┘
   di lapangan                    di server Anda                    di server

Firmware ESP32 yang mengirim data sensor ke backend proxy. Ringan (~30 KB RAM), tidak perlu HTTPS handshake.

ESP32 (Arduino C++)
// esp32_simapi_sensor.ino — Kirim data sensor ke SIMAPI via backend proxy

#include 
#include 
#include 

// === Konfigurasi ===
const char* WIFI_SSID     = "WifiKantor";
const char* WIFI_PASSWORD = "password-wifi";

// Backend proxy (BUKAN SIMAPI langsung — secret aman di server)
const char* PROXY_BASE    = "http://192.168.1.100:8000";   // backend proxy Anda
const char* DEVICE_KEY    = "device-abc123";                // device key sederhana

void setup() {
    Serial.begin(115200);
    delay(1000);

    Serial.println("Menghubungkan WiFi...");
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    unsigned long start = millis();
    while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
        delay(500);
        Serial.print(".");
    }

    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("\nWiFi terhubung!");
        Serial.print("IP: ");
        Serial.println(WiFi.localIP());
    } else {
        Serial.println("\nWiFi gagal.");
    }
}

void loop() {
    if (WiFi.status() != WL_CONNECTED) {
        delay(5000);
        return;
    }

    // Baca sensor (contoh: suhu)
    float suhu = random(200, 350) / 10.0;   // 20.0 – 35.0 °C
    float kelembaban = random(400, 800) / 10.0;

    // Kirim ke backend proxy
    sendSensorData(suhu, kelembaban);

    delay(30000);   // kirim tiap 30 detik
}

void sendSensorData(float suhu, float kelembaban) {
    HTTPClient http;

    String url = String(PROXY_BASE) + "/api/device/sensor_suhu";

    http.begin(url);
    http.addHeader("Content-Type", "application/json");
    http.addHeader("X-Device-Key", DEVICE_KEY);

    StaticJsonDocument<256> doc;
    doc["device_id"] = "esp32-gudang-01";
    doc["suhu"] = suhu;
    doc["kelembaban"] = kelembaban;

    String body;
    serializeJson(doc, body);

    Serial.println("POST " + url);
    Serial.println("Body: " + body);

    int httpCode = http.POST(body);

    if (httpCode > 0) {
        String response = http.getString();
        Serial.printf("HTTP %d: %s\n", httpCode, response.c_str());
    } else {
        Serial.printf("Error: %s\n", http.errorToString(httpCode).c_str());
    }

    http.end();
}
Backend Proxy — Endpoint Device
# Tambahkan endpoint ini di backend_proxy.py (Flask)

DEVICE_KEYS = {
    "device-abc123": "esp32-gudang-01",
    "device-def456": "esp8266-suhu-02",
}

@app.route("/api/device/<path:subpath>", methods=["GET", "POST"])
def device_proxy(subpath):
    """
    Endpoint khusus IoT device.
    Autentikasi pakai X-Device-Key (sederhana, tidak perlu OAuth penuh).
    """
    device_key = request.headers.get("X-Device-Key", "").strip()

    if device_key not in DEVICE_KEYS:
        return jsonify({"success": False, "error": "invalid_device_key"}), 401

    token = _ensure_token()
    url = f"{SIMAPI_BASE}/api/v1/{subpath}"

    resp = requests.request(
        method=request.method,
        url=url,
        headers={"Authorization": f"Bearer {token}"},
        params=request.args,
        json=request.get_json(silent=True) if request.method != "GET" else None,
        timeout=10,
    )

    if resp.status_code == 401:
        _token_cache["refresh_token"] = None
        token = _ensure_token()
        resp = requests.request(
            method=request.method,
            url=url,
            headers={"Authorization": f"Bearer {token}"},
            params=request.args,
            json=request.get_json(silent=True) if request.method != "GET" else None,
            timeout=10,
        )

    return (resp.content, resp.status_code, {"Content-Type": "application/json"})
Keuntungan pendekatan ini: firmware ESP32 tidak menyimpan client_secret, tidak perlu HTTPS handshake (ringan), dan mudah ganti device key tanpa update firmware semua device.
⚠️ TIDAK DIREKOMENDASIKAN untuk production. client_secret ada di firmware dan bisa di-extract dengan esptool.py read_flash. Hanya untuk prototype/hobby. Kalau tetap dipakai, wajib aktifkan flash encryption + secure boot di ESP32.
ESP32 → SIMAPI Langsung (HTTPS + OAuth)
// esp32_simapi_direct.ino — ESP32 → SIMAPI langsung (OAuth + HTTPS)
// ⚠️ TIDAK DIREKOMENDASIKAN untuk production.

#include 
#include 
#include 
#include 

const char* WIFI_SSID     = "WifiKantor";
const char* WIFI_PASSWORD = "password-wifi";

const char* SIMAPI_BASE   = "https://api.anda.com";
const char* CLIENT_ID     = "esp32_sensor_abc123";
const char* CLIENT_SECRET = "sk_live_xxxxxxxxxxxxx";   // ⚠️ JANGAN di production!

String accessToken = "";
String refreshToken = "";
unsigned long tokenExpiresAt = 0;

// Root CA (ganti dengan CA server Anda — bisa pakai CA Let's Encrypt)
const char* ROOT_CA = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ\n" \
"... isi dengan CA certificate server Anda ...\n" \
"-----END CERTIFICATE-----\n";

void setup() {
    Serial.begin(115200);
    delay(1000);

    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nWiFi OK");

    login();
}

void loop() {
    if (WiFi.status() != WL_CONNECTED) { delay(5000); return; }

    // Refresh token kalau hampir expired (60 detik sebelum)
    if (millis() > tokenExpiresAt - 60000) {
        refreshAccessToken();
    }

    fetchPegawai();

    delay(60000);
}

void login() {
    WiFiClientSecure client;
    client.setCACert(ROOT_CA);   // verifikasi sertifikat server (WAJIB)

    HTTPClient http;
    String url = String(SIMAPI_BASE) + "/oauth/token";
    http.begin(client, url);
    http.addHeader("Content-Type", "application/json");

    StaticJsonDocument<256> req;
    req["client_id"] = CLIENT_ID;
    req["client_secret"] = CLIENT_SECRET;
    req["grant_type"] = "client_credentials";

    String body;
    serializeJson(req, body);

    int httpCode = http.POST(body);

    if (httpCode == 200) {
        String response = http.getString();
        DynamicJsonDocument doc(1024);
        deserializeJson(doc, response);

        accessToken = doc["access_token"].as();
        refreshToken = doc["refresh_token"].as();
        int expiresIn = doc["expires_in"].as();
        tokenExpiresAt = millis() + (expiresIn * 1000UL);

        Serial.println("Login OK, expires in " + String(expiresIn) + "s");
    } else {
        Serial.printf("Login gagal: HTTP %d\n", httpCode);
    }

    http.end();
}

void refreshAccessToken() {
    if (refreshToken.isEmpty()) {
        login();
        return;
    }

    WiFiClientSecure client;
    client.setCACert(ROOT_CA);

    HTTPClient http;
    String url = String(SIMAPI_BASE) + "/oauth/token";
    http.begin(client, url);
    http.addHeader("Content-Type", "application/json");

    StaticJsonDocument<256> req;
    req["refresh_token"] = refreshToken;
    req["grant_type"] = "refresh_token";

    String body;
    serializeJson(req, body);

    int httpCode = http.POST(body);

    if (httpCode == 200) {
        String response = http.getString();
        DynamicJsonDocument doc(1024);
        deserializeJson(doc, response);

        accessToken = doc["access_token"].as();
        refreshToken = doc["refresh_token"].as();
        int expiresIn = doc["expires_in"].as();
        tokenExpiresAt = millis() + (expiresIn * 1000UL);

        Serial.println("Token refreshed");
    } else {
        Serial.printf("Refresh gagal: HTTP %d, fallback login\n", httpCode);
        login();
    }

    http.end();
}

void fetchPegawai() {
    WiFiClientSecure client;
    client.setCACert(ROOT_CA);

    HTTPClient http;
    String url = String(SIMAPI_BASE) + "/api/v1/pegawai?page=1&limit=5";
    http.begin(client, url);
    http.addHeader("Authorization", "Bearer " + accessToken);

    int httpCode = http.GET();

    if (httpCode == 200) {
        String response = http.getString();
        DynamicJsonDocument doc(4096);
        deserializeJson(doc, response);

        JsonArray data = doc["data"].as();
        Serial.println("Dapat " + String(data.size()) + " pegawai:");

        for (JsonObject p : data) {
            String nama = p["nama"].as();
            String jabatan = p["jabatan"].as();
            Serial.println("- " + nama + " (" + jabatan + ")");
        }
    } else {
        Serial.printf("Fetch gagal: HTTP %d\n", httpCode);
    }

    http.end();
}
Kalau tetap pakai cara ini, wajib:
  1. Aktifkan HTTPS dengan setCACert()
  2. Aktifkan flash encryption di ESP32
  3. Aktifkan secure boot (mencegah firmware dimodifikasi)
  4. Set client ESP32 dengan access token pendek (15 menit) & rate limit ketat (10/menit)
  5. Simpan refresh_token di NVS (bukan di RAM — supaya tidak hilang saat restart)

Error & Rate Limit

Kode Error
HTTPCodeArtinyaSolusi
401invalid_clientClient ID / secret salahCek kredensial
401invalid_tokenAccess token expiredRefresh token / login ulang
401invalid_grantRefresh token expired / reuseLogin ulang
403insufficient_scopeScope kurangHubungi admin
404resource_not_foundResource tidak adaCek endpoint
429rate_limit_exceededRate limit habisRetry-After header
500internal_errorServer errorHubungi admin
Rate Limit

Cek header response:

Response Headers
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 198
X-RateLimit-Reset: 60
X-RateLimit-Window: minute

Kalau kena rate limit:

JSON (HTTP 429)
{
  "success": false,
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded (minute). Limit: 200. Retry in 45s.",
    "retry_after": 45
  }
}

FAQ — Tanya Jawab

Bisa, tapi dengan catatan:

  • ESP32 — ✅ paling cocok (RAM 320 KB, HTTPS support)
  • ESP8266 — ⚠️ bisa, tapi HTTPS sering gagal (RAM 80 KB)
  • Arduino Uno/Mega — ❌ tidak realistis (RAM 2–8 KB, tidak cukup untuk HTTPS + JWT)
  • Raspberry Pi — ✅ sangat cocok (full Linux, RAM GB)

Cara terbaik untuk ESP32: pakai backend proxy dengan device key sederhana — bukan simpan client_secret di firmware. Contoh kode lengkap ada di section IoT / Embedded.

Firmware ESP32 bisa di-extract dari flash memory dengan perintah:

esptool.py
esptool.py --port /dev/ttyUSB0 read_flash 0x0 0x400000 firmware.bin
strings firmware.bin | grep "sk_live_"

Kalau client_secret ada di firmware, penyerang bisa mengambilnya dan mengakses SIMAPI atas nama device Anda.

Solusi: simpan client_secret di backend proxy (server), ESP32 cukup kirim X-Device-Key (string acak sederhana, bisa di-revoke tanpa update firmware).

Refresh token adalah token khusus untuk memperbarui access token tanpa perlu login ulang (tanpa kirim client_secret).

  • Access token — default 1 jam, untuk akses API
  • Refresh token — default 30 hari, untuk refresh access token

Setiap kali refresh, server mengembalikan refresh token baru (rotation). Token lama otomatis di-revoke. Kalau token lama dipakai lagi, semua token client di-revoke (reuse detection).

Jangan simpan client_secret di mobile app. APK/IPA bisa dibongkar, sehingga secret bisa dicuri.

Solusi: pakai backend proxy.

  1. Buat backend kecil (Flask/Node.js) di server Anda
  2. Backend menyimpan client_secret di environment variable
  3. Backend proxy request ke SIMAPI
  4. Mobile app hanya bicara ke backend proxy Anda

Contoh kode backend proxy ada di tab "Backend Proxy (WAJIB)" di section Contoh Penggunaan.

Bisa, tapi dengan backend proxy. Kode SPA dikirim ke browser, jadi siapapun bisa lihat source code — client_secret akan terlihat.

Pola yang benar:

Browser (React/Vue)
    ↓ fetch('/api/proxy/...')
Backend Proxy (Flask/Node.js)
    ↓ Authorization: Bearer ...
SIMAPI

Frontend hanya bicara ke backend proxy Anda, tidak langsung ke SIMAPI.

SIMAPI mendukung semua platform yang bisa kirim HTTP request:

  • Backend — cURL, Python, Node.js, PHP, Go, Java, C# (.NET), Ruby, Rust, dll.
  • Web frontend — React, Vue, Angular, Svelte (dengan backend proxy)
  • Mobile — Android (Kotlin/Java), iOS (Swift), Flutter, React Native, Xamarin (dengan backend proxy)
  • Desktop — Electron, .NET WPF/WinForms, JavaFX, Python Tkinter/PyQt
  • IoT — ESP32, ESP8266, Raspberry Pi (dengan backend proxy untuk keamanan)

Contoh kode lengkap ada di section Contoh Penggunaan & IoT / Embedded.

Token rotation adalah praktik keamanan standar OAuth2. Setiap kali refresh token dipakai, server membuat token baru dan merevoke yang lama.

  • Kalau refresh token bocor, penyerang hanya bisa pakai sekali
  • Kalau penyerang pakai token lama → terdeteksi (reuse detection) → semua token client di-revoke otomatis
  • Server bisa melacak siapa yang masih aktif

Ya! Setiap client bisa punya durasi token berbeda dari default:

  • Access token — default 60 menit, bisa diubah 5–1440 menit
  • Refresh token — default 30 hari, bisa diubah 1–365 hari

Contoh:

  • Aplikasi backend internal → access 60 menit, refresh 30 hari
  • Aplikasi mobile → access 15 menit, refresh 7 hari (lebih aman)
  • Dashboard publik → access 5 menit, refresh 1 hari (paling aman)
  • ESP32 / IoT → access 15 menit, refresh 1 hari (paling aman untuk device)

SIMAPI v2.0 mendukung 6 jenis database:

  • MySQL (siap pakai, port 3306)
  • MariaDB (siap pakai, port 3306)
  • SQLite (siap pakai, file .db)
  • PostgreSQL (perlu install psycopg2-binary, port 5432)
  • Oracle (perlu install oracledb, port 1521)
  • SQL Server (perlu install pyodbc + ODBC Driver, port 1433)

Hubungi admin SIMAPI di organisasi Anda. Admin akan mendaftarkan aplikasi Anda dan memberikan kredensial melalui channel aman (password manager / file terenkripsi). Secret hanya ditampilkan sekali.

Field di SIMAPI punya 3 level proteksi:

  • Normal — bisa diakses dengan scope read:basic
  • Sensitive — butuh scope read:sensitive (contoh: gaji, NIK)
  • Protected — tidak pernah muncul di API (contoh: password_hash)

Kalau butuh akses sensitive, hubungi admin untuk menambah scope.

Segera hubungi admin SIMAPI. Admin akan:

  1. Revoke semua secret client Anda
  2. Revoke semua refresh token aktif
  3. Generate secret baru
  4. Kirim secret baru melalui channel aman

Update aplikasi Anda dengan secret baru, restart, dan login ulang.

Ya. Setiap client punya rate limit yang diatur admin:

  • Limit per menit (default 60)
  • Limit per jam (default 1000)
  • Kuota harian (default 10000)

Kalau butuh lebih tinggi, hubungi admin.

Captcha
Masukkan 5 digit angka pada gambar.