# Dokumentasi Nomor OTP — Skirk Gateway & otp

Base URL: https://skirkdonate.my.id/api/v1

## API Nomor OTP: pesan & ambil daftar

Pesan nomor sekali pakai untuk menerima SMS verifikasi (WhatsApp, Telegram, dll.) dan ambil daftar negara, layanan, operator, serta **semua pilihan harga (termurah dulu)**. Memakai API key dan saldo yang sama dengan QRIS.

### Alur singkat

1. Ambil `country_id` dan `service_id` dari `/otp/countries` dan `/otp/services`.
2. `GET /otp/products` untuk melihat semua harga, lalu pilih satu baris (yang termurah ada di atas).
3. `POST /otp/orders` dengan data baris itu, `price`-nya, dan `ref` milikmu.
4. Tunggu kode: terima webhook `otp.received`, atau ulangi `GET /otp/orders/{id}` tiap 3-5 detik.
5. Panggil `finish` setelah kode dipakai, atau `cancel` bila tidak ada SMS (saldo kembali).

### Endpoint

| Endpoint | Fungsi |
| --- | --- |
| `GET /otp/countries` | Daftar negara (`id`, `name`, `emoji`, `dial_code`). |
| `GET /otp/services?country_id=` | Daftar layanan/aplikasi untuk negara itu. |
| `GET /otp/operators?country_id=&service_id=` | Operator yang punya stok (`operator_id` null = acak/any). |
| `GET /otp/products?country_id=&service_id=` | **Semua opsi harga**, termurah ke termahal, beserta stok. Opsional: `operator_id=`. |
| `POST /otp/orders` | Pesan satu nomor di harga yang dipilih. |
| `GET /otp/orders` | Daftar pesanan akunmu, terbaru dulu. Opsional: `status=`, `limit=` (maks. 100), `offset=`. |
| `GET /otp/orders/{id}` | Status satu pesanan; kode OTP muncul di sini. |
| `POST /otp/orders/{id}/cancel` | Batalkan sebelum ada SMS; saldo kembali. |
| `POST /otp/orders/{id}/finish` | Tandai selesai setelah kode dipakai. |

**Respons GET /otp/products: semua harga, termurah dulu**

```json
{
  "status": "success",
  "data": [
    {"catalog_product_id": 88, "operator_id": 42,   "operator": "Telkomsel",  "available": 142, "price": 2945},
    {"catalog_product_id": 88, "operator_id": null, "operator": "Acak (Any)", "available": 30,  "price": 3300},
    {"catalog_product_id": 88, "operator_id": 43,   "operator": "Indosat",    "available": 12,  "price": 3600}
  ]
}
```

### Memesan nomor

| Field | Tipe | Keterangan |
| --- | --- | --- |
| country_id | integer | Wajib. Dari `/otp/countries`. |
| service_id | integer | Wajib. Dari `/otp/services`. |
| catalog_product_id | integer | Wajib. Dari baris yang dipilih di `/otp/products`. |
| operator_id | integer / null | Dari baris yang sama (null untuk acak/any). |
| price | integer | Wajib. Harga baris itu (Rupiah). Harus **sama persis** dengan harga di daftar. |
| ref | string | Opsional tetapi **disarankan**, 1-64 karakter (huruf, angka, `_ . : -`). Ref sama = pesanan sama, jadi request yang timeout aman diulang tanpa memesan dua kali. |

**Request POST /otp/orders**

```json
{
  "country_id": 7,
  "service_id": 1,
  "catalog_product_id": 88,
  "operator_id": 42,
  "price": 2945,
  "ref": "ORD-1001"
}
```

> **Catatan:** Harga dicek ulang di server saat memesan. Bila harga atau stok berubah, pesanan ditolak dengan `price_changed`: ambil `/otp/products` lagi lalu pilih ulang. Saldo ditahan sebesar `price`, dan kembali otomatis bila pesanan batal, kedaluwarsa, atau gagal.

### Objek pesanan

**Respons 201**

