Developer · API
Public API v1 · Integrator
Hubungkan website atau bot ke katalog gabungan server 1–5, saldo, pembelian nomor, dan OTP menggunakan API key akun sendiri.
Unduh OpenAPI integratorLogin ke ping, buka Pengaturan akun → Developer API. Isi nama integrasi dan pilih izin yang diperlukan. Untuk alur penuh, aktifkan catalog:read, wallet:read, orders:read, dan orders:write. account:read opsional untuk profil. Nilai lengkap key hanya ditampilkan satu kali; pembuatan dan pencabutan tetap memerlukan sesi browser.
| Scope | Akses |
|---|---|
| account:read | Membaca profil akun pemilik key. |
| wallet:read | Membaca saldo, alokasi, dan saldo tersedia. |
| catalog:read | Membaca layanan, negara, dan quote gabungan. |
| orders:read | Membaca riwayat, nomor, dan SMS akun sendiri. |
| orders:write | Membeli memakai saldo akun, membatalkan, menyelesaikan, dan resend. |
Key dengan izin ini dapat membeli nomor dan melakukan aksi atas seluruh pesanan akun pemiliknya, termasuk yang dibuat dari website atau key lain. Hanya berikan ke integrasi tepercaya. Isi saldo lewat aplikasi ping!; API key tidak dapat membuat top-up, mengubah saldo langsung, mengelola key, atau mengakses admin.
- Maksimal lima key aktif per akun, berlaku 90 hari, pembuatan maksimal sepuluh kali per jam.
- Izin key lama tidak bertambah otomatis. Buat key baru untuk menambah izin, pindahkan integrasi, lalu cabut key lama.
- Simpan key hanya di secret manager atau environment backend integrator. Jangan commit, masukkan frontend, kirim sebagai query, atau mencetak Authorization/OTP ke log.
- Pencabutan, kedaluwarsa, penghapusan akun, atau penangguhan menghentikan akses request berikutnya.
Base URL produksi: https://pingotp.com/api/v1. Kirim Authorization: Bearer <API_KEY> pada setiap request dan Content-Type: application/json untuk POST. Tidak perlu login cookie di backend integrator; cookie tidak menjadi fallback. Respons memakai { ok: true, data } atau { ok: false, error: { code, message } }. Tidak ada CORS publik untuk pemanggilan key dari browser lintas origin.
| Method & path | Scope | Fungsi |
|---|---|---|
| GET /api/v1/me | account:read | Identitas akun |
| GET /api/v1/wallet | wallet:read | Saldo dompet |
| GET /api/v1/catalog/services | catalog:read | Layanan gabungan |
| GET /api/v1/catalog/countries | catalog:read | Negara gabungan |
| GET /api/v1/catalog/quotes | catalog:read | Quote gabungan |
| GET /api/v1/orders | orders:read | Riwayat pesanan |
| POST /api/v1/orders | orders:write | Beli nomor |
| GET /api/v1/orders/active | orders:read | Pesanan aktif |
| GET /api/v1/orders/{id} | orders:read | Status & OTP |
| POST /api/v1/orders/{id}/cancel | orders:write | Batalkan pesanan |
| POST /api/v1/orders/{id}/complete | orders:write | Selesaikan pesanan |
| POST /api/v1/orders/{id}/resend | orders:write | Minta SMS berikutnya |
curl --fail-with-body \
-H "Authorization: Bearer $PING_API_KEY" \
"https://pingotp.com/api/v1/wallet"Unduh OpenAPI integrator menggunakan tombol di atas atau /developers/integrator-openapi.json. Spesifikasi ini memakai Bearer; /developers/openapi.json tetap khusus API internal berbasis sesi. Semua contoh dokumentasi adalah ilustrasi dan tidak menjalankan transaksi.
- Langkah 1
Ambil layanan
GET /catalog/services menggabungkan layanan dari server 1–5 yang aktif dan dikonfigurasi tanpa duplikat. Gunakan id dari respons, bukan nama atau kode provider.
- Langkah 2
Ambil negara untuk layanan
GET /catalog/countries?serviceId=... menampilkan negara, harga mulai sesuai akun pemilik key, serta ketersediaan indikatif. Tanpa serviceId, respons hanya katalog negara aktif.
- Langkah 3
Ambil quote
GET /catalog/quotes?serviceId=...&countryId=... memberikan opsi harga terurut dan selection sumber yang sudah ditentukan server. provider boleh dihilangkan atau diisi all; provider lain ditolak pada API v1 ini.
| Field | Penggunaan |
|---|---|
| services[].logo_url | URL absolut artwork layanan yang sama dengan website, atau null jika belum ada. icon tetap kunci ikon, bukan URL. |
| countries[].flag_url | URL gambar bendera berdasarkan ISO2, atau null jika negara tidak dikenali. flag dan iso2 tetap disertakan. |
| options[].selection | Target awal pembelian yang tepat: service.id dan country.id. Salin keduanya dari opsi yang sama; tidak perlu menentukan provider/server sendiri. |
| options[].price_idr / id | Harga yang disetujui / priceOptionId untuk pembelian. Jangan mengarang ID atau menaikkan batas harga saat retry. |
| expires_at / stock_count | Masa berlaku quote / indikasi stok, bukan reservasi nomor atau jaminan OTP. Harga diperiksa lagi saat membeli. |
const origin = "https://pingotp.com";
const apiKey = process.env.PING_API_KEY;
if (!apiKey) throw new Error("API key belum dikonfigurasi");
async function ping(path, init = {}) {
const response = await fetch(`${origin}/api/v1${path}`, {
...init,
cache: "no-store",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
});
const body = await response.json();
if (!response.ok || !body.ok) {
throw Object.assign(new Error(body.error?.message ?? "Request gagal"), {
code: body.error?.code,
retryAfter: response.headers.get("retry-after"),
});
}
return body.data;
}
const services = await ping("/catalog/services");
// Gunakan id layanan yang dipilih pelanggan dari respons services.
async function loadCountries(serviceId) {
return ping(`/catalog/countries?${new URLSearchParams({ serviceId })}`);
}
async function loadQuote(serviceId, countryId) {
return ping(`/catalog/quotes?${new URLSearchParams({ serviceId, countryId })}`);
}Simpan satu UUID clientRequestId dan seluruh payload di database backend integrator sebelum POST. UUID harus tetap untuk satu niat beli, termasuk restart, timeout, atau respons hilang. Header Idempotency-Key bukan pengganti field clientRequestId. API memproses satu nomor per request.
async function buyConfirmed(option, savedClientRequestId) {
return ping("/orders", {
method: "POST",
body: JSON.stringify({
serviceId: option.selection.service.id,
countryId: option.selection.country.id,
priceOptionId: option.id,
expectedPriceIdr: option.price_idr,
clientRequestId: savedClientRequestId,
}),
});
}
// Jangan panggil otomatis ketika memuat halaman atau mengambil quote.
// Simpan payload persis dan UUID yang sama untuk pemulihan respons hilang.- Gunakan ID di options[].selection, bukan serviceId/countryId representatif pada root quote. Jangan kirim provider; server menentukannya dari sumber layanan.
- expectedPriceIdr wajib sama dengan harga opsi. maxPriceIdr opsional hanya untuk batas fallback yang disetujui sebelumnya dan tidak boleh lebih kecil; tanpa itu harga fallback tidak melebihi harga awal.
- Alur memakai hold, batas pesanan, validasi harga, diskon rank, fallback aman setelah penolakan pasti, dan settlement yang sama dengan aplikasi. Hasil ambigu tidak dilanjutkan dengan pembelian baru.
- HTTP 200 mengembalikan hasil order, bukan jaminan nomor berhasil. Periksa status, isPreparing, message, dan id hasil. Fallback dapat menghasilkan id order berbeda; simpan id yang dikembalikan.
- Replay payload yang sama memulihkan hasil awal; perubahan pilihan, harga, atau ceiling memakai UUID yang sudah terpakai menghasilkan IDEMPOTENCY_CONFLICT. Jangan ganti UUID karena timeout atau HTTP 5xx.
Riwayat GET /orders menerima limit 1–100 (default 20), offset 0–1000000 (default 0), status all/waiting/completed/cancelled/failed, dan search maksimal 128 karakter. cancelled mencakup expired. Respons berupa { orders, counts, pagination }; pencarian memakai ID order, nomor, layanan, atau negara, bukan clientRequestId. Untuk respons pembelian hilang, replay POST dengan payload persis yang tersimpan.
const order = await ping(`/orders/${encodeURIComponent(savedOrderId)}`);
// Poll paling cepat setiap 5 detik; hentikan saat terminal.
// Jangan log code atau smsText ke log produksi.
if (order.canComplete) {
// Tampilkan SMS ke pelanggan yang berhak.
// Kirim POST /orders/{id}/complete dengan body {} setelah selesai.
}| Field / aksi | Aturan |
|---|---|
| status | waiting, completed, cancelled, expired, atau failed. waiting dapat sudah menerima SMS; periksa canComplete, code, dan smsText. |
| isPreparing | Nomor masih disiapkan atau hasil upstream diperiksa. Jangan membuat pembelian baru untuk menggantikannya. |
| POST /orders/{id}/cancel · {} | Hanya sebelum SMS dan saat canCancel. Waktu izin ada di cancelAvailableAt. Refund/release menunggu konfirmasi provider. |
| POST /orders/{id}/complete · {} | Saat canComplete. Settlement tidak membebankan biaya ulang ketika request diulang. |
| POST /orders/{id}/resend · { requestId } | Saat canResend; satu UUID tetap untuk satu resend. Dukungan mengikuti provider. Tidak membeli nomor baru atau menambah biaya. |
| createdAt / expiresAt | Unix milidetik pada order, berbeda dari fetched_at/expires_at ISO 8601 pada quote. |
Tidak ada webhook keluar ke integrator. Gunakan polling detail dengan backoff; GET /orders dan GET /orders/active hanya membaca database. Pembatasan pesanan dan saldo mengikuti akun, bukan jumlah API key.
Batas per menit: 60 request per key, 120 per akun lintas key, dan 300 per IP tepercaya (600 untuk jaringan tanpa IP terpercaya yang teridentifikasi). Pembacaan penawaran negara dan quote juga berbagi batas katalog 20 per akun per menit serta 200 per IP dengan aplikasi. Batas bisnis pembelian tetap berlaku. Body POST maksimal 4096 byte; parameter asing, query berulang, userId, harga non-integer, dan provider pada pembelian ditolak.
| HTTP / kode | Tindakan |
|---|---|
| 401 · UNAUTHENTICATED | Key salah, dicabut, kedaluwarsa, atau akun nonaktif. Hentikan request dan periksa key di UI. |
| 403 · FORBIDDEN | Scope belum diberikan. Izin key tidak bisa dinaikkan lewat request; buat key baru melalui UI. |
| 404 · NOT_FOUND | Pesanan tidak ada atau bukan milik pemilik key, termasuk jika pemilik key admin. |
| 409 · PRICE_CHANGED / PRICE_OPTION_UNAVAILABLE | Ambil quote baru dan minta persetujuan ulang untuk niat baru setelah hasil sebelumnya dipastikan. |
| 409 · IDEMPOTENCY_CONFLICT | Periksa intent awal. Jangan mengubah payload atau UUID untuk menutupi konflik. |
| 422 · INSUFFICIENT_BALANCE | Isi saldo melalui aplikasi. API tidak menambah saldo sendiri. |
| 429 | Patuhi Retry-After, antrekan polling, dan jangan merotasi key untuk melewati batas akun. |
| 5xx / timeout | Hasil mutasi dapat belum pasti. Pulihkan dengan UUID dan payload semula, bukan pesanan baru. |
Gunakan error.code, bukan teks message. X-Request-ID tersedia untuk bantuan diagnosis tanpa mengirim API key, OTP, atau isi SMS. Seluruh respons akun private/no-store. API internal /api/orders, /api/payments, pengelolaan key, admin, dan webhook tetap memakai proteksi semula; Bearer hanya berlaku pada /api/v1.