ESC
Ketik untuk mencari…
v2026
Dokumentasi ini masih dalam pengembangan dan mungkin belum sepenuhnya mencerminkan cara kerja aplikasi. Bergabung di forum untuk bertanya dan berbagi masukan →
Docs Developer Ekstensi Add-In

Ekstensi Add-In

Add-in memungkinkan Anda menyematkan aplikasi web pihak ketiga ke dalam Basis sebagai dialog modal. Add-in berjalan dalam iframe yang terisolasi (sandboxed) dan berkomunikasi dengan Basis melalui protokol postMessage yang terstruktur — Basis menjadi perantara semua akses data menggunakan sesi pengguna yang sedang login, sehingga add-in tidak memerlukan kredensial tersendiri.

Contoh penggunaan:

  • Pengiriman e-Faktur (DJP Coretax, KSA ZATCA, MyInvois, dll.)
  • Integrasi tanda tangan digital
  • Pembuatan label pengiriman / logistik
  • Alur persetujuan kustom
  • Jembatan ERP — kirim data Basis ke sistem eksternal

Mendaftarkan Add-In (Pengguna)

Buka Administrasi → Ekstensi / Add-In → Baru.

Field Keterangan
Nama Ditampilkan pada tombol/menu di dalam Basis.
Entry URL URL lengkap aplikasi web add-in, mis. https://zatca.example.com/basis. Harus menggunakan https://.
Allowed Origin Origin (scheme + host) yang diterima Basis untuk pesan masuk, mis. https://zatca.example.com. Divalidasi pada setiap pesan yang masuk.
Placement Lokasi tombol add-in ditampilkan. Pilih satu atau lebih dari daftar.
Lebar / Tinggi Modal Nilai CSS untuk ukuran dialog, mis. 85vw / 85vh. Di perangkat mobile dialog selalu melebar hampir penuh layar.
Aktif Add-in yang tidak aktif disembunyikan dari semua pengguna.

Daftar Placement

Placement Lokasi tombol
Sales Invoice Halaman preview Faktur Penjualan
Purchase Invoice Halaman preview Faktur Pembelian
Credit Note Halaman preview Nota Kredit
Debit Note Halaman preview Nota Debit
Sales Order Halaman preview Sales Order
Purchase Order Halaman preview Purchase Order
Delivery Note Halaman preview Surat Jalan
Receipt Note Halaman preview Nota Penerimaan
Payment Halaman preview Pengeluaran
Receipt Halaman preview Penerimaan
Journal Entry Halaman preview Jurnal
Contra Entry Halaman preview Kontra Voucher
Global Bilah navigasi atas — selalu terlihat di semua halaman

Add-in dengan placement Global menampilkan tombolnya di navigasi atas, di sebelah tombol tema. Add-in Global tidak menerima entityId dan cocok untuk operasi lintas dokumen (dasbor, pengiriman massal, dll.).

Jika beberapa add-in berbagi placement yang sama, Basis menampilkan dropdown menu yang menampilkan semua add-in di bawah label "Ekstensi".


Membangun Add-In (Developer)

Add-in adalah aplikasi web biasa yang di-host di mana saja (server sendiri, CDN, SaaS). Basis memuatnya dalam <iframe> dengan atribut:

sandbox="allow-scripts allow-same-origin allow-forms allow-popups"

Tidak perlu instalasi SDK. Komunikasi dilakukan melalui postMessage biasa. Origin add-in harus sesuai dengan field Allowed Origin yang didaftarkan di Basis — Basis menolak semua pesan dari origin lain.

Arsitektur

Basis (host)                     Add-In (iframe)
─────────────────────────────────────────────────
Pengguna buka halaman preview
  → klik tombol → buka modal iframe
  → iframe selesai load → basis:init ───────────►
                                 Add-in terima konteks
                                 (placement, entityId…)
                   ◄─────────── basis:request (resource)
  → proxy ke service layer
  → basis:response ─────────────────────────────►
                                 Add-in render UI
                   ◄─────────── basis:setAddinSettings
                   ◄─────────── basis:toast / basis:close

Basis menangani semua pemanggilan service di sisi server menggunakan sesi pengguna yang login. Add-in tidak pernah menerima token, kredensial, atau akses langsung ke database.


Langkah 1 — Terima Inisialisasi

Saat iframe selesai dimuat, Basis mengirim pesan basis:init:

window.addEventListener('message', (event) => {
    // SELALU validasi origin sebelum memproses
    if (event.origin !== 'https://host-basis-anda.com') return;

    const msg = event.data;
    if (msg.type !== 'basis:init') return;

    console.log(msg.placement);   // mis. "SalesInvoice"
    console.log(msg.entityType);  // mis. "SalesInvoice"
    console.log(msg.entityId);    // string UUID, null untuk Global
    console.log(msg.language);    // "en" atau "id"
});