```json
{
  "status": "success",
  "data": {
    "id": 90210,
    "ref": "ORD-1001",
    "status": "waiting_sms",
    "phone": "+6281234567890",
    "service": "WhatsApp",
    "country": "Indonesia",
    "operator": "Telkomsel",
    "price": 2945,
    "otp_code": null,
    "otp_message": null,
    "sms_revision": 0,
    "created_at": "2026-10-09T10:00:00+07:00",
    "expires_at": "2026-10-09T10:20:00+07:00",
    "expires_in": 1200,
    "can_cancel": true,
    "can_finish": false
  }
}
```

| Field | Arti |
| --- | --- |
| id / ref | ID pesanan (dipakai di endpoint lain) dan referensi yang kamu kirim. |
| status | Lihat tabel status di bawah. |
| phone | Nomor yang dipesan; masukkan di aplikasi tujuan. |
| otp_code / otp_message | Kode terdeteksi dan teks SMS lengkap. Kode bisa `null` sementara `otp_message` terisi (mis. SMS berisi tautan). |
| sms_revision | Naik setiap ada SMS baru. Abaikan data yang `sms_revision`-nya lebih kecil dari yang sudah kamu simpan. |
| price | Harga yang ditagihkan (Rupiah). |
| expires_at / expires_in | Batas waktu sewa (ISO 8601) dan sisa detiknya. |
| can_cancel / can_finish | Aksi yang diizinkan saat ini. |

### Status pesanan

| status | Arti | Langkah berikutnya |
| --- | --- | --- |
| creating | Pesanan sedang dibuat. | Cek lagi sebentar. |
| waiting_sms | Nomor siap, menunggu SMS. | Kirim kode ke nomor itu. Bisa dibatalkan setelah jeda singkat (biasanya sekitar 2 menit). |
| otp_received | SMS masuk. | Ambil `otp_code` / `otp_message`, lalu panggil `finish`. Tidak bisa dibatalkan lagi. |
| completed | Selesai. | Saldo terpotong. |
| canceled, expired, failed | Dibatalkan, kedaluwarsa tanpa SMS, atau nomor tidak terbentuk. | Saldo kembali (atau tidak pernah terpotong). |
| pending_admin | Hasil pemesanan belum pasti (mis. koneksi terputus). Dibalas **202**. | Saldo ditahan. Cek `GET /otp/orders/{id}`; bila lama, hubungi admin dengan menyebut `id`. |

**Contoh alur lengkap: cari harga termurah, pesan, tunggu kode, selesai (cURL)**

```bash
# 1) Lihat semua harga (termurah di atas). country_id & service_id dari /otp/countries dan /otp/services
curl "https://skirkdonate.my.id/api/v1/otp/products?country_id=7&service_id=1" -H "Authorization: Bearer $SKIRK_KEY"

# 2) Pesan nomor di harga yang dipilih (price harus sama persis dengan hasil langkah 1)
curl -X POST https://skirkdonate.my.id/api/v1/otp/orders -H "Authorization: Bearer $SKIRK_KEY" -H "Content-Type: application/json" \
  -d '{"country_id":7,"service_id":1,"catalog_product_id":88,"operator_id":42,"price":2945,"ref":"ORD-1001"}'

# 3) Cek pesanan sampai status otp_received (ulangi tiap 3-5 detik, atau pakai webhook)
curl https://skirkdonate.my.id/api/v1/otp/orders/90210 -H "Authorization: Bearer $SKIRK_KEY"

# 4) Setelah kode dipakai: selesai. Bila tidak ada SMS: batalkan (saldo kembali)
curl -X POST https://skirkdonate.my.id/api/v1/otp/orders/90210/finish -H "Authorization: Bearer $SKIRK_KEY"
curl -X POST https://skirkdonate.my.id/api/v1/otp/orders/90210/cancel -H "Authorization: Bearer $SKIRK_KEY"
```

**Contoh alur lengkap: cari harga termurah, pesan, tunggu kode, selesai (PHP)**

