Lewati ke isi

Referensi API

Semua endpoint di halaman ini dipakai untuk mengirim dari sistem Anda sendiri — bukan untuk dashboard. Autentikasinya memakai API Key akun Anda.

Autentikasi

Kirim API Key di header Authorization. Kunci ini berbeda dari token sesi dashboard, dan tidak boleh ditempel di kode frontend karena siapa pun yang memegangnya bisa mengirim WhatsApp dari nomor Anda.

Authorization: Bearer <API_KEY_ANDA>
Content-Type: application/json

Nilai API Key dan Subdomain akun Anda bisa disalin dari Akun → tab API. Tipe Authorization yang sama juga dipakai sistem internal untuk kunci tenant, jadi jangan mengirim token dashboard ke jalur ini.

Mana saja yang menerima API Key

Halaman ini hanya memuat endpoint yang bisa dipanggil dengan API Key. Sisanya milik dashboard dan dijaga sesi login:

Endpoint API Key
POST pengiriman — send-text, send-otp, send-button, send-list, blast ya
GET /send/api/history, /send/api/status, /send/api/wallet, /send/api/wallet/ledger ya
POST /send/api/auth/magic/link ya — lihat Tautan Masuk
GET /send/api/webhook, GET /send/api/ip-allowlist ya — baca saja
POST /send/api/webhook, POST /send/api/webhook/test, POST /send/api/ip-allowlist tidak — sengaja, lihat Webhook Keluar
GET /send/api/templates tidak
GET /send/api/waha/session, GET /send/qr/status tidak
POST /send/api/wallet/topup tidak — butuh browser
POST /send/api/client/rotate-key tidak — sengaja, lihat bagiannya di bawah

Yang tidak menerima API Key menjawab 401 dengan code: no_session. Kalau Anda butuh daftar template atau kondisi perangkat, ambil dari GET /send/api/status.

Pengecualiannya baris yang ditandai "butuh browser" dan "sengaja": tiga endpoint penulisan webhook/daftar-IP dan wallet/topup menjawab 403 + code: session_required. Bedanya disengaja — 403 berarti "kunci Anda sah, hanya endpoint ini bukan untuk mesin", dan itu jawaban yang lebih berguna bagi integrasi daripada 401.

no_session juga berarti “kunci tidak dikenali”

Kunci yang salah tulis, sudah di-rotate, atau milik klien yang tidak aktif sama-sama dijawab 401 + code: no_session — bukan kode khusus. Jadi no_session di sini artinya tidak ada kredensial yang sah, bukan “Anda butuh login”. Saat integrasi menabrak kode ini, periksa dulu nilai header Authorization Anda sebelum menyimpulkan masalah sesi.

Akses API adalah fitur paket

API key hanya bisa dipakai pada paket yang mencantumkan akses API (Starter dan Bisnis). Pada paket yang tidak termasuk, permintaan dengan API key ditolak 403 dengan code: api_access_required — termasuk bila dashboard Anda sendiri masih bisa dipakai lewat sesi login. Tab API tetap menampilkan kredensialnya agar Anda bisa menyiapkan integrasi sebelum menaikkan paket.

API key itu rahasia

Kalau kunci Anda bocor, ganti sendiri lewat Akun → tab API → Ganti API Key. Kunci lama langsung berhenti bekerja, jadi sistem yang masih memakainya harus segera diperbarui. Kunci yang bocor memungkinkan orang lain memakai kuota Anda.

Base URL

Seluruh jalur di bawah ini relatif terhadap domain layanan:

https://send.yolked.id

Endpoint

POST /send/api/send-text

Kirim satu pesan teks. Ini endpoint yang paling sering dipakai untuk notifikasi transaksional.

curl -X POST https://send.yolked.id/send/api/send-text \
  -H "Authorization: Bearer <API_KEY_ANDA>" \
  -H "Content-Type: application/json" \
  -d '{"phone":"6281234567890","text":"Halo dari API"}'
Field Tipe Wajib Keterangan
phone string ya Nomor format internasional tanpa +, tanpa spasi
text string ya Isi pesan; mendukung {name}, {phone}, {otp}
scheduled_at string tidak Waktu ISO 8601; kalau diisi pesan masuk status scheduled

Balasan sukses — status bernilai sent atau failed, jadi periksa nilainya, bukan hanya kode HTTP-nya:

{ "status": "sent", "message_id": 1234, "to": "6281234567890" }

{otp} menghasilkan kode sungguhan

Kalau text memuat {otp}, server membuat kode 6 digit dan mengisinya, lalu menambahkan field otp pada balasan supaya backend Anda bisa memverifikasinya sendiri. Pesan seperti ini dihitung sebagai satu pesan teks biasa, bukan pesan OTP. Kalau {otp} tidak dipakai, field otp tidak muncul dan bentuk balasannya tidak berubah.

Field otp hanya untuk komunikasi server ke server. Jangan pernah meneruskannya ke browser atau menyimpannya di log.

POST /send/api/send-otp

Buat dan kirim kode OTP. Kodenya dibuat di sisi server dan dicatat sebagai pesan bertipe otp, sehingga kuota OTP yang terpakai — bukan kuota pesan biasa.

Field Tipe Wajib Keterangan
phone string ya Nomor tujuan
template string tidak Nama template OTP; bawaannya otp-default
otp_length integer tidak Panjang kode, bawaan 6; nilai di luar 4–8 dipangkas
copy_button boolean tidak Kirim sebagai pesan tombol dengan tombol salin
idempotency_key string tidak Kunci anti-pengiriman ganda; bisa juga lewat header

Template yang tidak ditemukan tidak menggagalkan pengiriman

Kalau nama di template tidak ada di akun Anda, server tetap mengirim — isinya diganti teks bawaan Yolked (Kode OTP Anda: …) dan balasannya tetap 200 dengan status: sent. Jadi jangan memakai 404 untuk mendeteksi salah tulis nama template. Periksa daftar templates dari GET /send/api/status lebih dulu kalau isi pesan harus persis seperti yang Anda rancang.

