# Authentication (/docs/authentication) API `/v1` memakai **token organisasi**: string berawalan `nyaur_oat_`. Satu token milik satu organisasi dan membawa scope organisasi itu di setiap panggilan. ## Membuat token [#membuat-token] Buat token di dashboard, **Pengaturan, API Tokens**, atau lewat API (memakai token yang sudah ada): ```bash curl -X POST https://api.nyaur.id/v1/tokens \ -H "Authorization: Bearer $NYAUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "backend produksi", "mode": "live" }' ``` `mode` bernilai `sandbox` (default) atau `live`. Secret lengkap dikembalikan **sekali** di respons create. Nyaur hanya menyimpan hash SHA-256 dan prefix tampilan, jadi token yang hilang harus diganti. Daftar token: `GET /v1/tokens`. Cabut token: `POST /v1/tokens/{id}/revoke`. ## Memakai token [#memakai-token] Kirim sebagai Bearer credential: ```bash curl https://api.nyaur.id/v1/products \ -H "Authorization: Bearer $NYAUR_TOKEN" ``` Simpan token di server. Token di bundle browser atau aplikasi mobile dianggap bocor. ## Mode token [#mode-token] Setiap token punya mode **sandbox** atau **live**. Mode dipilih saat token dibuat, bukan per request. Lihat [Sandbox vs live](/docs/sandbox-live). ## Batas laju [#batas-laju] 300 permintaan per menit per organisasi. Respons memuat `X-RateLimit-Limit` dan `X-RateLimit-Remaining`. Melewati batas mengembalikan `429` dengan `Retry-After` (detik). Tunggu lalu ulangi. ## Format error [#format-error] ```json { "error": { "type": "invalid_request", "message": "Query limit/offset tidak valid" } } ``` Nilai `type` yang umum: `unauthorized`, `invalid_request`, `not_found`, `org_not_active`, `org_not_reviewed`, `rate_limited`. # Checkout links dan sessions (/docs/checkout) Ada dua cara menagih pembeli: **checkout link** (halaman hosted yang dibagikan) dan **checkout session** (dibuat lewat API). ## Checkout link [#checkout-link] Checkout link adalah URL tetap untuk satu produk dan satu harga: `https://nyaur.id/checkout/`. Cocok untuk README, landing page, atau DM. Link dibuat dan dikelola dari dashboard, **Checkout links**. | Opsi | Keterangan | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `productId`, `priceId` | Wajib. Harga harus milik produk itu. | | `slug` | Opsional, huruf kecil, harus unik. Bila kosong, dibuat dari nama produk. Slug yang sudah dipakai ditolak (`CONFLICT`). | | `discountId` | Opsional, terapkan diskon. | | `allowPromoCodes` | Izinkan pembeli memasukkan kode promo. | Halaman checkout menampilkan baris PPN dan keterangan bahwa pembayaran diproses oleh PT Joyo Digitama Solusi. Pembeli memilih **QRIS** atau **virtual account** (BCA, BNI, BRI). ## Checkout session lewat API [#checkout-session-lewat-api] `POST /v1/checkouts` ```bash curl -X POST https://api.nyaur.id/v1/checkouts \ -H "Authorization: Bearer $NYAUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "productId": "prd_xxx", "priceId": "prc_xxx", "customerEmail": "pembeli@example.com", "customerName": "Dewi", "locale": "id", "metadata": { "userId": "u_123" } }' ``` | Field | Keterangan | | --------------- | --------------------------------------------------------------- | | `productId` | Wajib. | | `priceId` | Opsional. Tanpa ini dipakai harga default yang aktif. | | `customerEmail` | Opsional. Customer dibuat atau dipakai ulang berdasarkan email. | | `locale` | `id` atau `en`. Default mengikuti organisasi. | Respons `201` memuat `id`, `status`, `mode`, `subtotal`, `ppn`, `total`, dan `expiresAt`. * Sesi berlaku **60 menit**. Setelah itu `GET /v1/checkouts/{id}` melaporkan `expired`. * `status`: `open`, `paid`, `expired`, `failed`, `cancelled`. * `total` = `subtotal` + PPN. Contoh: subtotal Rp100.000 menjadi total Rp111.000. * `mode` mengikuti token. Lihat [Sandbox vs live](/docs/sandbox-live). Jangan polling status. Dengarkan webhook `checkout.paid` dan `order.paid`, lalu ambil order lewat `GET /v1/orders/{id}`. ## Event [#event] | Event | Kapan | | ------------------ | ---------------------------------- | | `checkout.created` | Sesi dibuat. | | `checkout.paid` | Pembeli lunas. | | `checkout.expired` | Sesi kedaluwarsa tanpa pembayaran. | | `order.paid` | Order dan invoice sudah ada. | ## Orders dan customers [#orders-dan-customers] * `GET /v1/orders` dan `GET /v1/orders/{id}`: order sesuai mode token. Field penting: `status` (`pending`, `paid`, `refunded`, `partially_refunded`, `disputed`), `subtotal`, `ppnAmount`, `total`, `platformFee`, `creatorCredit`, `invoiceNumber`, `refundedAmount`. * `GET /v1/customers`, `POST /v1/customers`, `GET /v1/customers/{id}`: `email` wajib, `name`, `externalId`, dan `metadata` opsional. # Fees and payouts (/docs/fees) Nyaur runs **two fee books**. Indonesia is priced near local rails; International is priced like a global MoR because the job is "no PT, no tax paperwork, no local rails." All numbers below come from the platform fee engine (`@nyaur/api/money`), the same code that computes every order. ## Indonesia [#indonesia] | Plan | Monthly | QRIS | Virtual account (BCA/BNI/BRI) | | ------- | -------- | ---------------- | ----------------------------- | | Starter | Rp0 | 1.4% (min Rp500) | Rp1.500 flat per charge | | Pro | Rp99.000 | 1.2% (min Rp500) | Rp1.000 flat per charge | * Biaya dihitung dari **harga produk sebelum pajak**. * MDR QRIS ditanggung penjual. Pembeli tidak pernah dikenai biaya tambahan QRIS. * VA: biaya platform adalah nominal flat di atas, bukan persentase. Biaya rail VA ditanggung Joyo. ## International [#international] Untuk menjual ke Indonesia tanpa entitas Indonesia. Payout USD hadir di v1.1; harga selalu dalam IDR. | Plan | Monthly | Per successful charge | | ------- | ------- | --------------------- | | Starter | USD 0 | 3.9% + Rp8.000 | | Pro | USD 20 | 3.4% + Rp6.500 | ## PPN (VAT) [#ppn-vat] Prices are exclusive of PPN. PPN (statutory rate, currently 11%) is added at checkout and remitted by PT Joyo Digitama Solusi as the PKP, it never touches your balance. Invoices and e-Faktur are issued in Joyo's name. ## Worked examples [#worked-examples] **Indonesia Starter, QRIS, Rp100.000 product** | Line | Amount | | ------------------- | --------- | | Product price | Rp100.000 | | PPN 11% | Rp11.000 | | Buyer pays | Rp111.000 | | Platform fee (1.4%) | Rp1.400 | | Your credit | Rp98.600 | **Indonesia Starter, VA, Rp100.000 product**, fee is the flat Rp1.500, your credit Rp98.500, buyer pays Rp111.000. **International Starter, Rp100.000 product**, fee is 3.9% + Rp8.000 = Rp11.900, your credit Rp88.100. ## Availability and payouts [#availability-and-payouts] * After a payment settles, funds are **held for 7 days** before becoming available. * Minimum payout: **Rp100.000** untuk rekening Indonesia (USD 50 untuk internasional, v1.1). * Setiap payout disetujui lewat maker-checker (penyetuju bukan pengaju). Lihat [Payouts dan KYC](/docs/payouts). * Refunds and chargebacks debit your balance; a negative balance blocks payouts until settled. ## Refunds and disputes [#refunds-and-disputes] * Kamu bisa me-refund order dari dashboard. **Biaya platform tidak dikembalikan.** Lihat [Refunds](/docs/refunds). * A chargeback costs **Rp240.000 plus the disputed GMV**. * Continuous review applies: high refund or chargeback ratios trigger additional holds. # Introduction (/docs) Nyaur dioperasikan oleh **PT Joyo Digitama Solusi** (Joyo) sebagai **Merchant of Record** untuk setiap transaksi. Kamu mengintegrasikan Nyaur. Pembeli membayar ke Joyo lewat QRIS atau virtual account. Joyo menerbitkan invoice, menambahkan dan menyetor PPN, menanggung refund dan chargeback, lalu membayar kamu setelah verifikasi penerima. Kamu tidak perlu mengurus DOKU, tidak perlu mendaftar sebagai penyelenggara pembayaran, dan tidak perlu mendirikan PT. ## Alur uang [#alur-uang] ``` pembeli --QRIS / VA--> PT Joyo Digitama Solusi (merchant of record) | | kredit ledger (dikurangi biaya platform) v saldo kamu | | payout setelah KYC + hold 7 hari v rekening bank Indonesia ``` * Harga belum termasuk PPN. PPN ditambahkan saat checkout dan disetor Joyo sebagai PKP. * Biaya platform dihitung dari **harga sebelum pajak**. Lihat [Fees](/docs/fees). * Setiap pergerakan ledger bersifat append-only. Saldo dihitung dari ledger, tidak pernah diubah langsung. * Semua nominal adalah **rupiah bulat** (integer). Tidak ada desimal. ## Objek utama [#objek-utama] | Objek | Fungsi | | ---------------- | ------------------------------------------------------------------------------------------------- | | Product | Barang yang kamu jual. Memuat satu atau lebih Price. | | Price | `fixed`, `pwyw`, atau `free`; `one_time` atau `day`/`week`/`month`/`year` dengan `intervalCount`. | | Benefit | `license`, `file`, atau `custom` (webhook). Diberikan otomatis saat order lunas. | | Checkout link | URL checkout hosted yang bisa dibagikan, per produk dan harga. | | Checkout session | Satu percobaan bayar dengan subtotal, PPN, dan total tetap. | | Order | Transaksi lunas, dengan invoice dan rincian biaya. | | Subscription | Tagihan berulang dengan dunning lewat invoice hosted. | | Webhook | Endpoint Standard Webhooks yang menerima event platform. | ## Mulai dari sini [#mulai-dari-sini] 1. [Quickstart](/docs/quickstart): dari daftar sampai checkout sandbox pertama. 2. [Authentication](/docs/authentication) dan [Sandbox vs live](/docs/sandbox-live). 3. [Products dan prices](/docs/products), [Checkout](/docs/checkout), [Subscriptions](/docs/subscriptions). 4. [Webhooks](/docs/webhooks), [Refunds](/docs/refunds), [Payouts dan KYC](/docs/payouts). 5. [SDK TypeScript](/docs/sdk). Dokumentasi untuk mesin: [llms.txt](/llms.txt) dan [llms-full.txt](/llms-full.txt). # Payouts dan KYC (/docs/payouts) Nyaur menyimpan uang kamu di ledger append-only. **Saldo tidak disimpan**, melainkan dihitung dari ledger. ## Status saldo [#status-saldo] | Bucket | Arti | | ----------- | ---------------------------------------------- | | `pending` | Pembayaran sudah lunas, masih dalam masa hold. | | `available` | Hold selesai, bisa ditarik. | | `paid` | Sudah dibayarkan ke bank. | Setiap pembayaran **ditahan 7 hari** sebelum menjadi `available`. ## Alur menuju payout pertama [#alur-menuju-payout-pertama] 1. **KYC.** Kirim data penerima (`id_person`, `id_pt`, atau `foreign`), nama legal, dan metadata dokumen. Tim Nyaur memutuskan: disetujui atau ditolak. Event `organization.reviewed`. 2. **Rekening bank.** Tambahkan rekening (`id_bank`, `id_ewallet`, `foreign_bank`). Nama pemilik rekening harus **sama dengan nama legal di KYC**, tanpa memandang huruf besar kecil atau spasi. Tim Nyaur memverifikasi pencocokan itu. 3. **Ajukan payout.** Jumlah minimal **Rp100.000** untuk rekening Indonesia, dan tidak boleh melebihi saldo `available`. (Minimum internasional USD 50, mulai v1.1.) 4. **Maker-checker.** Setiap payout disetujui oleh orang lain, bukan pengaju. Setelah disetujui dan dikirim ke bank, status menjadi `paid`. Status payout: `requested`, `approved`, `processing`, `paid`, `rejected`, `failed`. Event: `payout.created`, `payout.paid`. Hanya peran `owner`, `admin`, dan `finance` yang boleh mengajukan KYC, menambah rekening, dan menarik dana. ## Syarat lain [#syarat-lain] * Refund dan chargeback mendebit saldo. Saldo negatif menahan payout sampai tertutup. * Pembeli tetap bisa membayar selama KYC berjalan. * Dokumen mentah tidak disimpan di Nyaur, hanya metadata. Biaya dan contoh hitungan ada di [Fees](/docs/fees). # Products dan prices (/docs/products) Produk adalah barang yang kamu jual. Satu produk punya satu atau lebih **price**. Harga sekali bayar dan langganan memakai objek yang sama. ## Membuat produk [#membuat-produk] `POST /v1/products` ```bash curl -X POST https://api.nyaur.id/v1/products \ -H "Authorization: Bearer $NYAUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Kelas Copywriting", "description": "Akses seumur hidup", "status": "active", "prices": [ { "type": "fixed", "amount": 250000, "recurring": "one_time" } ] }' ``` | Field | Keterangan | | ------------- | --------------------------------------------- | | `name` | 1 sampai 120 karakter. Wajib. | | `description` | Maksimal 2000 karakter. | | `imageUrl` | URL gambar. | | `status` | `draft` (default), `active`, atau `archived`. | | `prices` | Maksimal 10 harga. | Produk `archived` tidak bisa dibuat checkout. `DELETE /v1/products/{id}` mengarsipkan produk, tidak menghapusnya. Endpoint lain: `GET /v1/products` (dengan `limit` 1 sampai 100 dan `offset`), `GET /v1/products/{id}`, `PATCH /v1/products/{id}`. Update hanya mengubah `name`, `description`, `imageUrl`, dan `status`. ## Tipe harga [#tipe-harga] | `type` | Aturan | | ------- | ---------------------------------------------------------------------------------- | | `fixed` | `amount` wajib lebih dari 0. | | `pwyw` | Bayar sesukanya. `minAmount` wajib. Checkout memakai `minAmount` sebagai subtotal. | | `free` | `amount` harus 0. | Tipe `metered` dan `seat` dicadangkan dan belum bisa dijual lewat API. Semua `amount` adalah rupiah bulat, maksimal 2.000.000.000. Harga **belum** termasuk PPN. ## Interval berulang [#interval-berulang] | Field | Nilai | | --------------- | -------------------------------------------------------------- | | `recurring` | `one_time` (default), `day`, `week`, `month`, `year` | | `intervalCount` | 1 sampai 1000. Contoh `month` dengan `3` berarti tiap 3 bulan. | | `isDefault` | Harga default saat checkout tidak menyebut `priceId`. | | `active` | Harga nonaktif tidak bisa dipakai checkout. | Harga berulang membuat [subscription](/docs/subscriptions) setelah pembayaran pertama. ## Benefit [#benefit] Benefit diberikan otomatis saat order lunas dan memicu event `benefit.granted`. Benefit dikelola dari dashboard pada halaman produk: * `license`: license key otomatis. Opsi `prefix` (maksimal 12 karakter) dan `activationLimit` (1 sampai 1000). * `file`: unduhan dari file yang kamu unggah. * `custom`: memanggil `webhookUrl` milikmu. # Quickstart (/docs/quickstart) Panduan ini membawa kamu dari akun kosong sampai pembayaran sandbox pertama. Mode sandbox memakai driver DOKU mock: payload QRIS dan nomor virtual account disimulasikan, dan aksi "Simulasikan pembayaran" di halaman checkout melunasi transaksi tanpa uang sungguhan. ## 1. Daftar dan buat organisasi [#1-daftar-dan-buat-organisasi] Daftar di [nyaur.id](https://nyaur.id), lalu buat organisasi. Tipe organisasi menentukan buku biaya: * **Indonesia** untuk developer dengan KTP atau badan usaha Indonesia. * **International** untuk penjual tanpa entitas Indonesia yang menjual ke Indonesia. ## 2. Buat token API sandbox [#2-buat-token-api-sandbox] Buka **Pengaturan, API Tokens** di dashboard dan buat token dengan mode `sandbox`. Token berawalan `nyaur_oat_` dan hanya ditampilkan sekali. Lihat [Authentication](/docs/authentication). ## 3. Buat produk [#3-buat-produk] Semua yang kamu jual adalah produk. Harga dikirim sebagai array `prices`. ```bash curl -X POST https://api.nyaur.id/v1/products \ -H "Authorization: Bearer $NYAUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "API Credits", "description": "10.000 API credits per bulan", "status": "active", "prices": [ { "type": "fixed", "amount": 79000, "recurring": "month", "intervalCount": 1 } ] }' ``` Respons memuat `id` produk dan `prices[].id`. Detail lengkap ada di [Products dan prices](/docs/products). ## 4. Buat checkout [#4-buat-checkout] ```bash curl -X POST https://api.nyaur.id/v1/checkouts \ -H "Authorization: Bearer $NYAUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "productId": "prd_xxx", "customerEmail": "pembeli@example.com", "locale": "id" }' ``` Respons berisi `subtotal`, `ppn`, dan `total` dalam rupiah, serta `expiresAt` (60 menit). Untuk halaman bayar yang bisa dibagikan, buat **checkout link** di dashboard, lalu buka `https://nyaur.id/checkout/`. Pembeli memilih QRIS atau virtual account (BCA, BNI, BRI). Di sandbox, tekan "Simulasikan pembayaran". Lihat [Checkout](/docs/checkout). ## 5. Dengarkan webhook [#5-dengarkan-webhook] ```bash curl -X POST https://api.nyaur.id/v1/webhooks \ -H "Authorization: Bearer $NYAUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://contoh.id/webhooks/nyaur", "events": ["checkout.paid", "order.paid", "benefit.granted"] }' ``` Signing secret `nyaur_whsec_...` hanya muncul sekali. Verifikasi tanda tangan seperti di [Webhooks](/docs/webhooks) sebelum memercayai payload. Checkout yang lunas memicu, berurutan: 1. `checkout.paid`: sesi lunas. 2. `order.paid`: order dan invoice sudah ada, lengkap dengan rincian biaya. 3. `benefit.granted`: satu per benefit, misalnya license key pembeli. ## Selanjutnya [#selanjutnya] * Dana masuk ke saldo **pending** selama [7 hari](/docs/payouts) setelah pembayaran, lalu menjadi tersedia. * Sebelum payout pertama, selesaikan KYC dan tambahkan rekening bank atas nama yang sama. Pembeli tetap bisa membayar selama verifikasi berjalan. * Checkout **live** baru terbuka setelah review produk (Gate 1) lulus. Lihat [Sandbox vs live](/docs/sandbox-live). ## Error [#error] Error `/v1` selalu berbentuk `{"error":{"type","message"}}`. Batas laju 300 permintaan per menit per organisasi; saat `429`, tunggu sesuai header `Retry-After`. # Refunds (/docs/refunds) Refund dilakukan dari dashboard, **Orders**, oleh peran `owner`, `admin`, atau `finance`. ## Penuh atau sebagian [#penuh-atau-sebagian] * Sebutkan `amount` untuk refund sebagian (rupiah bulat, lebih dari 0). * Tanpa `amount`, seluruh sisa yang belum direfund dikembalikan. * `reason` opsional, maksimal 500 karakter. * `key` opsional (8 sampai 100 karakter) sebagai kunci idempotensi. Request ulang dengan `key` yang sama mengembalikan hasil pertama, bukan refund kedua. Bila request pertama masih berjalan, hasilnya `CONFLICT`. Order berubah menjadi `partially_refunded` atau `refunded` dan event `order.refunded` terkirim. ## Dampak ke uang [#dampak-ke-uang] * Pembeli menerima kembali total, termasuk bagian PPN. * Bagian PPN dibalik di ledger. * Kamu didebit sebesar kreditmu atas bagian yang direfund. * **Biaya platform tidak dikembalikan.** * Saldo yang negatif menahan payout sampai tertutup. Lihat [Payouts dan KYC](/docs/payouts). ## Chargeback [#chargeback] Sengketa chargeback dikenai **Rp240.000** ditambah nilai transaksi yang disengketakan. Order berstatus `disputed`. Rasio refund atau chargeback yang tinggi dapat memicu penahanan tambahan. ## Memantau [#memantau] Pantau lewat webhook `order.refunded` dan baca `refundedAmount` di `GET /v1/orders/{id}`. # Sandbox vs live (/docs/sandbox-live) Setiap token API punya mode, **sandbox** atau **live**. Mode dipilih saat token dibuat dan tidak bisa diganti per request. | | Sandbox | Live | | ----------------- | -------------------------------------- | ---------------------------- | | Uang | Simulasi | Sungguhan | | Pembayaran | Driver DOKU mock, QRIS dan VA simulasi | QRIS dan VA (BCA, BNI, BRI) | | Melunasi checkout | Tombol "Simulasikan pembayaran" | Pembeli membayar | | Syarat | Selalu terbuka | Review produk (Gate 1) lulus | | Token | `mode: "sandbox"` | `mode: "live"` | ## Apa yang dipisah per mode [#apa-yang-dipisah-per-mode] Checkout session dan order diberi cap `mode` dari token yang membuatnya, dan pembacaan difilter berdasarkan mode token. Jadi token sandbox tidak melihat order live, dan sebaliknya. Produk, harga, customer, dan endpoint webhook milik organisasi dan dipakai bersama oleh kedua mode. ## Membuka checkout live [#membuka-checkout-live] Membuat checkout dengan token `live` mengembalikan `409 org_not_reviewed` sampai review produk (Gate 1) lulus. Organisasi yang ditangguhkan mengembalikan `409 org_not_active`. Sandbox tidak terpengaruh, jadi kamu bisa membangun seluruh alur lebih dulu. Urutan menuju live: 1. Bangun dan uji di sandbox. 2. Ajukan review produk dari dashboard. 3. Setelah lulus, buat token `live` dan ganti token di backend kamu. 4. Sebelum payout pertama: KYC dan rekening bank. Lihat [Payouts dan KYC](/docs/payouts). ## Webhook [#webhook] Event dikirim dengan format yang sama di kedua mode. Gunakan endpoint dan secret terpisah bila kamu ingin memisahkan lalu lintas uji dari produksi. # SDK TypeScript (/docs/sdk) `@nyaur/sdk` adalah client TypeScript untuk API `/v1`. Tanpa dependency runtime, berjalan di Node 18+, Bun, Deno, Cloudflare Workers, dan browser (tetap simpan token di server). ## Instalasi [#instalasi] ```bash bun add @nyaur/sdk # atau npm install @nyaur/sdk ``` ## Quickstart [#quickstart] ```ts import { createClient, NyaurApiError } from "@nyaur/sdk"; const nyaur = createClient({ baseUrl: "https://api.nyaur.id", token: process.env.NYAUR_TOKEN!, }); const product = await nyaur.products.create({ name: "Kelas Copywriting", status: "active", prices: [{ type: "fixed", amount: 250_000, recurring: "one_time" }], }); const checkout = await nyaur.checkouts.create({ productId: product.id, customerEmail: "pembeli@example.com", locale: "id", }); console.log(checkout.total); // rupiah bulat, sudah termasuk PPN try { await nyaur.orders.get("ord_tidak_ada"); } catch (error) { if (error instanceof NyaurApiError) { console.error(error.status, error.type, error.message); } } ``` ## Resource [#resource] | Resource | Method | | ------------ | -------------------------------------------------------------------- | | `products` | `list`, `create`, `get`, `update`, `remove` (mengarsipkan) | | `checkouts` | `create`, `get` | | `orders` | `list`, `get` | | `customers` | `list`, `create`, `get` | | `webhooks` | `list`, `create`, `get`, `update`, `remove`, `deliveries`, `process` | | `events` | `list`, `replay` | | `deliveries` | `replay` | | `tokens` | `create`, `list`, `revoke` | Method `list` menerima `{ limit, offset }` dan mengembalikan `{ data, meta }`. Method lain mengembalikan objeknya langsung. ## Webhook [#webhook] ```ts const endpoint = await nyaur.webhooks.create({ url: "https://contoh.id/webhooks/nyaur", events: ["checkout.paid", "order.paid"], }); console.log(endpoint.secret); // nyaur_whsec_..., hanya muncul sekali ``` SDK tidak memverifikasi tanda tangan. Pakai contoh di [Webhooks](/docs/webhooks). ## Error [#error] Respons non-2xx melempar `NyaurApiError` dengan `status`, `type`, dan `message`. Pada `429`, tunggu sesuai header `Retry-After`. Batas: 300 permintaan per menit per organisasi. ## Mode [#mode] Mode (`sandbox` atau `live`) mengikuti token. Lihat [Sandbox vs live](/docs/sandbox-live). ## Mock fetch [#mock-fetch] Opsi `fetch` memungkinkan menyuntikkan implementasi sendiri, misalnya untuk tes: ```ts const nyaur = createClient({ baseUrl: "https://api.nyaur.id", token: "nyaur_oat_test", fetch: async () => new Response(JSON.stringify({ data: { id: "prd_1" } }), { status: 200 }), }); ``` # Subscriptions dan dunning (/docs/subscriptions) QRIS dan virtual account tidak bisa didebit otomatis. Karena itu Nyaur **tidak menarik uang dari pembeli secara otomatis**. Setiap periode, Nyaur membuat **invoice hosted** yang dibayar pembeli lewat QRIS atau VA, lalu menjalankan dunning bila belum dibayar. ## Membuat langganan [#membuat-langganan] Langganan lahir dari pembayaran pertama sebuah harga berulang (`recurring` bukan `one_time`, lihat [Products dan prices](/docs/products)). Setelah pembayaran lunas, event `subscription.created` terkirim. ## Siklus periode [#siklus-periode] 1. Saat periode berakhir, Nyaur membuka order dan invoice baru untuk periode berikutnya, dengan `dueAt` **3 hari** setelah invoice dibuat. 2. Pembeli membayar invoice lewat QRIS atau VA. Setelah lunas: `subscription.renewed`, `order.paid`, dan langganan berlanjut. 3. Bila lewat jatuh tempo, dunning berjalan. Panjang periode mengikuti `recurring` dan `intervalCount`. Tanggal akhir bulan dijepit ke hari terakhir bulan itu (31 Januari berlanjut ke 28 atau 29 Februari). ## Dunning [#dunning] | Percobaan | Terjadi pada | Status langganan | | --------- | -------------------- | ----------------------------------------------- | | 1 | Jatuh tempo | `past_due` dan event `subscription.past_due` | | 2 | Jatuh tempo + 1 hari | `unpaid` dan event `subscription.past_due` lagi | | 3 | Jatuh tempo + 2 hari | Dibatalkan, invoice `uncollectible` | Nyaur menjalankan tick dunning dari cron tiap 5 menit. Tick yang berulang pada jendela yang sama tidak menghabiskan percobaan. Pembayaran invoice kapan pun sebelum percobaan ketiga mengembalikan langganan ke `active` dan mereset hitungan. Payload `subscription.past_due` memuat `attempt`, `invoiceId`, `dueAt`, dan `status`. Dengarkan event ini untuk mengirim pengingat atau membatasi akses sementara. ## Membatalkan [#membatalkan] Pembatalan dilakukan dari dashboard: * Sebelum periode berakhir: langganan hanya diberi tanda `cancelAtPeriodEnd`. Pembeli tetap punya akses sampai akhir periode yang sudah dibayar. Tanda ini bisa dicabut (resume). * Setelah periode lewat: pembatalan langsung. Event `subscription.canceled`. Pembeli juga bisa membatalkan dari portal pelanggan di [nyaur.id/portal](https://nyaur.id/portal). ## Event [#event] `subscription.created`, `subscription.renewed`, `subscription.past_due`, `subscription.canceled`. Lihat [Webhooks](/docs/webhooks). # Webhooks (/docs/webhooks) Nyaur delivers platform events to your HTTPS endpoints using the [Standard Webhooks](https://www.standardwebhooks.com) convention. Every delivery is signed with your endpoint's secret (`nyaur_whsec_…`, issued once at creation) so you can verify it came from us and arrived unmodified. ## Events [#events] | Event | Fired when | | ----------------------- | ------------------------------------------------- | | `checkout.created` | A checkout session was opened. | | `checkout.paid` | The buyer completed payment. | | `checkout.expired` | The session expired unpaid. | | `order.paid` | The order settled; ledger rows and invoice exist. | | `order.refunded` | A refund completed (partial or full). | | `subscription.created` | A subscription started. | | `subscription.renewed` | A renewal invoice was paid. | | `subscription.canceled` | The subscription was canceled. | | `subscription.past_due` | A renewal went unpaid after reminders (dunning). | | `benefit.granted` | A benefit was granted (e.g. license key issued). | | `payout.created` | A payout was requested. | | `payout.paid` | A payout reached the bank. | | `organization.reviewed` | Product review or org status changed. | Payload berupa JSON. Isi payload berbeda per event, misalnya `sessionId` pada `checkout.*` dan `invoiceId`, `subscriptionId` pada `subscription.*`. ## Signature verification [#signature-verification] Each delivery carries three headers: | Header | Value | | ------------------- | ---------------------------------------------- | | `webhook-id` | Unique delivery id, stable across retries. | | `webhook-timestamp` | Unix seconds (string). | | `webhook-signature` | `v1,` | The signed content is `..`. The HMAC key is the secret's `nyaur_whsec_` prefix stripped, then base64-decoded. ### Verify in TypeScript [#verify-in-typescript] ```ts import { createHmac, timingSafeEqual } from "node:crypto"; const SECRET = process.env.NYAUR_WEBHOOK_SECRET as string; const TOLERANCE_SECONDS = 300; export function verifyWebhook(headers: Headers, rawBody: string): boolean { const id = headers.get("webhook-id") ?? ""; const timestamp = headers.get("webhook-timestamp") ?? ""; const signature = headers.get("webhook-signature") ?? ""; // 1. Freshness, reject replays older than 5 minutes. const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false; // 2. Signature, constant-time comparison over the signed content. const key = Buffer.from(SECRET.replace("nyaur_whsec_", ""), "base64"); const signedContent = `${id}.${timestamp}.${rawBody}`; const expected = createHmac("sha256", key) .update(signedContent) .digest("base64"); const received = signature.replace(/^v1,/, ""); const a = Buffer.from(expected); const b = Buffer.from(received); return a.length === b.length && timingSafeEqual(a, b); } ``` Verify against the **raw** request body, not a re-serialized object, key order and whitespace are part of the signature. ## Retries and idempotency [#retries-and-idempotency] * Setiap delivery dicoba sampai **6 kali**. Jeda setelah percobaan 1 sampai 5 adalah 5, 25, 125, 625, dan 3125 detik. Respons `2xx` menghentikan retry. * `webhook-id` stabil di antara retry. Simpan id yang sudah diproses sebentar dan lewati duplikat. * Tolak timestamp yang selisihnya lebih dari 300 detik dari jam kamu. * Replay lewat API: `POST /v1/events/{id}/replay` mengirim ulang sebuah event ke semua endpoint aktif, `POST /v1/deliveries/{id}/replay` mengirim ulang satu delivery. Dari dashboard: **Webhooks, endpoint, Replay**. ## Mengelola endpoint [#mengelola-endpoint] | Endpoint | Fungsi | | ---------------------------------- | --------------------------------------------------------------------------------------------------------- | | `POST /v1/webhooks` | Buat endpoint. Body `url` dan `events` (1 sampai 20 nama event, atau `*`). Secret ada di respons, sekali. | | `GET /v1/webhooks` | Daftar endpoint. | | `GET /v1/webhooks/{id}` | Detail beserta delivery terbaru. | | `PATCH /v1/webhooks/{id}` | Ubah `url`, `events`, atau `active`. | | `DELETE /v1/webhooks/{id}` | Hapus endpoint. | | `GET /v1/webhooks/{id}/deliveries` | Riwayat delivery. | | `GET /v1/events` | Log event organisasi. | ## Best practices [#best-practices] * Acknowledge fast: validate, enqueue, return `200`. Do slow work after responding. * Never trust amounts from payloads for security decisions, re-fetch the order over the API. * Subscribe to `subscription.past_due` to win back failed renewals; QRIS dan VA tidak bisa auto-debit, jadi dunning berjalan lewat invoice hosted. Lihat [Subscriptions](/docs/subscriptions).