Dokumentasi API Publik Jual-Akun.com

Integrasikan katalog melalui API metadata dan webhook. Ambil judul, deskripsi, URL gambar, kategori, dan durasi garansi melalui API. Harga, stok, ketersediaan, status aktif, serta pemberitahuan versi metadata dikirim melalui webhook saat berubah.

Setelah pembayaran pembeli di toko Anda terverifikasi lunas, buat pembelian menggunakan saldo akun Jual-Akun.com. Simpan hubungan ID produk dan invoice secara terpisah dari produk lokal toko Anda.

Setelah endpoint webhook terverifikasi, snapshot harga/stok/status dikirim otomatis. Ambil metadata untuk ID produk dari snapshot melalui API. Untuk memulihkan data di server Anda, gunakan tombol Kirim ulang snapshot dan lengkapi metadata yang belum tersedia.

OpenAPI JSON dapat dibaca tanpa login dan berisi endpoint, parameter, schema respons, contoh JSON, serta format webhook. Semua data pada contoh di bawah adalah data contoh, bukan akun atau transaksi nyata.

Webhook katalog publik

Satu endpoint per akun. Pembaruan harga, stok, dan ketersediaan/aktif produk digabung dalam payload ringan. Teks metadata tidak ikut dikirim ulang ketika hanya stok/harga berubah. Jika metadata berubah, webhook mengirim ID produk dan metadata_version.

Login dan generate API key untuk mendaftarkan endpoint webhook.


Urutan pemasangan

  1. Generate API key. Siapkan endpoint HTTPS, lalu simpan URL-nya di atas.
  2. Salin Subscription ID dan Public key Ed25519 ke konfigurasi backend Anda. Public key digunakan untuk verifikasi tanda tangan; API key tetap rahasia.
  3. Sistem mengirim webhook.verify. Verifikasi tanda tangan lalu balas dengan received: true, event_id, dan challenge yang sama. Data katalog dikirim setelah tahap ini berhasil.
  4. Simpan setiap event ke inbox database sebelum mengirim tanda terima. Proses inbox berurutan berdasarkan sequence per subscription. Simpan ID event untuk mencegah pemrosesan ganda. Jangan menunggu pengambilan metadata sebelum ACK; kerjakan melalui worker setelah event tersimpan.
  5. Terima catalog.snapshot bertahap (maksimal 50 ID produk unik dan 512 KB per pesan). Simpan setiap halaman sementara berdasarkan snapshot_id. Setelah complete: true, terapkan state harga/stok/status dan nonaktifkan produk sumber ini saja yang tidak tercantum. Produk lokal dan sumber lain tetap terpisah.
  6. Lengkapi metadata untuk ID dari snapshot melalui GET /products.php?ids=101,102, maksimal 50 ID per request. Simpan metadata_version dari respons API. Jangan tampilkan/aktifkan produk baru sebelum metadata dan state tersedia.
  7. Terima catalog.updated. Terapkan data.products berdasarkan version state; stok adalah angka absolut. Proses data.metadata_changes secara terpisah: ambil metadata hanya jika belum ada atau versi lebih baru. Respons API dengan versi metadata lebih lama tidak boleh menimpa data terbaru.
  8. Jika data penerima hilang, gunakan Kirim ulang snapshot. Jika URL diganti atau webhook diaktifkan ulang setelah dijeda, salin ulang subscription/public key yang baru dan ulangi verifikasi.

Pengiriman berlangsung otomatis dan dapat tertunda. Jika endpoint gagal menerima pesan, sistem mencoba kembali dengan event ID dan isi yang sama, menggunakan jeda bertahap. Setelah 12 kegagalan berturut-turut, webhook dijeda; periksa endpoint lalu aktifkan kembali. Perubahan berdekatan dapat digabung. Stok mengikuti informasi terakhir yang diketahui Jual-Akun.com; checkout tetap memvalidasi harga dan ketersediaan.

Format webhook

data.products berisi harga/stok/status. data.metadata_changes hanya berisi ID dan versi metadata. Keduanya dapat muncul dalam satu event; salah satunya dapat berupa array kosong. Data item pembelian diambil melalui API hasil pesanan, bukan webhook katalog.

Contoh pembaruan stok

{
    "schema_version": 1,
    "subscription_id": "11111111-1111-4111-8111-111111111111",
    "event_id": "55555555-5555-4555-8555-555555555555",
    "sequence": "3",
    "type": "catalog.updated",
    "occurred_at": "2026-10-13T10:00:00+07:00",
    "data": {
        "from_version": "40",
        "to_version": "43",
        "products": [
            {
                "id": "101",
                "version": "43",
                "metadata_version": "40",
                "price": 25000,
                "currency": "IDR",
                "stock": 10,
                "is_active": true,
                "status": "active",
                "is_available": true,
                "product_type": "ready_stock"
            }
        ],
        "metadata_changes": []
    }
}

Contoh pemberitahuan metadata berubah

{
    "schema_version": 1,
    "subscription_id": "11111111-1111-4111-8111-111111111111",
    "event_id": "66666666-6666-4666-8666-666666666666",
    "sequence": "4",
    "type": "catalog.updated",
    "occurred_at": "2026-10-13T10:00:00+07:00",
    "data": {
        "from_version": "43",
        "to_version": "44",
        "products": [],
        "metadata_changes": [
            {
                "id": "101",
                "metadata_version": "44"
            }
        ]
    }
}