OTP tidak bisa dijadwalkan

Endpoint ini menolak scheduled_at dengan 400 + code: otp_not_schedulable. Alasannya dua: kode yang dibuat sekarang lalu dikirim dua jam kemudian adalah kredensial yang jauh lebih lemah, dan backend Anda juga tidak akan pernah menerima kodenya untuk disimpan — jadi tidak ada yang bisa memverifikasinya. Kirim OTP tanpa scheduled_at. Untuk pesan yang memang perlu dijadwalkan, pakai send-text, send-button, send-list, atau blast.

curl -X POST https://send.yolked.id/send/api/send-otp \
  -H "Authorization: Bearer <API_KEY_ANDA>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: otp-9f2c1a" \
  -d '{"phone":"6281234567890","otp_length":6}'

Balasan sukses menyertakan kodenya supaya backend Anda bisa menyimpannya dan membandingkannya sendiri:

{ "status": "sent", "otp": "481920", "message_id": 1234, "to": "6281234567890" }

otp hanya untuk backend Anda

Yolked membuat dan mengirim kode; Yolked tidak memverifikasinya. Verifikasi adalah tanggung jawab sistem Anda, jadi field otp hanya boleh dibaca di server. Mengirimkannya ke browser, aplikasi mobile, atau log sama saja dengan menyerahkan kode itu kepada pihak yang justru ingin dikecualikan oleh OTP. Pola yang disarankan ada di Panduan Integrasi.

Rate limit

Untuk melindungi nomor penerima sekaligus kuota akun Anda, endpoint ini dibatasi:

  • 1 OTP per 60 detik untuk nomor yang sama.
  • Maksimum 3 OTP per 15 menit untuk nomor yang sama.

62812…, +62812…, dan 0812… dihitung sebagai satu nomor, jadi mengubah format penulisan tidak menembus batas ini. Hanya pengiriman yang benar-benar terkirim yang dihitung — kalau pengiriman gagal karena gangguan, Anda tetap bisa mencoba lagi nomor yang sama.

Pelanggaran mengembalikan 429 beserta header Retry-After:

{
  "error": "Terlalu banyak permintaan OTP untuk nomor ini. Coba lagi dalam 47 detik.",
  "code": "otp_rate_limited",
  "reason": "cooldown",
  "retry_after": 47
}

Idempotency

Kirim Idempotency-Key (header) atau idempotency_key (body) yang unik untuk setiap permintaan. Kalau permintaan yang sama diulang dalam 15 menit, server tidak mengirim pesan kedua dan tidak memotong kuota lagi — balasan asli dikembalikan apa adanya, ditandai idempotent_replay: true dan header Idempotent-Replay: true.

Ini penting karena pengiriman bersifat sinkron dengan batas tunggu 15 detik: klien yang kehabisan waktu lalu mencoba ulang tanpa kunci idempotency akan membuat penerima mendapat dua kode dan kuota Anda terpotong dua kali.

{
  "status": "sent",
  "otp": "481920",
  "message_id": 1234,
  "to": "6281234567890",
  "idempotent_replay": true
}

Pakai kunci yang benar-benar unik

Kunci idempotency yang dipakai ulang untuk nomor berbeda akan mengembalikan balasan lama, bukan mengirim pesan baru. Bentuk yang aman: gabungan id transaksi dan nomor, misalnya otp-order-7781-6281234567890.

Kapasitas penampung batas

Kedua catatan di atas (stempel rate limit dan kunci idempotency) disimpan per API key, di dalam satu baris config berbentuk JSON. Artinya ada batas kapasitas, dan batas itu dihitung untuk satu usaha, bukan untuk jaringan cabang:

Penampung Bawaan Isi
Rentang jendela rate limit 2.000 nomor Nomor yang dikirimi OTP dalam 15 menit terakhir
Kunci idempotency 1.000 kunci Kunci yang masih bisa dijawab ulang dalam 15 menit

Urutan pembuangannya disengaja. Entri yang sudah kedaluwarsa dibuang lebih dulu — entri seperti itu tidak bisa lagi menolak apa pun, jadi membuangnya tidak mengubah satu pun keputusan. Baru kalau nomor yang benar-benar aktif dalam satu jendela melebihi kapasitas, server terpaksa membuang entri yang masih hidup (nomor yang paling lama tidak dipakai), dan mencatatnya di log:

[send] warning: OTP rate-limit store is over capacity (2140 numbers active
within the window, cap 2000); dropping the 140 least recently used. ...

Konsekuensinya harus dipahami: selama kapasitas cukup, batas 3 OTP per 15 menit per nomor berlaku mutlak. Kalau kapasitas terlampaui, nomor yang dibuang bisa dikirimi melebihi batas itu, dan kunci idempotency yang dibuang membuat percobaan ulang mengirim — dan menagih — OTP kedua.

Untuk operator

Dua variabel lingkungan berikut menaikkan kapasitas dan dibaca sekali saat aplikasi dijalankan:

Variabel Bawaan
SEND_OTP_RL_PHONES_MAX 2000
SEND_OTP_IDEM_MAX 1000

Nilainya harus bilangan bulat positif; nilai yang salah ketik dilaporkan di log dan diabaikan, tidak menggagalkan proses boot. Ukurannya sebaiknya sebesar jumlah nomor berbeda dalam sehari untuk klien itu.

Perhatikan harga menaikkannya: seluruh isi penampung ditulis ulang setiap kali OTP terkirim, jadi kapasitas besar berarti tulisan besar setiap saat.

Kalau satu API key dipakai untuk banyak cabang, memisahkan API key per cabang lebih baik daripada menaikkan kapasitas: batas 3-per-15-menit lalu berlaku di dalam tiap cabang, dan pemakaian kuota tiap cabang terbaca terpisah di halaman Riwayat.

POST /send/api/send-button

Kirim pesan dengan tombol. Cocok untuk konfirmasi atau menu sederhana.