Payload basis:init:

{
  "type":         "basis:init",
  "placement":    "SalesInvoice",
  "entityType":   "SalesInvoice",
  "entityId":     "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "language":     "id",
  "userId":       "a1b2c3d4-0000-0000-0000-000000000001",
  "userName":     "Budi Santoso",
  "userEmail":    "budi@example.com",
  "businessId":   "acme-id",
  "businessName": "PT Acme Indonesia",
  "businessRole": "Owner"
}
Field Nilai
placement Nama enum placement yang didaftarkan (mis. "SalesInvoice")
entityType String tipe dokumen. CreditNote dan DebitNote berbagi placement dengan SalesInvoice/PurchaseInvoice tetapi memiliki nilai entityType yang berbeda.
entityId UUID dokumen saat ini, atau null untuk Global.
language Kode bahasa dua huruf pengguna aktif ("en" atau "id").
userId UUID pengguna yang sedang login.
userName Nama tampilan pengguna yang sedang login.
userEmail Alamat email pengguna yang sedang login.
businessId Identifier internal bisnis aktif.
businessName Nama tampilan bisnis aktif.
businessRole Peran pengguna di bisnis ini: "Owner", "Admin", "Staff", dll.

Simpan nilai-nilai ini — Anda akan menyertakan entityId pada permintaan data berikutnya.


Langkah 2 — Minta Data

Kirim pesan basis:request dan terima basis:response (atau basis:error) kembali.

Fungsi helper:

let _seq = 0;
const _pending = {};

function basisRequest(resource, extra = {}) {
    return new Promise((resolve, reject) => {
        const id = `r${++_seq}`;
        _pending[id] = { resolve, reject };

        window.parent.postMessage(
            { type: 'basis:request', id, resource, ...extra },
            '*'   // Basis memvalidasi origin di server; gunakan '*' di sini
        );
    });
}

window.addEventListener('message', (event) => {
    if (event.origin !== 'https://host-basis-anda.com') return;
    const msg = event.data;

    if (msg.type === 'basis:response' && _pending[msg.id]) {
        _pending[msg.id].resolve(msg.data);
        delete _pending[msg.id];
    }
    if (msg.type === 'basis:error' && _pending[msg.id]) {
        _pending[msg.id].reject(new Error(msg.message));
        delete _pending[msg.id];
    }
});

Resource: voucher

Mengambil dokumen saat ini (berdasarkan entityType + entityId dari basis:init):

const invoice = await basisRequest('voucher');
// Mengembalikan VoucherDetailResponse atau objek detail order lengkap

Mengembalikan DTO yang sama dengan REST API Basis untuk tipe dokumen tersebut. GUID seperti partyId, itemId, dan taxCategoryId disertakan tetapi tidak di-resolve ke objek lengkap.

Resource: voucherFull

Mengambil dokumen saat ini beserta semua entitas yang direferensikan, dalam satu panggilan. Dirancang untuk integrasi yang membutuhkan data lengkap tanpa melakukan banyak request tambahan.

const data = await basisRequest('voucherFull');
// {
//   voucher:         { …voucher lengkap… },
//   party:           { name, taxNumber, addresses, customFields, … },
//   items:           { "<itemId>": { code, name, customFields, … }, … },
//   taxCategories:   { "<taxCategoryId>": { name, customFields, rates: [ { name, rate, customFields, … } ] }, … },
//   customFieldDefs: [ { id, name, dataType, … }, … ]
// }

items adalah map itemId → ItemDetailResponse. Custom fields pada item (mis. kode HS, negara asal) disertakan di dalam setiap objek item.

taxCategories adalah map taxCategoryId → TaxCategoryResponse. Setiap entri memuat nama kategori, customFields-nya, dan array rates — masing-masing tarif juga memiliki customFields sendiri. Ini adalah tempat yang tepat untuk menyimpan kode invoicing yang dibutuhkan standar e-faktur (mis. kode jenis pajak DJP, kode kategori ZATCA), karena kode tersebut merupakan bagian dari konfigurasi pajak, bukan dari dokumen itu sendiri.

customFieldDefs mendaftar semua definisi field untuk bisnis ini, sehingga add-in dapat menginterpretasi dictionary customFields (yang diindeks oleh CustomFieldDef.Id).

Persentase tarif pajak per baris juga tersedia di voucher.ledgerEntries — entri dengan sourceType = "Tax" memuat taxRate, taxableAmount, dan taxConfigName, dihubungkan kembali ke barisnya melalui sourceItemAllocationId.

Resource: business

Mengambil profil bisnis aktif:

const biz = await basisRequest('business');
// { name, taxNumber, address, city, country, currency, … }

Resource: addinSettings

Mengambil pengaturan tersimpan milik add-in (disimpan di database bisnis):