Pemberitahuan di atas meminta worker penerima mengambil GET /product.php?product=101 atau batch GET /products.php?ids=101. Simpan metadata bila versi respons sama atau lebih baru daripada versi yang diumumkan. Jika lebih rendah, antrekan retry dengan backoff; jangan polling seluruh katalog.

is_active: false berarti katalog tidak boleh ditampilkan/dibeli. Pesan nonaktif berisi ID, versi, stock: 0, price: null dan is_available: false. is_active: true dengan is_available: false berarti katalog masih aktif tetapi belum dapat dibeli. product_type adalah ready_stock atau pre_order. Nilai version, metadata_version, sequence dan ID dikirim sebagai string; bandingkan versi secara numerik, bukan alfabet.

version adalah versi harga/stok/status; metadata_version adalah versi judul/deskripsi/gambar/kategori/garansi. Simpan keduanya secara terpisah. Jangan membuang pemberitahuan metadata baru hanya karena versi state tidak berubah. Produk aktif yang metadata-nya belum tersedia tetap disembunyikan sampai data lengkap.

Nomor versi katalog tidak harus berurutan karena perubahan berdekatan digabung. Nomor sequence berurutan per subscription. Retry mempertahankan event dan sequence yang sama; timestamp tanda tangan diperbarui setiap pengiriman.

Verifikasi kepemilikan endpoint

{
    "schema_version": 1,
    "subscription_id": "11111111-1111-4111-8111-111111111111",
    "event_id": "22222222-2222-4222-8222-222222222222",
    "sequence": "1",
    "type": "webhook.verify",
    "occurred_at": "2026-10-13T10:00:00+07:00",
    "data": {
        "challenge": "challenge-contoh-balas-dengan-nilai-yang-sama"
    }
}

Balas setelah verifikasi tanda tangan dan penyimpanan event berhasil. Nilai event_id dan challenge harus sama dengan pesan yang diterima:

{
    "received": true,
    "event_id": "22222222-2222-4222-8222-222222222222",
    "challenge": "challenge-contoh-balas-dengan-nilai-yang-sama"
}

Untuk event katalog, cukup balas:

{"received": true, "event_id": "ID-EVENT-YANG-DITERIMA"}

HTTP 2xx saja belum cukup. Tanda terima harus JSON yang cocok dengan event dan maksimal 8 KB. Selesaikan penerimaan dalam 4 detik; proses katalog yang berat dilanjutkan melalui worker di server Anda.

Snapshot

{
    "schema_version": 1,
    "subscription_id": "11111111-1111-4111-8111-111111111111",
    "event_id": "33333333-3333-4333-8333-333333333333",
    "sequence": "2",
    "type": "catalog.snapshot",
    "occurred_at": "2026-10-13T10:00:00+07:00",
    "data": {
        "snapshot_id": "44444444-4444-4444-8444-444444444444",
        "complete": true,
        "products": [
            {
                "id": "101",
                "version": "40",
                "metadata_version": "40",
                "price": 25000,
                "currency": "IDR",
                "stock": 12,
                "is_active": true,
                "status": "active",
                "is_available": true,
                "product_type": "ready_stock"
            }
        ],
        "metadata_changes": [
            {
                "id": "101",
                "metadata_version": "40"
            }
        ]
    }
}

Halaman terakhir memiliki complete: true, termasuk snapshot kosong. Pembaruan yang terjadi selama snapshot akan menyusul melalui catalog.updated.

Verifikasi tanda tangan

Gunakan public key yang disalin dari halaman ini. Tolak permintaan tanpa tanda tangan valid atau dengan timestamp berbeda lebih dari 5 menit. Sinkronkan jam server.

X-SiJual-Public-Subscription: SUBSCRIPTION_ID
X-SiJual-Public-Event-Id: EVENT_ID
X-SiJual-Public-Timestamp: UNIX_TIMESTAMP
X-SiJual-Public-Content-SHA256: SHA256_RAW_BODY
X-SiJual-Public-Signature: BASE64_ED25519_SIGNATURE

String yang ditandatangani (pemisah \n, tanpa newline tambahan di akhir):

sijual-public-v1
SUBSCRIPTION_ID
UNIX_TIMESTAMP
EVENT_ID
SHA256_RAW_BODY

Hitung hash dari body HTTP asli sebelum parsing JSON. Periksa kecocokan subscription/event ID pada header dan body. Batasi ukuran request di reverse proxy dan aplikasi.

Contoh penerima PHP: verifikasi dan penyimpanan inbox

Buat tabel inbox sekali melalui migrasi aplikasi penerima:

CREATE TABLE sijual_public_webhook_inbox (
  event_id CHAR(36) PRIMARY KEY,
  subscription_id CHAR(36) NOT NULL,
  sequence_no BIGINT UNSIGNED NOT NULL,
  body_hash CHAR(64) NOT NULL,
  payload MEDIUMTEXT NOT NULL,
  received_at TIMESTAMP NOT NULL,
  processed_at TIMESTAMP NULL,
  UNIQUE KEY subscription_sequence (subscription_id, sequence_no)
);
<?php
// Contoh endpoint HTTPS dengan PHP sodium, PDO, dan inbox MySQL.
// Isi SIJUAL_PUBLIC_SUBSCRIPTION dan SIJUAL_PUBLIC_KEY dari halaman dokumentasi.
declare(strict_types=1);