curl -X POST https://send.yolked.id/send/api/send-button \
  -H "Authorization: Bearer <API_KEY_ANDA>" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "6281234567890",
    "text": "Pesanan Anda sudah siap.",
    "title": "Status Pesanan",
    "footer": "Yolked",
    "buttons": [{"text": "Lacak"}]
  }'

Alih-alih menulis seluruh isi pesan, Anda bisa mengirim template_id (ID template yang sudah dibuat di dashboard) dan server akan mengambil text, footer, serta buttons dari template itu. Field yang Anda kirim tetap menang atas nilai dari template.

Field Tipe Wajib Keterangan
phone string ya Nomor tujuan
text string tidak Isi pesan; boleh kosong bila template_id diisi
title string tidak Judul di atas isi pesan
footer string tidak Teks kecil di bawah pesan
buttons array tidak Daftar tombol
template_id integer tidak Ambil isi dari template ini
scheduled_at string tidak Waktu ISO 8601

POST /send/api/send-list

Kirim list message — pesan dengan daftar pilihan yang bisa dibuka penerima.

Field Tipe Wajib Keterangan
phone string ya Nomor tujuan
text string ya Kalimat pembuka
title string tidak Judul pesan
footer string tidak Teks kaki pesan
button_text string tidak Label tombol pembuka daftar
sections array tidak Bagian dan baris pilihannya
template_id integer tidak Ambil isi dari template ini
scheduled_at string tidak Waktu ISO 8601

POST /send/api/blast

Kirim pesan yang sama ke banyak nomor sekaligus. Nomor dikirim sebagai larik, dan hasilnya dikelompokkan dalam satu batch_id sehingga bisa dipantau sebagai satu siklus pengiriman.

curl -X POST https://send.yolked.id/send/api/blast \
  -H "Authorization: Bearer <API_KEY_ANDA>" \
  -H "Content-Type: application/json" \
  -d '{"phones":["6281234567890","6281234567891"],"text":"Promo bulan ini"}'
Field Tipe Wajib Keterangan
phones array ya Daftar nomor tujuan; tidak boleh kosong
text string ya* Isi pesan
template string ya* Nama template tipe blast
cycle_name string tidak Nama siklus, untuk pengelompokan di dashboard
group_filter string tidak Batasi ke satu grup kontak
scheduled_at string tidak Waktu ISO 8601

* Salah satu dari text atau template wajib ada. Kalau template diisi, isinya diambil dari template tersebut.

Blast terjadwal menolak kelebihan kuota

Pengiriman blast yang dijadwalkan harus masuk kuota paket. Kalau kuotanya habis, permintaan ditolak dengan code: quota_exhausted. Kirim tanpa scheduled_at kalau ingin memakai saldo E-Wallet untuk kelebihannya.

GET /send/api/history

Ambil riwayat pengiriman akun Anda, terbaru lebih dulu.

Query Tipe Bawaan Keterangan
limit integer 50 Jumlah maksimum baris yang dikembalikan
type string kosong Filter tipe pesan, misalnya otp, text, atau blast
curl -H "Authorization: Bearer <API_KEY_ANDA>" \
  "https://send.yolked.id/send/api/history?limit=20&type=otp"

Balasannya larik objek pesan:

[
  {
    "id": 1234,
    "type": "text",
    "to": "6281234567890",
    "message": "Halo dari API",
    "status": "sent",
    "created_at": "2026-09-22T04:15:02",
    "batch_id": "",
    "delivered_at": null,
    "read_at": null,
    "replied_at": null,
    "reply_message": ""
  }
]

GET /send/api/status

Mulai dari sini. Satu panggilan yang menjawab "apakah saya boleh mengirim sekarang?", tanpa perlu dashboard. Cocok untuk tombol Uji koneksi di halaman pengaturan Anda, atau untuk pemeriksaan berkala sebelum mengirim.