```php
<?php
const BASE = 'https://skirkdonate.my.id/api/v1';
function skirk(string $method, string $path, ?array $body = null): array {
  $ch = curl_init(BASE . $path);
  curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_TIMEOUT => 90,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SKIRK_KEY'), 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body)]);
  $res = json_decode(curl_exec($ch), true) ?: [];
  $res['_http'] = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  return $res;
}

// 1) harga termurah untuk layanan 1 di negara 7
$prices = skirk('GET', '/otp/products?country_id=7&service_id=1')['data'] ?? [];
$p = $prices[0] ?? exit("stok kosong\n");

// 2) pesan (ref = id order di sistemmu; aman diulang bila timeout)
$o = skirk('POST', '/otp/orders', ['country_id' => 7, 'service_id' => 1, 'catalog_product_id' => $p['catalog_product_id'],
        'operator_id' => $p['operator_id'], 'price' => $p['price'], 'ref' => 'ORD-1001']);
if (!in_array($o['_http'], [200, 201, 202], true)) exit('gagal: ' . ($o['message'] ?? $o['_http']) . "\n");
$id = $o['data']['id'];
echo "Nomor: {$o['data']['phone']}\n";

// 3) tunggu kode (maks. ~5 menit)
for ($i = 0; $i < 100; $i++) {
  sleep(3);
  $d = skirk('GET', "/otp/orders/$id")['data'] ?? [];
  if (($d['status'] ?? '') === 'otp_received') { echo 'Kode: ' . ($d['otp_code'] ?? $d['otp_message']) . "\n"; skirk('POST', "/otp/orders/$id/finish"); exit; }
  if (!in_array($d['status'] ?? '', ['waiting_sms', 'creating'], true)) exit("berakhir: {$d['status']}\n");
}
skirk('POST', "/otp/orders/$id/cancel");   // tidak ada SMS: batalkan, saldo kembali
```

**Contoh alur lengkap: cari harga termurah, pesan, tunggu kode, selesai (Node.js 18+)**

```js
const BASE = 'https://skirkdonate.my.id/api/v1';
const H = { Authorization: 'Bearer ' + process.env.SKIRK_KEY, 'Content-Type': 'application/json' };
const call = async (method, path, body) => {
  const r = await fetch(BASE + path, { method, headers: H, body: body ? JSON.stringify(body) : undefined });
  return { http: r.status, ...(await r.json()) };
};
const sleep = ms => new Promise(r => setTimeout(r, ms));

const p = ((await call('GET', '/otp/products?country_id=7&service_id=1')).data || [])[0];   // termurah
if (!p) throw new Error('stok kosong');

const o = await call('POST', '/otp/orders', { country_id: 7, service_id: 1, catalog_product_id: p.catalog_product_id,
  operator_id: p.operator_id, price: p.price, ref: 'ORD-1001' });
if (![200, 201, 202].includes(o.http)) throw new Error(o.message);
console.log('Nomor:', o.data.phone);

for (let i = 0; i < 100; i++) {
  await sleep(3000);
  const d = (await call('GET', '/otp/orders/' + o.data.id)).data;
  if (d.status === 'otp_received') { console.log('Kode:', d.otp_code ?? d.otp_message); await call('POST', '/otp/orders/' + d.id + '/finish'); break; }
  if (!['waiting_sms', 'creating'].includes(d.status)) { console.log('berakhir:', d.status); break; }
}
```

**Contoh alur lengkap: cari harga termurah, pesan, tunggu kode, selesai (Python)**

```python
import os, time, requests
BASE = 'https://skirkdonate.my.id/api/v1'
S = requests.Session(); S.headers['Authorization'] = 'Bearer ' + os.environ['SKIRK_KEY']

prices = S.get(BASE + '/otp/products', params={'country_id': 7, 'service_id': 1}, timeout=30).json().get('data', [])
if not prices: raise SystemExit('stok kosong')
p = prices[0]                                          # termurah

r = S.post(BASE + '/otp/orders', timeout=90, json={'country_id': 7, 'service_id': 1, 'catalog_product_id': p['catalog_product_id'],
        'operator_id': p['operator_id'], 'price': p['price'], 'ref': 'ORD-1001'})
if r.status_code not in (200, 201, 202): raise SystemExit(r.json().get('message'))
o = r.json()['data']; print('Nomor:', o['phone'])

for _ in range(100):
    time.sleep(3)
    d = S.get(f"{BASE}/otp/orders/{o['id']}", timeout=30).json()['data']
    if d['status'] == 'otp_received':
        print('Kode:', d['otp_code'] or d['otp_message']); S.post(f"{BASE}/otp/orders/{o['id']}/finish", timeout=30); break
    if d['status'] not in ('waiting_sms', 'creating'):
        print('berakhir:', d['status']); break
```