header('Content-Type: application/json');
function rejectWebhook(int $status): void {
    http_response_code($status);
    echo json_encode(['received' => false]);
    exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') rejectWebhook(405);
if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 524288) rejectWebhook(413);
$raw = file_get_contents('php://input', false, null, 0, 524289);
if ($raw === false || strlen($raw) > 524288) rejectWebhook(413);
$subscription = $_SERVER['HTTP_X_SIJUAL_PUBLIC_SUBSCRIPTION'] ?? '';
$eventId = $_SERVER['HTTP_X_SIJUAL_PUBLIC_EVENT_ID'] ?? '';
$timestamp = $_SERVER['HTTP_X_SIJUAL_PUBLIC_TIMESTAMP'] ?? '';
$hash = $_SERVER['HTTP_X_SIJUAL_PUBLIC_CONTENT_SHA256'] ?? '';
$signature = base64_decode($_SERVER['HTTP_X_SIJUAL_PUBLIC_SIGNATURE'] ?? '', true);
$publicKey = base64_decode((string) getenv('SIJUAL_PUBLIC_KEY'), true);
$expectedSubscription = (string) getenv('SIJUAL_PUBLIC_SUBSCRIPTION');
if ($expectedSubscription === '' || !hash_equals($expectedSubscription, $subscription) ||
    !preg_match('/^[a-f0-9-]{36}$/D', $eventId) || !ctype_digit($timestamp) ||
    abs(time() - (int) $timestamp) > 300 ||
    !hash_equals(hash('sha256', $raw), $hash) ||
    $signature === false || strlen($signature) !== SODIUM_CRYPTO_SIGN_BYTES ||
    $publicKey === false || strlen($publicKey) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) rejectWebhook(401);
$canonical = "sijual-public-v1\n{$subscription}\n{$timestamp}\n{$eventId}\n{$hash}";
if (!sodium_crypto_sign_verify_detached($signature, $canonical, $publicKey)) rejectWebhook(401);
$event = json_decode($raw, true);
if (!is_array($event) || ($event['schema_version'] ?? null) !== 1 ||
    ($event['event_id'] ?? null) !== $eventId || ($event['subscription_id'] ?? null) !== $subscription ||
    !is_string($event['sequence'] ?? null) || !preg_match('/^[1-9][0-9]{0,18}$/D', $event['sequence']) ||
    !in_array($event['type'] ?? null, ['webhook.verify', 'catalog.snapshot', 'catalog.updated'], true) ||
    !is_array($event['data'] ?? null)) rejectWebhook(400);
if ($event['type'] === 'webhook.verify' && !is_string($event['data']['challenge'] ?? null)) rejectWebhook(400);