curl -H "Authorization: Bearer <API_KEY_ANDA>" \
  "https://send.yolked.id/send/api/status"
{
  "status": "ok",
  "server_time": "2026-09-22T04:15:02Z",
  "client": { "subdomain": "toko-anda", "status": "active", "expires_at": "2026-10-01T00:00:00" },
  "plan": { "name": "Starter", "slug": "starter", "api_access": true, "webhook": false, "custom_sender": false },
  "api_access": true,
  "device": {
    "session": "toko-anda",
    "status": "WORKING",
    "connected": true,
    "reachable": true,
    "number": "6281234567890",
    "push_name": "Toko Anda"
  },
  "quota": {
    "otp":   { "used": 120, "limit": 1000, "remaining": 880 },
    "text":  { "used": 40,  "limit": 1000, "remaining": 960 },
    "blast": { "used": 0,   "limit": 500,  "remaining": 500 }
  },
  "wallet": { "balance": 50000, "currency": "IDR" },
  "templates": [{ "name": "otp-default", "type": "otp", "has_buttons": false }],
  "webhook": { "allowed": true, "configured": true, "events": ["message.sent", "message.failed"], "recent_failures": 0 },
  "ip_allowlist": { "enforced": true, "entries": 2, "current_ip": "203.0.113.10" },
  "pos": {
    "connected": true,
    "app": "POS Yolked",
    "client_id": "pos.yolked.id",
    "connected_at": "2026-09-24T06:00:00Z",
    "last_exchange_at": "2026-09-24T06:00:00Z",
    "exchanges": 1,
    "last_seen_at": "2026-09-24T06:12:00Z",
    "active": true,
    "stale": false
  },
  "warnings": []
}
Field Keterangan
server_time Waktu server dalam UTC. Pakai untuk mengukur selisih jam mesin Anda — jendela OTP dan pembatasan laju diukur memakai jam ini
api_access false berarti paket tidak mencantumkan akses API; endpoint pengiriman akan menolak dengan 403
device.connected true hanya kalau sesi WhatsApp berstatus WORKING. Kalau false, pesan akan tercatat failed walaupun balasannya 200
device.reachable false berarti WAHA tidak bisa dihubungi dari server, jadi status belum tentu mencerminkan keadaan sebenarnya
device.number Nomor WhatsApp yang tersambung, tanpa akhiran @c.us
quota.<jenis>.remaining null berarti kuota tidak dibatasi; angka 0 berarti kuota jenis itu benar-benar habis
templates Template yang benar-benar ada di akun Anda — pakai daftar ini untuk memvalidasi nama template sebelum mengirim
webhook.allowed false berarti paket tidak mencantumkan fitur Webhook
webhook.configured true hanya kalau URL dan secret webhook sudah tersimpan
webhook.events Event yang benar-benar dikirim; daftar kosong berarti semua event
webhook.recent_failures Berapa dari 8 pengiriman terakhir yang gagal — angka selain 0 berarti masalah ada di sisi penerima
ip_allowlist.enforced true berarti pembatasan IP aktif untuk API key
ip_allowlist.entries Jumlah entri di daftar
ip_allowlist.current_ip Alamat yang kami lihat untuk permintaan ini. Bandingkan dengan daftarnya — inilah cara mendiagnosis ip_not_allowed tanpa membuka browser
pos.connected true berarti akun ini pernah menukar kode otorisasi POS menjadi API key — ada aplikasi POS yang memegang kunci akun ini
pos.app Nama aplikasi yang menyambung, seperti yang tertulis di halaman persetujuan saat koneksi dibuat
pos.client_id Identitas client_id yang dipakai saat menukar kode
pos.exchanges Berapa kali koneksi diselesaikan. Bertambah kalau POS menyambung ulang; tanggal connected_at tetap yang pertama
pos.last_exchange_at Kapan penukaran kode terakhir berhasil
pos.last_seen_at Panggilan API terakhir yang memakai API key akun ini — null berarti belum pernah
pos.active true berarti tersambung, kuncinya masih yang berlaku, dan ada panggilan dalam 24 jam terakhir
pos.stale true berarti kunci yang dipegang POS sudah tidak berlaku, biasanya karena rotate key di dashboard. Sambungkan ulang dari sisi POS; tidak ada yang perlu diubah di sini
warnings Daftar masalah yang perlu ditindaklanjuti, lihat di bawah

Tambah ?live=0 untuk melewati pemeriksaan ke WhatsApp dan hanya melaporkan status terakhir yang tercatat. Lebih cepat, tapi device.connected bisa sudah tidak akurat — jangan pakai untuk memutuskan pengiriman.

Kode di dalam warnings sengaja sama dengan kode error pada endpoint pengiriman, jadi satu penanganan error bisa dipakai untuk keduanya:

Kode Arti
api_access_required Paket tidak mencantumkan akses API
device_offline Sesi WhatsApp belum WORKING; pindai QR di dashboard
insufficient_balance Ada jenis pesan yang kuotanya sudah 0 dan saldo E-Wallet juga habis
webhook_unconfigured Paket mencantumkan fitur Webhook tapi URL webhook belum diisi — event tidak akan sampai ke mana pun

Periksa ini sebelum mengirim massal

Pola yang disarankan: panggil /send/api/status, hentikan proses kalau warnings tidak kosong, baru kirim. Ini mencegah ribuan pesan tercatat failed gara-gara sesi WhatsApp terputus di tengah malam.

GET /send/api/wallet

Saldo E-Wallet akun Anda, dipakai untuk kelebihan pengiriman di luar kuota.

curl -H "Authorization: Bearer <API_KEY_ANDA>" \
  "https://send.yolked.id/send/api/wallet"

Bentuk balasannya mengikuti halaman E-Wallet di dashboard: balance beserta ringkasan pemakaian. Mutasinya ada di GET /send/api/wallet/ledger.

Top up tidak bisa dari API

POST /send/api/wallet/topup harus diselesaikan di browser karena berakhir di halaman pembayaran Doku. Kalau dipanggil dengan API key, balasannya 403 + code: session_required — bukan 401 — supaya jelas bahwa kuncinya benar dan hanya jalurnya yang tidak sesuai.

POST /send/api/client/rotate-key

Buat API key baru untuk akun Anda dan matikan yang lama.

Ini endpoint dashboard, bukan endpoint integrasi

Jalur ini sengaja menolak API key dan hanya menerima sesi login browser. Alasannya keamanan: kunci yang bocor harus mustahil dipakai untuk membuat penggantinya sendiri. Di dashboard, tombolnya ada di Akun → tab API → Ganti API Key.

Field Tipe Wajib Keterangan
confirm boolean ya Harus true; tanpa ini balasannya 400 + code: confirmation_required

Balasan sukses:

{
  "status": "ok",
  "api_key": "send_xxxxxxxxxxxxxxxx",
  "subdomain": "toko-anda",
  "rotated_at": "2026-09-22T04:15:02Z",
  "previous_key_prefix": "send_A1"
}
Field Keterangan
api_key Kunci baru. Ditampilkan sekali di sini dan di dashboard; simpan langsung
rotated_at Waktu penggantian, UTC
previous_key_prefix Enam karakter pertama kunci lama, untuk jejak audit — bukan kunci lama

Tidak ada masa tenggang: kunci lama berhenti berlaku begitu permintaan ini selesai. Perbarui konfigurasi integrasi Anda, lalu uji dengan GET /send/api/status sebelum mengirim sungguhan.

POST /send/api/auth/magic/link

Kirim tautan masuk sekali pakai ke email salah satu pengguna aplikasi Anda — tanpa password, dan tanpa kode yang harus diketik. Cocok untuk kasir yang perlu membuka POS, atau pengguna aplikasi Anda yang sudah Anda autentikasi sendiri dan ingin Anda pindahkan ke sesi Send.

Balasannya tidak pernah memuat tautannya. Tautan itu kredensial: kalau ikut dikembalikan, siapa pun yang memegang API key Anda bisa masuk ke akun mana pun yang alamat emailnya ditebak. Tautannya hanya dikirim ke email penerimanya.