const settings = await basisRequest('addinSettings', { key: 'my-addin-v1' });
// Mengembalikan objek yang sebelumnya disimpan via basis:setAddinSettings
// Mengembalikan {} jika belum ada pengaturan yang disimpan

key adalah string stabil yang Anda definisikan dalam kode add-in. Key ini bertahan meskipun add-in dihapus dan didaftarkan ulang, atau URL host berubah — Basis menyimpannya secara terpisah dari record AddInDef.

Resource: api

Proxy penuh ke REST API Basis. Berikan path relatif dan method HTTP — Basis meneruskan permintaan di sisi server menggunakan sesi dan izin pengguna yang login. Segmen businessId ditambahkan otomatis; jangan sertakan dalam path.

// Daftar faktur penjualan (paginated)
const list = await basisRequest('api', {
    method: 'GET',
    path:   '/sales?page=1&pageSize=20'
});

// Ambil detail pihak (NPWP, alamat, dll.)
const party = await basisRequest('api', {
    method: 'GET',
    path:   `/parties/${partyId}`
});

// Buat record baru
const invoice = await basisRequest('api', {
    method: 'POST',
    path:   '/sales',
    body:   JSON.stringify({ date: '2026-06-06', partyId: '…', lines: [ … ] })
});

// Update record
await basisRequest('api', {
    method: 'PUT',
    path:   `/sales/${invoiceId}`,
    body:   JSON.stringify({ … })
});

// Hapus record
await basisRequest('api', { method: 'DELETE', path: `/sales/${invoiceId}` });

Semua endpoint REST API Basis dapat diakses — /sales, /purchases, /payments, /receipts, /journals, /contra-entries, /sales-orders, /purchase-orders, /sales-orders/delivery-notes/{id}, /purchase-orders/receipt-notes/{id}, /parties, /items, /custom-field-defs, dan lainnya. Lihat dokumentasi REST API untuk daftar endpoint lengkap.

Wrapper ApiResponse<T>.data dibuka otomatis sebelum nilai dikembalikan ke add-in.

Envelope respons:

{ "type": "basis:response", "id": "r1", "data": { … } }

Envelope error:

{ "type": "basis:error", "id": "r1", "message": "Not found" }

Langkah 3 — Tulis Data

Simpan Pengaturan Add-In

Simpan konfigurasi add-in ke database bisnis (mis. kredensial API, status onboarding):

window.parent.postMessage({
    type:     'basis:setAddinSettings',
    key:      'my-addin-v1',
    settings: { apiKey: 'abc123', region: 'SA', onboarded: true }
}, '*');

settings dapat berupa objek JSON apa pun. Basis menyimpannya sebagai blob JSON dengan key yang diberikan di dalam database bisnis. Tidak ada respons yang dikirim balik.

Catatan keamanan: Basis menyimpan pengaturan sebagai plaintext. Jangan simpan private key atau secret sensitif langsung di sini. Gunakan layanan manajemen kunci (KMS) Anda sendiri jika enkripsi diperlukan.

Set Nilai Custom Field

Tulis nilai ke custom field pada dokumen yang sedang dibuka (entity di halaman preview). Hanya tersedia di transaction placement — tidak di Global (yang tidak memiliki konteks entity).

window.parent.postMessage({
    type:    'basis:setCustomField',
    fieldId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',  // GUID CustomFieldDef
    value:   'REF-20260601-0042'
}, '*');

fieldId adalah GUID CustomFieldDef.Id dari Pengaturan → Custom Fields. Nilai selalu berupa string; Basis mengurai ke tipe penyimpanan yang sesuai:

Tipe field Format nilai
Single line / Paragraph / List String teks biasa
Number String desimal invariant culture, mis. "1234.56"
Date ISO 8601, mis. "2026-06-06" atau "2026-06-06T00:00:00Z"
Yes/No "true" atau "false"

Menggunakan GUID milik add-in (penyimpanan privat): fieldId tidak perlu didaftarkan di Administrasi → Custom Fields. Anda dapat menggunakan GUID yang Anda buat sekali dan hardcode dalam kode add-in. Basis akan menyimpan nilainya di blob customFields.strings milik entity. Berguna untuk menyimpan status integrasi (status pengiriman, nomor referensi, timestamp) langsung pada dokumen tanpa perlu konfigurasi admin.

// GUID privat — hardcoded di add-in, tidak pernah berubah
const SUBMISSION_STATUS_KEY = '11111111-aaaa-bbbb-cccc-000000000001';

window.parent.postMessage({
    type:    'basis:setCustomField',
    fieldId: SUBMISSION_STATUS_KEY,
    value:   'submitted'
}, '*');