> **Catatan:** Cara paling hemat menunggu kode adalah [webhook](#webhooks) `otp.received`. Polling cukup sebagai cadangan; tetap patuhi rate limit. Pesanan lewat API juga muncul di halaman Nomor OTP dengan saldo yang sama.

## Nomor OTP lewat halaman web

Menu **Nomor OTP** menjual nomor sekali pakai untuk menerima SMS verifikasi dengan saldo Skirk-mu. Menu hanya muncul bila admin sudah mengaktifkannya.

1. Pilih **negara**, lalu **layanan/aplikasi** (ada kolom cari), lalu **operator** (atau biarkan "Acak").
2. Semua harga tampil dari yang **termurah**, lengkap dengan operator dan stok. Klik **Beli** pada harga yang kamu mau, lalu konfirmasi.
3. Masukkan nomor yang tampil di aplikasi tujuan dan minta kode verifikasi.
4. Kode OTP muncul otomatis di kartu pesanan dan bisa disalin dengan tombol **Salin**.
5. Setelah kode dipakai, klik **Selesai**. Bila tidak ada SMS, klik **Batalkan & kembalikan saldo**.

> **Catatan:** Harga yang tampil sudah final dan dicek ulang saat kamu menekan Beli. Bila harga atau stok berubah, pembelian ditolak dan kamu memilih ulang, jadi tidak pernah ditagih lebih mahal dari yang kamu lihat. Saldo ditahan saat memesan dan kembali otomatis bila pesanan batal, kedaluwarsa, atau gagal; saldo benar-benar terpakai hanya bila statusnya **Selesai**.

### Arti status

| Status | Arti | Yang bisa kamu lakukan |
| --- | --- | --- |
| Menunggu SMS | Nomor siap, menunggu SMS masuk. | Kirim kode ke nomor itu. Tombol Batalkan aktif setelah jeda singkat (sekitar 2 menit) dan selama belum ada SMS. |
| Kode masuk | SMS sudah diterima. | Salin kodenya, pakai di aplikasi, lalu klik **Selesai**. |
| Selesai | Pesanan tuntas. | Saldo tetap terpotong. |
| Dibatalkan / Kedaluwarsa / Gagal | Tidak ada SMS masuk atau nomor tidak terbentuk. | Saldo kembali otomatis. Pesan ulang bila perlu. |
| Menunggu admin | Hasil pemesanan belum pasti (mis. koneksi terputus). | Saldo ditahan, bukan hilang. Admin akan memutuskan; hubungi admin bila lebih dari beberapa menit. |

### Masalah umum

| Masalah | Solusi |
| --- | --- |
| Kode tidak kunjung masuk | Pastikan nomor dimasukkan lengkap dengan kode negara dan kode sudah diminta di aplikasi tujuan. Bila tetap tidak ada, batalkan (saldo kembali) dan coba operator atau harga lain. |
| Tombol Batalkan tidak aktif | Ada jeda minimum setelah memesan, atau SMS sudah masuk (pesanan tidak bisa dibatalkan lagi). |
| "Saldo tidak cukup" | Saldo tersedia = saldo dikurangi pesanan yang masih berjalan. Isi saldo atau selesaikan/batalkan pesanan lain. |
| "Maksimal ... pesanan berjalan" | Selesaikan atau batalkan pesanan yang sedang berjalan dulu. |
| "Harga/stok sudah berubah" atau daftar kosong | Daftar dimuat ulang otomatis; pilih lagi. Bila kosong, stok habis: coba operator "Acak" atau layanan lain. |
| SMS masuk tetapi kode kosong | Beberapa SMS berisi tautan, bukan angka. Teks SMS lengkap tetap tampil di kartu pesanan. |

Nomor bersifat sekali pakai untuk satu verifikasi; pesan lagi untuk verifikasi berikutnya. Gunakan hanya untuk akun dan keperluan yang sah.