curl -X POST "https://send.yolked.id/send/api/auth/magic/link" \
  -H "Authorization: Bearer <API_KEY_ANDA>" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "kasir@toko-anda.com",
    "app": "POS Toko Anda",
    "redirect_uri": "https://pos.toko-anda.com/auth/send/callback",
    "state": "sesi-8f21"
  }'
Field Tipe Wajib Keterangan
email string ya Alamat penerima — ini yang menentukan siapa yang masuk
name string tidak Nama untuk akun yang baru dibuat; kalau kosong dipakai bagian sebelum @
phone string tidak Nomor untuk akun yang baru dibuat
app string tidak Nama aplikasi Anda. Muncul di halaman konfirmasi dan di email supaya penerima tahu tautannya dari mana. Kalau kosong dipakai nama klien
redirect_uri string tidak Ke mana penerima dikembalikan setelah tautan dipakai. Harus terdaftar lebih dulu — lihat di bawah
state string tidak Nilai yang ingin Anda terima kembali; dikembalikan apa adanya bersama token
create_account boolean tidak Default true: akun dibuat kalau alamatnya belum ada. Kirim false kalau Anda hanya mau melayani pengguna yang sudah terdaftar

Balasan sukses — perhatikan tidak ada url maupun token di dalamnya:

{
  "status": "ok",
  "email": "kasir@toko-anda.com",
  "created": true,
  "expires_in_minutes": 15
}
Field Keterangan
created true kalau akunnya baru dibuat oleh panggilan ini
expires_in_minutes Masa berlaku tautan. Diatur admin di dashboard, default 15 menit

create_account: false tidak membocorkan siapa yang sudah terdaftar

Kalau alamatnya belum punya akun dan create_account bernilai false, balasannya sama dengan sukses biasa meskipun tidak ada email yang terkirim. Kalau kedua kasus dibedakan, endpoint ini bisa dipakai menebak alamat email mana yang sudah terdaftar.

redirect_uri dan daftar izin

redirect_uri hanya diizinkan kalau terdaftar di variabel lingkungan SEND_MAGIC_REDIRECT_URIS di server (dipisah koma), atau kalau hostnya localhost / 127.0.0.1 — yang terakhir supaya pengembangan lokal bisa jalan tanpa konfigurasi tambahan. Selama belum terdaftar, permintaan yang menyertakan redirect_uri dijawab 400 + code: invalid_redirect_uri.

Daftar izin ini ada karena redirect_uri yang diterima begitu saja adalah celah pengalihan terbuka: penyerang bisa minta tautan resmi kami yang, setelah dipakai, melempar token sesi ke domainnya sendiri.

Kalau redirect_uri tidak diisi, penerima hanya melihat halaman konfirmasi Send dan berakhir di dashboard Send — bukan di aplikasi Anda.

Batas pengiriman

Sama seperti jalur OTP: 3 permintaan per 10 menit untuk alamat yang sama, dan jeda 60 detik sebelum boleh minta lagi. Melewatinya dijawab 429 + code: magic_rate_limited, atau magic_cooldown khusus untuk jeda 60 detik.

Jumlah tautan aktif dibatasi per akun, dan tautan lama tidak dibatalkan saat Anda minta yang baru: setiap tautan berdiri sendiri sampai dipakai atau kedaluwarsa.

Memakai tautannya

Tautannya berbentuk https://send.yolked.id/send/magic-login?t=<token>. Membuka alamat itu belum memakai tokennya — halaman yang muncul hanya meminta satu konfirmasi, lalu mengirim POST dari halaman itu juga. Pemisahan ini disengaja: pemindai tautan di email korporat (Outlook SafeLinks dan sejenisnya) membuka setiap URL di kotak masuk lebih dulu, jadi tautan yang habis begitu dibuka akan selalu mati sebelum pemiliknya sempat mengklik.

Setelah dipakai, tautannya langsung tidak berlaku — termasuk untuk penekanan kedua di perangkat yang sama. Yang diterima pengguna adalah sesi Send biasa: token yang bentuknya sama dengan hasil login, dan bisa dipakai di mana saja seperti token dari POST /send/api/auth/otp/verify.

Kalau redirect_uri diisi, penerima diarahkan ke sana dengan token (dan state bila Anda mengirimnya) ditambahkan ke query string-nya. Simpan token itu seperti Anda menyimpan token hasil login — lihat Panduan Integrasi untuk contoh lengkapnya.

Endpoint ini juga menerima sesi login pemilik klien sebagai ganti API key. Cabang sesi diperiksa lewat menu Akun milik pengguna itu, bukan lewat kolom akses API pada paketnya — jadi pemilik klien bisa memakainya dari dashboard walaupun paketnya tidak mencantumkan akses API. Tanpa kredensial yang dikenali, balasannya 403 + client_required.

Webhook Keluar

Kalau Anda tidak mau menebak status pesan dengan polling GET /send/api/history, aktifkan webhook: kami yang memanggil server Anda setiap kali status sebuah pesan berubah. Arahnya terbalik dari seluruh endpoint di halaman ini — di sini Anda yang menyediakan URL, kami yang jadi klien HTTP.

Webhook adalah fitur paket (Bisnis dan ke atas). Di paket yang tidak mencantumkannya, GET /send/api/status menjawab "webhook": {"allowed": false} dan tidak ada event yang dikirim.

Event yang tersedia

Event Dikirim saat
message.sent Pesan diterima WhatsApp dan mulai diproses
message.delivered WhatsApp melaporkan pesan sampai ke perangkat penerima
message.read Penerima membaca pesan (hanya kalau pengaturannya aktif di ponselnya)
message.failed Pengiriman gagal — termasuk saat sesi WhatsApp sedang tidak WORKING
message.incoming Ada pesan masuk ke nomor Anda. Satu-satunya event yang memuat isi pesan
blast.completed Satu batch blast selesai diproses. Diringkas per batch, bukan per nomor
ping Hanya dari POST /send/api/webhook/test. Tidak pernah dikirim otomatis