Menggunakan field terdaftar (tampil di UI): Jika field harus muncul di form Basis dan dokumen PDF, field tersebut harus didaftarkan di Administrasi → Custom Fields. Karena fieldId berbeda di setiap instalasi Basis, pola discovery yang direkomendasikan adalah:

  1. Saat pertama dijalankan, panggil GET /custom-field-defs via resource:api untuk mendaftar field yang tersedia.
  2. Biarkan user/admin memilih field mana yang memetakan output integrasi.
  3. Simpan pilihan via basis:setAddinSettings.
  4. Saat runtime, baca settings → ambil fieldId → tulis nilai.

Tidak ada respons yang dikirim balik. Kirim basis:refresh setelahnya agar halaman preview memuat ulang dan menampilkan nilai yang diperbarui.


Langkah 4 — Aksi UI

Tampilkan Toast Notifikasi

window.parent.postMessage({
    type:    'basis:toast',
    intent:  'success',   // 'success' | 'error' | 'warning' | 'info'
    message: 'Berhasil dikirim ke ZATCA.'
}, '*');

Refresh Halaman Host

Instruksikan Basis untuk memuat ulang dokumen saat ini dan meregenerasi PDF/preview:

window.parent.postMessage({ type: 'basis:refresh' }, '*');

Gunakan ini setelah menulis custom field agar pengguna melihat nilai yang diperbarui.

Tutup Dialog

window.parent.postMessage({ type: 'basis:close' }, '*');

Menutup modal. Halaman preview di bawahnya tetap terbuka.


Contoh Lengkap Minimal

Add-in satu file minimal yang membaca voucher dan menyimpan pengaturan:

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Add-In Saya</title>
    <style>
        body { font-family: sans-serif; padding: 16px; }
        button { padding: 8px 16px; margin-top: 8px; cursor: pointer; }
        pre { background: #f5f5f5; padding: 12px; font-size: 12px; overflow: auto; }
    </style>
</head>
<body>
    <h2>Add-In Saya</h2>
    <button onclick="loadVoucher()">Muat Voucher</button>
    <button onclick="submit()">Kirim</button>
    <button onclick="closeMe()">Tutup</button>
    <pre id="out">Menunggu init…</pre>

    <script>
        const BASIS_ORIGIN = 'https://app.basis-apps.net'; // ganti dengan host Anda
        let _ctx = null;
        let _seq  = 0;
        const _pending = {};

        window.addEventListener('message', (event) => {
            if (event.origin !== BASIS_ORIGIN) return;
            const msg = event.data;

            if (msg.type === 'basis:init') {
                _ctx = msg;
                document.getElementById('out').textContent =
                    'Init diterima:\n' + JSON.stringify(msg, null, 2);
                return;
            }
            if ((msg.type === 'basis:response' || msg.type === 'basis:error')
                    && _pending[msg.id]) {
                const p = _pending[msg.id];
                delete _pending[msg.id];
                msg.type === 'basis:response' ? p.resolve(msg.data) : p.reject(new Error(msg.message));
            }
        });

        function basisRequest(resource, extra = {}) {
            return new Promise((resolve, reject) => {
                const id = `r${++_seq}`;
                _pending[id] = { resolve, reject };
                window.parent.postMessage({ type: 'basis:request', id, resource, ...extra }, '*');
            });
        }

        async function loadVoucher() {
            try {
                const voucher = await basisRequest('voucher');
                document.getElementById('out').textContent =
                    JSON.stringify(voucher, null, 2);
            } catch (e) {
                document.getElementById('out').textContent = 'Error: ' + e.message;
            }
        }

        async function submit() {
            window.parent.postMessage({
                type: 'basis:setAddinSettings',
                key:  'my-addin-v1',
                settings: { lastSubmit: new Date().toISOString() }
            }, '*');

            window.parent.postMessage({
                type: 'basis:toast', intent: 'success', message: 'Selesai!'
            }, '*');

            window.parent.postMessage({ type: 'basis:refresh' }, '*');
            window.parent.postMessage({ type: 'basis:close' }, '*');
        }

        function closeMe() {
            window.parent.postMessage({ type: 'basis:close' }, '*');
        }
    </script>
</body>
</html>

Model Keamanan

Aspek Cara penanganan
Validasi origin Basis memvalidasi event.origin === AllowedOrigin pada setiap pesan masuk. Pesan dari origin lain diam-diam diabaikan.
Autentikasi Semua permintaan data menggunakan sesi server pengguna yang login. Tidak ada token yang diteruskan ke iframe.
Izin akses Add-in mewarisi izin pengguna yang sedang login — tidak dapat membaca atau menulis data yang tidak dapat diakses pengguna secara langsung.
Sandbox iframe allow-scripts allow-same-origin allow-forms allow-popups — tidak ada navigasi top-level, tidak ada akses storage di luar origin add-in.
HTTPS Entry URL harus menggunakan https://. Basis Cloud menolak origin http:// sepenuhnya.

Lihat Juga