try {
    $db = new PDO((string) getenv('WEBHOOK_DB_DSN'), (string) getenv('WEBHOOK_DB_USER'),
        (string) getenv('WEBHOOK_DB_PASSWORD'), [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    // Create the inbox table once with your application's migrations, not here.
    $db->beginTransaction();
    $insert = $db->prepare('INSERT IGNORE INTO sijual_public_webhook_inbox
        (event_id, subscription_id, sequence_no, body_hash, payload, received_at)
        VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)');
    $insert->execute([$eventId, $subscription, $event['sequence'], $hash, $raw]);
    $check = $db->prepare('SELECT body_hash, subscription_id, sequence_no
        FROM sijual_public_webhook_inbox WHERE event_id = ? FOR UPDATE');
    $check->execute([$eventId]);
    $existing = $check->fetch(PDO::FETCH_ASSOC);
    if (!$existing || !hash_equals($existing['body_hash'], $hash) ||
        $existing['subscription_id'] !== $subscription || (string) $existing['sequence_no'] !== $event['sequence']) {
        $db->rollBack();
        rejectWebhook(409);
    }
    $db->commit(); // ACK only after durable storage, including duplicate events.
} catch (Throwable $error) {
    if (isset($db) && $db->inTransaction()) $db->rollBack();
    rejectWebhook(503);
}
$ack = ['received' => true, 'event_id' => $eventId];
if ($event['type'] === 'webhook.verify') $ack['challenge'] = $event['data']['challenge'];
echo json_encode($ack);
// Worker setelah ACK, berurutan per subscription:
// - data.products: terapkan state dengan version yang lebih baru (stok absolut).
// - data.metadata_changes: gabungkan ID+metadata_version terbaru dalam antrean.
// - Ambil API products.php?ids=... dalam batch maksimal 50 ID; bukan per kunjungan.
// - Simpan metadata_version terpisah dari version state, abaikan respons lama.
// - Jangan aktifkan produk baru sebelum metadata dan state tersedia.
// - Snapshot hanya boleh menonaktifkan produk sumber Si-Jual, bukan produk lokal.
// Tidak ada HTTP/API lanjutan sebelum inbox tersimpan dan ACK dikirim.

Contoh menyimpan event dan memberikan tanda terima. Tambahkan worker untuk menerapkan data katalog sesuai aturan snapshot/versi di atas. Keputusan pembayaran dan saldo tetap dilakukan oleh server transaksi.

API metadata katalog

Base URL: https://jual-akun.com/api/v1/public. Gunakan header X-Api-Key dan Accept: application/json. Metadata hanya berisi id, metadata_version, title, description, image_url, categories, warranty_days, dan max_quantity. Harga, stok dan status tidak tersedia pada respons metadata. image_url bernilai null jika gambar tidak tersedia atau URL tidak valid; gunakan gambar pengganti di aplikasi Anda.

max_quantity adalah batas teknis jumlah per transaksi untuk produk tersebut (1–1.000), bukan jumlah stok. Untuk ready_stock, gunakan batas terkecil antara nilai ini dan stok terbaru dari webhook. Untuk pre_order, tidak ada batas stok tetapi batas teknis tetap berlaku. Server memeriksa kembali jumlah dan ketersediaan saat pembelian; jangan menganggap data katalog sebagai reservasi stok.

Katalog mencakup produk lokal, produk marketplace reseller, serta produk supplier yang dipublikasikan di Jual-Akun.com. Seluruhnya memakai kontrak metadata, webhook, dan pembelian yang sama; integrasi tidak perlu menghubungi supplier secara langsung. Produk draft, ditolak, atau milik toko yang dinonaktifkan tidak boleh diaktifkan. Simpan ID produk persis seperti yang diterima sebagai string, termasuk ID marketplace yang panjang.

DataSumber
Judul, deskripsi, URL gambar, kategori, durasi garansiAPI metadata
Harga, stok, aktif/nonaktif, ketersediaan, tipe ready-stock/pre-orderWebhook data.products
ID dan versi metadata yang berubahWebhook data.metadata_changes

Daftarkan dan verifikasi webhook dahulu, lalu tunggu snapshot awal. API membaca metadata yang sudah disiapkan server, bukan meminta ulang supplier. Selama pembangunan awal belum selesai, API mengembalikan HTTP 503, bukan daftar kosong atau sebagian yang seolah-olah lengkap. Jika belum siap atau metadata belum dapat dibaca, respons HTTP 503 dengan Retry-After: 60; tunggu sebelum mencoba lagi dan hubungi admin bila berlanjut. Ambil metadata pada sinkron awal, saat pemberitahuan versi berubah, atau saat pemulihan. Simpan di database backend Anda; jangan memanggil API setiap kunjungan pembeli atau polling berkala.

Mengambil beberapa produk sekaligus

curl "https://jual-akun.com/api/v1/public/products.php?ids=101,999" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Accept: application/json"
{
    "status": "success",
    "success": true,
    "msg": "Catalog metadata fetched successfully.",
    "message": "Catalog metadata fetched successfully.",
    "data": [
        {
            "id": "101",
            "title": "Contoh produk digital",
            "description": "Deskripsi produk",
            "image_url": "https://jual-akun.example.test/images/contoh.webp",
            "categories": [
                {
                    "id": "8",
                    "name": "Digital"
                }
            ],
            "warranty_days": 1,
            "max_quantity": 1000,
            "metadata_version": "40"
        }
    ],
    "meta": {
        "missing_ids": [
            "999"
        ]
    }
}

meta.missing_ids berisi ID yang metadata-nya tidak tersedia atau tidak aktif. Jangan membuatnya aktif berdasarkan respons API; status mengikuti webhook. Jangan menghapus data produk lokal/sumber lain. Gabungkan ID dari beberapa event, deduplikasi dan ambil maksimal 50 ID per request untuk menghemat kuota.

Daftar metadata dengan cursor

curl "https://jual-akun.com/api/v1/public/products.php?limit=1&after_id=0" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Accept: application/json"
{
    "status": "success",
    "success": true,
    "msg": "Catalog metadata fetched successfully.",
    "message": "Catalog metadata fetched successfully.",
    "data": [
        {
            "id": "101",
            "title": "Contoh produk digital",
            "description": "Deskripsi produk",
            "image_url": "https://jual-akun.example.test/images/contoh.webp",
            "categories": [
                {
                    "id": "8",
                    "name": "Digital"
                }
            ],
            "warranty_days": 1,
            "max_quantity": 1000,
            "metadata_version": "40"
        }
    ],
    "meta": {
        "limit": 1,
        "has_more": true,
        "next_after_id": "101"
    }
}

Jika meta.has_more: true, gunakan meta.next_after_id sebagai after_id berikutnya. Setelah selesai nilainya null. Tidak ada parameter page/offset. Daftar metadata bukan snapshot status: jangan menonaktifkan produk hanya karena tidak tercantum pada satu halaman API.

Mengambil satu produk

curl "https://jual-akun.com/api/v1/public/product.php?product=101" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Accept: application/json"
{
    "status": "success",
    "success": true,
    "msg": "Product metadata fetched successfully.",
    "message": "Product metadata fetched successfully.",
    "data": {
        "id": "101",
        "title": "Contoh produk digital",
        "description": "Deskripsi produk",
        "image_url": "https://jual-akun.example.test/images/contoh.webp",
        "categories": [
            {
                "id": "8",
                "name": "Digital"
            }
        ],
        "warranty_days": 1,
        "max_quantity": 1000,
        "metadata_version": "40"
    }
}
Parameter queryEndpointTipeWajibBatas
idsproducts.phpstringOpsional, mode batch1–50 ID positif, dipisahkan koma tanpa spasi; maksimal 849 karakter. Jangan digabung dengan limit/after_id. Duplikat dihapus.
limitproducts.phpintegerOpsional, default 201–50, untuk mode cursor saja.
after_idproducts.phpstring angka atau integerOpsional, default 00–8999999999999999; ID terakhir dari next_after_id.
productproduct.phpstring angka atau integerYa1–8999999999999999; ID produk dari webhook.

Hanya parameter pada tabel ini yang diterima. Parameter seperti page, offset, keyword, atau alias id ditolak HTTP 422; gunakan cursor after_id, batch ids, atau product sesuai endpoint. Jangan menyertakan API key pada query URL.

Contoh error metadata — parameter tidak didukung (HTTP 422)
{
    "status": "error",
    "success": false,
    "msg": "Parameter query tidak didukung. Gunakan parameter metadata sesuai dokumentasi.",
    "message": "Parameter query tidak didukung. Gunakan parameter metadata sesuai dokumentasi."
}
Contoh metadata belum dapat dibaca (HTTP 503, Retry-After: 60)
{
    "status": "error",
    "success": false,
    "msg": "Metadata katalog belum dapat dibaca. Silakan coba kembali atau hubungi admin.",
    "message": "Metadata katalog belum dapat dibaca. Silakan coba kembali atau hubungi admin."
}

Bandingkan metadata_version secara numerik. Respons API dapat lebih baru daripada pemberitahuan webhook; gunakan versi yang lebih baru, tanpa menimpa harga/stok/status. Respons lama yang selesai belakangan tidak boleh menimpa metadata baru. Retry pengambilan metadata tidak membuat pembelian dan tidak memotong saldo.

Judul, deskripsi, dan nama kategori adalah teks biasa. Escape saat ditampilkan, bukan dimasukkan langsung sebagai HTML. Deskripsi maksimal 12.000 karakter, judul 500, URL gambar 2.000; maksimal 20 kategori. URL gambar bukan file gambar; gunakan kebijakan media yang aman di aplikasi penerima. warranty_days adalah durasi hari, bukan endpoint untuk mengajukan klaim garansi. API publik belum menyediakan pengajuan klaim.

API transaksi dan akun

Base URL: https://jual-akun.com/api/v1/public. Gunakan header X-Api-Key dan Accept: application/json. API key pada query URL tidak diterima. Kirim body pembelian sebagai JSON dengan Content-Type: application/json. Batas lengkap tersedia pada bagian Batas request.

MethodEndpointKegunaan
GET/profile.phpProfil dan saldo akun pemilik API key.
GET/categories.phpDaftar kategori; harga, stok dan ketersediaan produk tetap melalui webhook.
POST/buy_product.phpMembeli menggunakan saldo akun.
GET/orders.php?order=TRANS_IDStatus serta item pesanan milik akun tersebut.
HeaderTipeWajibNilai
X-Api-KeystringYa, semua endpoint API termasuk metadataKey yang dibuat melalui halaman ini; hanya dikirim dari backend.
AcceptstringDisarankanapplication/json, agar kesalahan framework/routing juga dikembalikan sebagai JSON.
Content-TypestringUntuk body JSON pembelianapplication/json. Form-data dan form-urlencoded juga diterima.

GET /profile.php dan GET /categories.php tidak memerlukan parameter query. Respons profil memakai objek data; kategori memakai array data, yang dapat kosong. Nilai saldo dan harga menggunakan IDR. Tanggal menggunakan format ISO 8601 dan dapat null jika belum tersedia.

curl "https://jual-akun.com/api/v1/public/profile.php" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Accept: application/json"

Contoh respons profil — HTTP 200

{
    "status": "success",
    "success": true,
    "msg": "Profile fetched successfully.",
    "message": "Profile fetched successfully.",
    "data": {
        "user_id": 5,
        "username": "contoh_user",
        "email": "[email protected]",
        "phone": "6281234567890",
        "balance": 150000,
        "is_active": true,
        "created_at": "2026-10-13T10:00:00+07:00"
    }
}

Contoh respons kategori — HTTP 200

{
    "status": "success",
    "success": true,
    "msg": "Categories fetched successfully.",
    "message": "Categories fetched successfully.",
    "data": [
        {
            "id": 8,
            "name": "Digital",
            "slug": "digital",
            "product_count": 12
        }
    ]
}

Membuat pembelian

curl -X POST "https://jual-akun.com/api/v1/public/buy_product.php" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"action":"buyProduct","id":101,"amount":1,"max_total":25000,"invoice_external":"TOKO-ORDER-000123"}'

id diambil dari webhook. amount adalah jumlah bulat positif, maksimal 1.000 per transaksi; produk tertentu dapat memiliki batas lebih rendah. max_total wajib diisi dengan batas total IDR yang sudah disetujui untuk seluruh quantity. Jika harga melebihi batas, respons HTTP 409 dengan PRICE_LIMIT_EXCEEDED; saldo tidak dipotong. Kupon dapat dikirim melalui coupon.

Parameter bodyTipeWajibBatas dan arti
actionstringOpsionalJika dikirim, harus buyProduct.
idinteger positif atau string angkaYaID produk dari webhook. ID webhook berupa string; boleh dikirim sebagai string angka agar tidak kehilangan presisi.
amountintegerOpsional, default 11–1.000 item produk yang sama; batas produk tertentu dapat lebih rendah. Pecahan, nol, dan angka negatif ditolak.
max_totalnumber, IDRYa untuk pembelian baru; tetap kirim saat retryLebih dari 0, maksimal 12 digit sebelum desimal dan 2 sesudahnya. Tanpa pemisah ribuan. Batas untuk seluruh quantity, bukan satu item.
invoice_externalstringYa1–191 karakter setelah trim; unik per akun. ID pesanan di toko Anda, bukan invoice Jual-Akun.com.
couponstringOpsionalMaksimal 64 byte UTF-8 setelah trim, setara 64 karakter jika semuanya ASCII.

invoice_external wajib unik per pesanan (maksimal 191 karakter). Jika koneksi terputus, retry dengan invoice yang sama. Invoice yang sudah berhasil akan mengembalikan transaksi yang sama; jangan membuat invoice baru hanya karena timeout. Penggunaan invoice yang sama untuk produk/jumlah berbeda ditolak HTTP 409.

Respons pembelian dan detail order menaruh trans_id, status, harga dan items langsung di tingkat utama JSON. Berbeda dari profil/kategori, data di sini adalah array teks item, bukan objek metadata order. success: true atau HTTP 200 saja belum cukup untuk menyatakan pesanan selesai.

Contoh pembelian selesai — HTTP 200

{
    "status": "success",
    "success": true,
    "msg": "Order created successfully.",
    "message": "Order created successfully.",
    "trans_id": "API-202610-EXAMPLE0001",
    "invoice": "INV-EXAMPLE-0001",
    "invoice_external": "TOKO-ORDER-000123",
    "external_order_id": "TOKO-ORDER-000123",
    "product_id": 101,
    "product_title": "Contoh produk digital",
    "product_name": "Contoh produk digital",
    "product_type": "ready_stock",
    "quantity": 1,
    "unit_price": 25000,
    "total_price": 25000,
    "payment_method": "saldo",
    "payment_name": "Saldo",
    "payment_status": "PAID",
    "fulfillment_status": "completed",
    "paid_at": "2026-10-13T10:00:00+07:00",
    "created_at": "2026-10-13T10:00:00+07:00",
    "pre_order": null,
    "data": [
        "Email : [email protected]\nPassword : CONTOH-BUKAN-AKUN-ASLI"
    ],
    "items": [
        {
            "transaction_detail_id": 9001,
            "product_detail_id": 12001,
            "content": "Email : [email protected]\nPassword : CONTOH-BUKAN-AKUN-ASLI"
        }
    ]
}

Contoh pembelian masih diproses — HTTP 200

{
    "status": "success",
    "success": true,
    "msg": "Order created successfully.",
    "message": "Order created successfully.",
    "trans_id": "API-202610-EXAMPLE0001",
    "invoice": "INV-EXAMPLE-0001",
    "invoice_external": "TOKO-ORDER-000123",
    "external_order_id": "TOKO-ORDER-000123",
    "product_id": 101,
    "product_title": "Contoh produk digital",
    "product_name": "Contoh produk digital",
    "product_type": "ready_stock",
    "quantity": 1,
    "unit_price": 25000,
    "total_price": 25000,
    "payment_method": "saldo",
    "payment_name": "Saldo",
    "payment_status": "PAID",
    "fulfillment_status": "processing",
    "paid_at": "2026-10-13T10:00:00+07:00",
    "created_at": "2026-10-13T10:00:00+07:00",
    "pre_order": null,
    "data": [],
    "items": []
}
Contoh respons retry invoice yang sudah ada — HTTP 200
{
    "status": "success",
    "success": true,
    "msg": "Order already exists for this invoice_external.",
    "message": "Order already exists for this invoice_external.",
    "trans_id": "API-202610-EXAMPLE0001",
    "invoice": "INV-EXAMPLE-0001",
    "invoice_external": "TOKO-ORDER-000123",
    "external_order_id": "TOKO-ORDER-000123",
    "product_id": 101,
    "product_title": "Contoh produk digital",
    "product_name": "Contoh produk digital",
    "product_type": "ready_stock",
    "quantity": 1,
    "unit_price": 25000,
    "total_price": 25000,
    "payment_method": "saldo",
    "payment_name": "Saldo",
    "payment_status": "PAID",
    "fulfillment_status": "processing",
    "paid_at": "2026-10-13T10:00:00+07:00",
    "created_at": "2026-10-13T10:00:00+07:00",
    "pre_order": null,
    "data": [],
    "items": [],
    "idempotent": true
}

Mengambil hasil pesanan

curl "https://jual-akun.com/api/v1/public/orders.php?order=TRANS_ID_DARI_PEMBELIAN" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Accept: application/json"
Parameter queryTipeWajibNilai
orderstring tidak kosongYatrans_id dari respons pembelian. Bukan invoice atau invoice_external.

Simpan trans_id dari respons pembelian. Data hanya dapat diakses oleh akun pemilik transaksi. Item/credentials hanya dikirim setelah pembayaran PAID dan pengiriman item lengkap. Periksa payment_status, fulfillment_status dan kelengkapan item; PAID dapat berarti saldo sudah dibayar tetapi item masih diproses.

Nominal pada respons order adalah harga transaksi tersebut, bukan harga katalog terbaru.

Webhook digunakan untuk katalog, bukan status pesanan. Untuk order yang belum selesai, gunakan jeda bertahap, misalnya 5, 10, 20 lalu 30 detik; angka ini rekomendasi client, bukan jadwal wajib server. Gabungkan pemeriksaan banyak order dalam antrean agar total request tidak melebihi kuota akun/IP. Hentikan pemeriksaan setelah selesai atau berstatus akhir. Tetapkan batas waktu operasional; jika processing terlalu lama, hubungi admin, bukan otomatis membeli ulang atau refund.

Pengiriman item harus melalui backend yang memverifikasi pemilik order. Jangan mencatat nilai data atau items pada log publik. Item pengganti garansi dapat membuat jumlah items melebihi quantity; gunakan status fulfillment, bukan kesamaan jumlah array, untuk menentukan selesai.

Perlakukan trans_id sebagai ID opaque: simpan dan kirim kembali persis seperti respons, tanpa mengasumsikan format atau mengambilnya dari invoice. Isi item ada pada items[].content atau array data. ID detail opsional dapat berbeda antar jenis katalog, misalnya marketplace_order_item_id; jangan menggunakannya sebagai ID produk.

Contoh hasil order selesai — HTTP 200

{
    "status": "success",
    "success": true,
    "msg": "Order fetched successfully.",
    "message": "Order fetched successfully.",
    "trans_id": "API-202610-EXAMPLE0001",
    "invoice": "INV-EXAMPLE-0001",
    "invoice_external": "TOKO-ORDER-000123",
    "external_order_id": "TOKO-ORDER-000123",
    "product_id": 101,
    "product_title": "Contoh produk digital",
    "product_name": "Contoh produk digital",
    "product_type": "ready_stock",
    "quantity": 1,
    "unit_price": 25000,
    "total_price": 25000,
    "payment_method": "saldo",
    "payment_name": "Saldo",
    "payment_status": "PAID",
    "fulfillment_status": "completed",
    "paid_at": "2026-10-13T10:00:00+07:00",
    "created_at": "2026-10-13T10:00:00+07:00",
    "pre_order": null,
    "data": [
        "Email : [email protected]\nPassword : CONTOH-BUKAN-AKUN-ASLI"
    ],
    "items": [
        {
            "transaction_detail_id": 9001,
            "product_detail_id": 12001,
            "content": "Email : [email protected]\nPassword : CONTOH-BUKAN-AKUN-ASLI"
        }
    ]
}

Contoh hasil order masih diproses — HTTP 200

{
    "status": "success",
    "success": true,
    "msg": "Order fetched successfully.",
    "message": "Order fetched successfully.",
    "trans_id": "API-202610-EXAMPLE0001",
    "invoice": "INV-EXAMPLE-0001",
    "invoice_external": "TOKO-ORDER-000123",
    "external_order_id": "TOKO-ORDER-000123",
    "product_id": 101,
    "product_title": "Contoh produk digital",
    "product_name": "Contoh produk digital",
    "product_type": "ready_stock",
    "quantity": 1,
    "unit_price": 25000,
    "total_price": 25000,
    "payment_method": "saldo",
    "payment_name": "Saldo",
    "payment_status": "PAID",
    "fulfillment_status": "processing",
    "paid_at": "2026-10-13T10:00:00+07:00",
    "created_at": "2026-10-13T10:00:00+07:00",
    "pre_order": null,
    "data": [],
    "items": []
}

Status pesanan dan tindakan client

status: success dan success: true berarti request berhasil diproses, bukan selalu berarti pesanan selesai. Keputusan pengiriman item harus mempertimbangkan payment_status dan fulfillment_status bersama.

fulfillment_statusArtiTindakan
completedPembayaran PAID dan item lengkap.Simpan item dengan aman, kirim ke pelanggan yang tepat, dan hentikan pemeriksaan berkala.
processingSudah PAID, tetapi pengiriman item belum selesai. Bisa membutuhkan penanganan manual.Tunggu/periksa dengan backoff. processing tidak berarti pembelian gagal. Jangan langsung dibeli ulang atau direfund. Jika terlalu lama, hubungi admin dengan trans_id/invoice.
pending_paymentNilai pembayaran saat ini bukan PAID.Periksa payment_status; bisa belum lunas, kedaluwarsa, gagal, atau sudah direfund. Jangan menafsirkan ini selalu sebagai menunggu pembayaran.
payment_status yang umumArtiTindakan
UNPAIDBelum lunas.Jangan kirim item atau membuat pembelian duplikat. Periksa transaksi yang sama.
PAIDTransaksi sudah dibayar.Periksa fulfillment_status. PAID saja tidak membuktikan item sudah selesai.
EXPIREDTransaksi kedaluwarsa.Hentikan pemeriksaan otomatis dan rekonsiliasi sebelum membuat pesanan baru.
FAILEDServer menandai transaksi gagal.Jangan kirim item; periksa penyelesaian saldo dengan server/admin. Jangan mengasumsikan saldo sudah kembali.
REFUNDEDServer menandai transaksi direfund.Hentikan pemeriksaan otomatis dan rekonsiliasi saldo. Jangan mengkreditkan refund pelanggan dua kali.

Nilai pembayaran di atas adalah nilai yang umum, bukan alasan untuk menganggap nilai lain sebagai gagal. Untuk nilai yang tidak dikenal atau status yang tidak konsisten, hentikan keputusan finansial otomatis dan hubungi admin. Status internal sumber item tidak diekspos; client tetap memakai status API di atas.

Batas request dan penanganan error

Kelompok APIPer akunPer IP
GET metadata (products.php/product.php), profil, kategori, dan order60 request/menit, gabungan endpoint baca120 request/menit, gabungan request baca semua akun pada IP tersebut
POST pembelian10 request/menit30 request/menit, gabungan request pembelian semua akun pada IP tersebut

Kuota baca dan pembelian terpisah. Batas IP berlaku sebelum autentikasi; mengganti API key tidak mereset kuota akun. Retry dan request dengan key valid yang gagal validasi juga memakai kuota. Satu request pembelian dapat berisi lebih dari satu item produk yang sama; batas request bukan batas jumlah item.

Jika menerima HTTP 429, baca Retry-After sebagai waktu tunggu dalam detik. Header X-RateLimit-Limit dan X-RateLimit-Remaining menunjukkan batas dan sisa kuota limiter yang menghasilkan header tersebut. Hindari retry rapat atau tanpa batas.

Tindakan pada halaman iniBatas per pengguna
Generate/Generate Ulang API Key6 kali/menit
Simpan/Aktifkan Webhook3 kali/10 menit
Nonaktifkan Webhook6 kali/menit
Kirim ulang snapshot1 kali/10 menit

Penghitung tiap tindakan pengaturan terpisah. Tombol Kirim ulang snapshot hanya untuk webhook aktif dan terverifikasi; tunggu pengiriman/snapshot sebelumnya selesai sebelum meminta lagi. Snapshot memulihkan harga/stok/status dan referensi metadata; ambil metadata yang belum tersedia atau versinya berbeda melalui API. Gunakan untuk pemulihan data, bukan sebagai polling katalog. Tindakan pengaturan membutuhkan sesi login dan CSRF, bukan hanya header API key.

HTTPArti/tindakan
401 / 403Periksa API key atau status/izin akun; jangan retry berulang tanpa perbaikan.
404Produk/order tidak ditemukan untuk akun tersebut.
405Method salah; gunakan GET untuk baca dan POST untuk membeli.
409Konflik invoice atau harga/kondisi produk berubah. Jangan otomatis menaikkan max_total tanpa persetujuan pelanggan.
422Perbaiki parameter, kupon, saldo atau stok sebelum retry.
429Tunggu sesuai Retry-After.
500 / 503 / timeoutGangguan atau layanan belum tersedia. Jika hasil pembelian belum pasti, rekonsiliasi dengan invoice_external yang sama sebelum keputusan pesanan/refund.

Respons error API biasanya memiliki status, success, msg dan message. error_code dan field tambahan hanya ada pada error tertentu. Kesalahan framework/routing, termasuk limiter IP, dapat hanya memiliki message. Utamakan kode HTTP; jangan bergantung pada pencocokan kalimat message.

401 — API key tidak valid
{
    "status": "error",
    "success": false,
    "msg": "API key tidak valid atau sudah kedaluwarsa.",
    "message": "API key tidak valid atau sudah kedaluwarsa."
}
404 — order tidak ditemukan
{
    "status": "error",
    "success": false,
    "msg": "Order tidak ditemukan untuk API key ini.",
    "message": "Order tidak ditemukan untuk API key ini."
}
422 — parameter wajib tidak diisi
{
    "status": "error",
    "success": false,
    "msg": "invoice_external wajib diisi agar retry pembelian tidak memotong saldo dua kali.",
    "message": "invoice_external wajib diisi agar retry pembelian tidak memotong saldo dua kali."
}
422 — saldo tidak cukup
{
    "status": "error",
    "success": false,
    "msg": "Saldo tidak mencukupi.",
    "message": "Saldo tidak mencukupi.",
    "required_balance": 25000,
    "balance": 10000
}
409 — harga melebihi max_total
{
    "status": "error",
    "success": false,
    "msg": "Harga melebihi max_total. Tunggu pembaruan webhook sebelum mengonfirmasi harga baru.",
    "message": "Harga melebihi max_total. Tunggu pembaruan webhook sebelum mengonfirmasi harga baru.",
    "error_code": "PRICE_LIMIT_EXCEEDED"
}
409 — invoice bertentangan
{
    "status": "error",
    "success": false,
    "msg": "invoice_external sudah pernah digunakan untuk order dengan detail yang berbeda. Gunakan invoice_external baru untuk order baru, atau kirim ulang detail yang sama untuk retry.",
    "message": "invoice_external sudah pernah digunakan untuk order dengan detail yang berbeda. Gunakan invoice_external baru untuk order baru, atau kirim ulang detail yang sama untuk retry.",
    "error_code": "INVOICE_EXTERNAL_CONFLICT",
    "existing_order": {
        "trans_id": "API-202610-EXAMPLE0001",
        "product_id": 101,
        "quantity": 1,
        "total_price": 25000
    },
    "requested_order": {
        "product_id": 102,
        "quantity": 1,
        "total_price": null
    }
}
422 — quantity melewati batas
{
    "status": "error",
    "success": false,
    "msg": "Jumlah pembelian API maksimal 1000 per transaksi.",
    "message": "Jumlah pembelian API maksimal 1000 per transaksi.",
    "error_code": "QUANTITY_LIMIT_EXCEEDED",
    "max_quantity": 1000
}
429 — kuota akun
{
    "status": "error",
    "success": false,
    "msg": "Terlalu banyak request. Silakan coba lagi setelah 60 detik.",
    "message": "Terlalu banyak request. Silakan coba lagi setelah 60 detik."
}
429 — kuota IP/framework
{
    "message": "Too Many Attempts."
}
500 — hasil pembelian perlu direkonsiliasi
{
    "status": "error",
    "success": false,
    "msg": "Gagal memproses transaksi. Silakan coba beberapa saat lagi.",
    "message": "Gagal memproses transaksi. Silakan coba beberapa saat lagi."
}
503 — layanan pembelian belum tersedia
{
    "status": "error",
    "success": false,
    "msg": "Pembelian API belum siap. Hubungi admin agar penyimpanan invoice eksternal diaktifkan.",
    "message": "Pembelian API belum siap. Hubungi admin agar penyimpanan invoice eksternal diaktifkan.",
    "error_code": "PUBLIC_API_NOT_READY"
}

Respons kesalahan jaringan tidak membuktikan pembelian gagal. Simpan invoice_external sebelum mengirim pembelian; jika respons hilang dan trans_id belum diketahui, kirim ulang pembelian dengan invoice, produk dan quantity yang sama. Jangan membuat invoice baru hanya untuk mengulang pesanan yang hasilnya belum pasti.