Pilih event mana saja yang ingin dikirim dari dashboard. Daftar kosong berarti semua event dikirim. blast.completed sengaja satu event per batch supaya blast 5.000 nomor tidak berubah menjadi 5.000 permintaan ke server Anda.

Bentuk permintaan

POST ke URL Anda, Content-Type: application/json, badan berupa JSON padat (tanpa spasi) dengan urutan field tetap:

POST /hooks/yolked-send HTTP/1.1
Content-Type: application/json
User-Agent: YolkedSend-Webhook/1.0
X-Send-Event: message.sent
X-Send-Delivery: 9f1c4a8e2b7d4f0c8a3e6b1d5c9f2a7e
X-Send-Attempt: 1
X-Send-Timestamp: 1789000000
X-Send-Signature: sha256=5c1f...e29a
{"delivery_id":"9f1c4a8e2b7d4f0c8a3e6b1d5c9f2a7e","event":"message.sent","created_at":"2026-09-22T04:15:02Z","data":{"message_id":4821,"type":"text","to":"6281234567890","status":"sent","created_at":"2026-09-22T04:15:02Z","delivered_at":null,"read_at":null,"error":""}}
Header Keterangan
X-Send-Event Nama event. Sama dengan event di badan
X-Send-Delivery ID unik percobaan pengiriman ini. Pakai ini sebagai kunci idempotensi — percobaan ulang memakai ID yang sama
X-Send-Attempt Nomor percobaan, mulai dari 1
X-Send-Timestamp Unix time (detik) saat permintaan dibuat
X-Send-Signature Tanda tangan, lihat di bawah

blast.completed membawa data yang berbeda — {"batch_id", "cycle_name", "group_filter", "total", "sent", "failed"}. Pakai batch_id untuk mengaitkannya dengan hasil POST /send/api/blast.

Isi pesan tidak pernah dikirim lewat webhook

Payload hanya memuat metadata: nomor, tipe, status, waktu, dan pesan error. Satu-satunya pengecualian adalah message.incoming, yang memang membawa isi balasan pelanggan di data.message (dipotong 4.096 karakter) — karena itu gunanya. Kalau Anda butuh isi pesan keluar, simpan sendiri saat mengirim.

Memverifikasi tanda tangan

Setiap permintaan ditandatangani dengan HMAC-SHA256 memakai secret akun Anda. Secret dibuat otomatis saat URL webhook pertama kali disimpan dan hanya ditampilkan satu kali di dashboard — kalau hilang, ganti dengan Akun → tab API → Ganti secret.

Teks yang ditandatangani adalah <X-Send-Timestamp> + "." + badan permintaan mentah, bukan JSON yang sudah di-parse:

signature = "sha256=" + hex(HMAC_SHA256(secret, timestamp + "." + raw_body))
import hashlib, hmac

def verify(secret: str, raw_body: bytes, timestamp: str, signature: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Tiga hal yang gampang salah di sini:

  1. Pakai badan mentah. Tanda tangan dihitung atas byte yang dikirim. Kalau middleware Anda mengubah JSON lebih dulu (re-serialisasi, normalisasi spasi), verifikasinya gagal. Baca request.get_data() sebelum di-parse.
  2. Timestamp ikut ditandatangani, bukan hanya ditempel di sebelahnya. Jadi batas umur yang Anda terapkan tidak bisa dilewati dengan memindahkan timestamp baru ke badan lama — mengubahnya merusak digest.
  3. Bandingkan dengan compare_digest, bukan ==, supaya tidak ada kebocoran waktu.

Tolak permintaan yang umurnya lebih dari beberapa menit, dan simpan X-Send-Delivery yang sudah diproses supaya percobaan ulang tidak menghasilkan pesan ganda di sistem Anda.

Kebijakan percobaan ulang

Balasan HTTP 2xx dianggap berhasil. Selain itu (termasuk 3xx) dianggap gagal dan dicoba lagi:

Percobaan Jeda sebelum percobaan berikutnya
1 2 detik
2 8 detik
3 menyerah

Timeout setiap percobaan 5 detik untuk koneksi dan 10 detik untuk keseluruhan, redirect tidak diikuti (3xx dihitung gagal). Jadi satu event paling lama memakan sekitar 25 detik sebelum dinyatakan gagal. Balas 200 secepat mungkin dan kerjakan sisanya di latar belakang.

Delapan pengiriman terakhir — berhasil maupun gagal — terlihat di Akun → tab API, lengkap dengan kode HTTP, jumlah percobaan, dan pesan errornya. Ini alat pertama untuk menjawab "kok webhook saya tidak masuk?".

Webhook hanya boleh menuju alamat publik

Server kami yang melakukan permintaan, jadi URL diarahkan ke alamat internal akan berubah menjadi cara mengetuk jaringan kami sendiri: http://127.0.0.1:5432, alamat metadata cloud, port admin. Karena itu hanya http:// dan https:// ke nama host yang semua alamatnya publik yang diterima, tanpa kredensial di URL (user:pass@host ditolak). Nama host diperiksa ulang sebelum setiap pengiriman, jadi domain yang tadinya publik lalu menunjuk ke IP privat tetap ditolak. Kalau integrator Anda memang berada di jaringan privat yang sama, hanya administrator server yang bisa mengendurkan aturan ini lewat variabel lingkungan SEND_WEBHOOK_ALLOW_PRIVATE=1 — dan itu berlaku untuk semua tenant, bukan untuk satu akun.

GET /send/api/webhook

Lihat target webhook dan riwayat pengiriman terakhir. Bisa dipanggil dengan API key supaya integrasi bisa memastikan sendiri URL yang terpasang memang URL-nya — ketidakcocokan di sini biasanya hanya terlihat sebagai "webhook tidak pernah datang".

curl -H "Authorization: Bearer <API_KEY_ANDA>" \
  "https://send.yolked.id/send/api/webhook"
{
  "status": "ok",
  "allowed": true,
  "enabled": true,
  "url": "https://pos.contoh.id/hooks/yolked-send",
  "events": ["message.sent", "message.failed", "message.incoming"],
  "available_events": ["message.sent", "message.delivered", "message.read", "message.failed", "message.incoming", "blast.completed"],
  "secret_set": true,
  "secret_hint": "K7pQ2vXa...",
  "updated_at": "2026-09-22T04:15:02Z",
  "recent": [
    { "at": "2026-09-22T04:16:10Z", "event": "message.sent", "delivery_id": "9f1c4a8e...", "ok": true, "status": 200, "attempts": 1, "error": "" }
  ]
}

secret_hint hanya 8 karakter pertama — cukup untuk memastikan secret mana yang tersimpan di POS Anda, tidak cukup untuk menjadi secretnya. secret_set adalah satu-satunya cara memeriksa keberadaan secretnya dari integrasi.

POST /send/api/webhook

Simpan URL dan event yang dikirim. Sesi browser saja — lihat alasannya di Pembatasan IP, alasannya sama.

Field Tipe Keterangan
url string Wajib. Maksimal 500 karakter, harus lolos pemeriksaan alamat publik
enabled boolean Kalau false, pengaturan tetap tersimpan tapi tidak ada event yang dikirim
events array string Nama event yang diinginkan. Dihilangkan berarti semua; kosong berarti 400
rotate_secret boolean true untuk membuat secret baru, membatalkan yang lama seketika

Balasan sukses berbentuk sama dengan GET, dengan tambahan secret (nilai utuh, ditampilkan sekali ini saja) dan secret_created: true hanya pada penyimpanan yang membuat secret baru. Setelah itu secret tidak pernah muncul lagi.

POST /send/api/webhook/test

Kirim satu event ping ke URL yang tersimpan dan laporkan apa yang dijawab endpoint Anda. Sengaja sinkron — uji coba yang hasilnya baru terlihat di log satu jam kemudian bukan uji coba. Balasannya 200 kalau endpoint Anda menjawab 2xx dan 502 kalau tidak, keduanya dengan bentuk:

{ "status": "ok", "ok": true, "http_status": 200, "error": "", "delivery_id": "…", "url": "https://pos.contoh.id/hooks/yolked-send" }

Hasil uji tidak ditulis ke riwayat pengiriman, supaya riwayat tetap bisa dipakai menjawab pertanyaan tentang lalu lintas sungguhan.

Pembatasan IP

Kalau API key Anda hanya dipakai dari satu server, mengikatnya ke alamat IP itu membuat kunci yang bocor tidak ada gunanya bagi pencurinya. Ini lapisan kedua setelah rencana "ganti kunci kalau bocor", untuk kasus ketika kebocoran baru diketahui belakangan.

Hanya berlaku untuk API key — dashboard tidak pernah dibatasi

Sesi login browser selalu lolos. Itu bukan kelalaian: kalau Anda mengunci diri sendiri keluar (IP kantor berubah, salah ketik/CIDR, server pindah), Anda masih bisa masuk lewat browser dan memperbaiki daftarnya. Karena alasan yang sama, mengubah daftar ini hanya bisa dari sesi browser — kalau API key boleh menulis ke daftarnya sendiri, kunci yang bocor tinggal menambahkan alamat pencurinya dan pembatasannya tidak menjaga apa pun.

Alamat yang tidak diizinkan dijawab:

{ "error": "…", "code": "ip_not_allowed", "ip": "203.0.113.10" }

dengan status 403. Kode ini berbeda dari no_session: artinya kredensial Anda dikenali dan sah, hanya saja dipakai dari alamat yang tidak ada di daftar. Bedakan keduanya di penanganan error integrasi Anda — no_session berarti perbaiki kuncinya, ip_not_allowed berarti perbaiki atau perluas daftarnya.

Tidak ada endpoint API yang dikecualikan

Termasuk endpoint yang hanya membaca, GET /send/api/ip-allowlist dan GET /send/api/status — keduanya juga menjawab 403 + ip_not_allowed dari alamat yang tidak ada di daftar. Itu disengaja: pemeriksaannya berada di satu dekorator yang dilalui semua endpoint API key, dan daftar pengecualian adalah hal yang gampang terlupa diperbarui.

Yang dibutuhkan integrasi tetap tersedia, karena balasan penolakannya sendiri membawa code dan ip — "kunci Anda ditolak karena permintaan datang dari X". Daftar lengkapnya bisa dibaca dari dashboard, dan GET /send/api/status di sisi browser mencantumkan jumlah entri serta apakah alamat Anda lolos.

Dari alamat mana IP dinilai

Kalau layanan Anda berada di belakang reverse proxy, X-Forwarded-For adalah satu-satunya cara mengetahui alamat asli pemanggil — tapi header itu juga bisa ditulis siapa saja. Aturannya karena itu gagal-tertutup:

  • Header X-Forwarded-For / X-Real-IP hanya dibaca kalau koneksi datang dari proxy yang dipercaya (bawaan: 127.0.0.1 dan ::1).
  • Dari X-Forwarded-For, entri yang dipakai adalah entri terakhir — entri yang ditulis proxy terdekat, bukan yang diklaim pemanggil.
  • Kalau koneksi datang langsung, yang dinilai remote_addr itu sendiri dan header diabaikan sepenuhnya.

Daftar proxy yang dipercaya diatur administrator lewat variabel lingkungan SEND_TRUSTED_PROXIES (dipisah koma, boleh CIDR). Jangan pernah menambahkan 0.0.0.0/0 — itu mengembalikan pembatasan ini menjadi hiasan, karena setiap pemanggil lalu bebas mengaku berada di alamat mana pun.

GET /send/api/ip-allowlist

curl -H "Authorization: Bearer <API_KEY_ANDA>" \
  "https://send.yolked.id/send/api/ip-allowlist"
{
  "status": "ok",
  "enabled": true,
  "ips": ["203.0.113.10", "10.0.0.0/24"],
  "max_entries": 20,
  "current_ip": "203.0.113.10",
  "current_ip_source": "peer",
  "current_ip_allowed": true,
  "updated_at": "2026-09-22T04:15:02Z"
}
Field Keterangan
ips Daftar yang tersimpan, sudah dinormalkan (10.0.0.1 menjadi 10.0.0.1/32)
current_ip Alamat yang kami lihat untuk permintaan ini — pakai ini saat menulis daftarnya, bukan hasil whatismyip
current_ip_source peer kalau dari koneksi langsung, x-forwarded-for atau x-real-ip kalau lewat proxy tepercaya
current_ip_allowed Apakah current_ip lolos daftar saat ini

POST /send/api/ip-allowlist

Sesi browser saja. Daftar diganti seluruhnya, bukan ditambal, supaya apa yang Anda lihat di formulir persis sama dengan yang tersimpan.

Field Tipe Keterangan
enabled boolean true untuk mulai menolak alamat di luar daftar
ips array string Alamat IP tunggal atau CIDR. 10.0.0.1 dan 10.0.0.1/32 dianggap satu entri

Batasannya 20 entri per akun, cukup untuk rantai beberapa toko. Mengaktifkan pembatasan dengan daftar kosong ditolak 400 + code: no_ips — mengaktifkan sesuatu yang mengunci semuanya hampir pasti bukan yang Anda maksud.

Format nomor

Pakai format internasional tanpa tanda +, tanpa spasi, dan tanpa tanda hubung — contoh 6281234567890. Pastikan kode negaranya benar; nomor Indonesia diawali 62.

Kode error

Semua kegagalan mengembalikan JSON dengan field error, dan sebagian menyertakan field code yang stabil untuk dibaca program:

Kode Arti
401 API key tidak dikenali, klien tidak aktif, atau langganannya kedaluwarsa
401 + no_session Permintaan tidak punya kredensial yang dikenali. Muncul di endpoint jalur API maupun dashboard: menu_required mencari klien dari API key lebih dulu, dan kalau tidak ketemu ia jatuh ke cabang sesi. Jadi kunci yang salah tulis pun menghasilkan no_session, bukan kode tersendiri — pembedaannya memang tidak dilakukan supaya pemanggil tidak bisa menebak kunci mana yang pernah ada
400 Isi permintaan kurang atau tidak valid
400 + otp_not_schedulable send-otp dipanggil dengan scheduled_at; OTP tidak bisa dijadwalkan
400 + confirmation_required client/rotate-key dipanggil tanpa {"confirm": true}
400 + invalid_email auth/magic/link dipanggil dengan alamat email yang bentuknya tidak sah
400 + invalid_redirect_uri auth/magic/link meminta pengembalian ke alamat yang belum terdaftar di SEND_MAGIC_REDIRECT_URIS
402 + insufficient_balance Kuota habis dan saldo E-Wallet tidak cukup. Balasannya menyertakan required, balance, dan shortfall
403 + api_access_required Paket akun tidak mencantumkan akses API
403 + client_required auth/magic/link dipanggil tanpa API key klien maupun sesi pemilik klien
403 + magic_disabled Masuk lewat tautan sedang dimatikan admin di dashboard
403 + ip_not_allowed Pembatasan IP aktif dan permintaan datang dari alamat di luar daftar. Kredensialnya sah — bedakan dari no_session. Balasannya menyertakan ip yang kami lihat
403 + webhook_not_in_plan POST /send/api/webhook dipanggil pada paket yang tidak mencantumkan fitur Webhook
403 + quota_exhausted Kuota habis untuk pengiriman yang dijadwalkan
403 + menu_forbidden Jalur ini tidak diaktifkan untuk akun Anda
403 + session_required Endpoint yang harus diselesaikan di browser (mis. wallet/topup) dipanggil dengan API key
400 + url_invalid URL webhook bukan http(s), memuat kredensial, atau menunjuk ke alamat yang tidak publik
400 + bad_ip Daftar IP memuat entri yang bukan alamat IP/CIDR yang sah. Field invalid memuat entri yang ditolak
400 + no_ips Pembatasan IP diaktifkan dengan daftar kosong
400 + too_many_ips Daftar IP melebihi batas entri per akun
400 + no_events Daftar event webhook dikirim dalam keadaan kosong
404 template_id tidak ditemukan atau bukan milik akun Anda. Field template pada send-otp tidak menghasilkan 404 — lihat catatan di bagiannya
404 + no_client client/rotate-key dijalankan oleh akun yang belum punya klien Send
429 + otp_rate_limited Terlalu sering mengirim OTP ke nomor yang sama; lihat Retry-After
429 + magic_rate_limited Terlalu sering meminta tautan masuk untuk alamat yang sama; lihat Retry-After
429 + magic_cooldown Masih dalam jeda 60 detik setelah permintaan tautan masuk sebelumnya
502 + smtp_failed Tautan masuk gagal dikirim karena SMTP bermasalah. Kuota kirimnya tidak terpakai — baris tautannya dibatalkan, jadi tidak ada tautan hidup yang tidak sampai ke mana pun
502 + store_failed Tautan masuk gagal disimpan di server, jadi tidak ada yang dikirim

Kuota dulu, saldo kemudian

Kuota paket dipakai lebih dulu. Setelah kuota habis, kelebihan pengiriman — untuk pengiriman yang dikirim saat itu juga — ditagih ke saldo E-Wallet selama saldonya mencukupi. Kalau saldo tidak cukup, permintaan ditolak dengan 402 + insufficient_balance dan tidak ada pesan yang dikirim. Periksa wallet.balance dan quota.*.remaining dari GET /send/api/status untuk mengetahui posisi Anda tanpa mengirim apa pun.

Sudah siap mengintegrasikan?

Contoh kode lengkap untuk Python/Flask, Node.js/Express, dan PHP/Laravel — termasuk cara menyimpan dan memverifikasi kode OTP di database Anda sendiri — ada di Panduan Integrasi.