Mulai cepat
- Aktifkan domain pengirim di dashboard (Settings → Domains) sampai berstatus
active. Email hanya bisa dikirim dari domain yang sudah terverifikasi. - Buat API key di Settings → API Keys dengan scope
send_email. Simpan di server, jangan di aplikasi HP atau kode browser. - Cari ID domain lewat
GET /v1/domains, lalu kirim email lewatPOST /v1/messages.
# 1. Lihat domain pengirim (ambil "id" yang statusnya active)
curl https://api.kirim.id/v1/domains \
-H "Authorization: Bearer $KIRIM_API_KEY"
# [{"id":1,"domain":"tokomu.com","status":"active"}]
# 2. Kirim email
curl -X POST https://api.kirim.id/v1/messages \
-H "Authorization: Bearer $KIRIM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"subject": "Pesanan {{nama|Kakak}} sudah kami terima",
"html": "<p>Halo {{nama_depan|Kak}}, pesanan #1001 sedang diproses.</p>",
"sending_domain_id": 1
}'
# 201 {"message_id":"9b1c7e2a-5d0f-4b8e-9a51-3f0c2d6e7a10","status":"queued"}
# 3. Cek statusnya
curl https://api.kirim.id/v1/messages/9b1c7e2a-5d0f-4b8e-9a51-3f0c2d6e7a10 \
-H "Authorization: Bearer $KIRIM_API_KEY"
Dasar & format
| Hal | Keterangan |
|---|---|
| Base URL | https://api.kirim.id/v1 — hanya HTTPS. Path yang sama di domain lain tidak dilayani. |
| Format request | JSON (Content-Type: application/json) atau form (application/x-www-form-urlencoded). Spasi di awal/akhir teks otomatis dibuang; teks kosong dianggap tidak diisi. |
| Format jawaban | Selalu JSON, termasuk saat error — header Accept tidak wajib. |
| Waktu | UTC, format ISO 8601. Data objek memakai 2026-09-22T03:04:05.000000Z; payload webhook memakai 2026-09-22T03:04:05+00:00. |
| Uang | Rupiah bulat (integer), tanpa desimal. |
| Pemakaian | Server-ke-server. API tidak mengirim header CORS, jadi tidak bisa (dan tidak boleh) dipanggil langsung dari browser — API key akan bocor. |
| Modul | Workspace harus punya modul Email Marketing aktif. Tanpa itu semua endpoint di halaman ini menjawab 403. |
Autentikasi & API key
Setiap request wajib membawa header:
Authorization: Bearer api_kirimXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- Format key:
api_kirimdiikuti 32 huruf/angka (total 41 karakter). Key tidak bisa dikirim lewat query string. - Key bisa dilihat ulang di dashboard kapan pun, dan bisa dicabut — request dengan key yang dicabut langsung ditolak.
- Satu key boleh punya beberapa scope. Buat key terpisah per sistem (mis. "Website", "CRM") supaya mudah dicabut satu per satu.
Scope
| Scope | Label di dashboard | Endpoint |
|---|---|---|
send_email | Kirim Email | GET /domains, POST /messages, GET /messages/{id}, GET /suppressions, GET /campaigns, POST /campaigns/{id}/send, GET /journeys, POST /journeys/{id}/enroll, POST /conversions |
manage_contacts | Kelola Kontak/Audiens | GET /contacts, GET /contacts/engagement, POST /contacts, DELETE /contacts/{id}, GET /audiences, POST /audiences |
manage_webhooks | Kelola Webhook | GET /webhooks, POST /webhooks, DELETE /webhooks/{id} |
Jawaban gagal autentikasi
| Status | Kapan | Isi message |
|---|---|---|
| 401 | Header Authorization tidak ada | Missing API key. |
| 401 | Key salah atau sudah dicabut | Invalid or revoked API key. |
| 403 | Akun disuspend | Workspace disuspend. |
| 403 | Key tidak punya scope yang dibutuhkan | API key ini tidak punya scope 'send_email'. |
| 403 | Modul Email tidak aktif | Modul Email Marketing belum aktif untuk akun ini. Aktifkan di app.kirim.id/produk. |
Error & kode status
Semua error berbentuk JSON dengan field message. Error validasi (422) juga membawa errors per field:
{
"message": "Penerima wajib diisi. (and 1 more error)",
"errors": {
"to": ["Penerima wajib diisi."],
"subject": ["Subjek wajib diisi."]
}
}
| Status | Arti | Yang sebaiknya dilakukan |
|---|---|---|
| 200 / 201 | Berhasil (201 = data baru dibuat/diterima) | — |
| 401 | API key tidak ada / salah / dicabut | Periksa key. Jangan diulang otomatis. |
| 403 | Scope kurang, akun disuspend, atau modul tidak aktif | Perbaiki pengaturan key/akun. Jangan diulang otomatis. |
| 404 | Data tidak ditemukan, milik akun lain, atau ID salah bentuk | Periksa ID. |
| 405 | Method HTTP salah (mis. GET ke endpoint POST) | Periksa method. |
| 422 | Validasi gagal atau aturan bisnis menolak (domain belum aktif, koin habis, penerima di suppression list, dll.) | Baca message/errors. Jangan diulang tanpa perubahan. |
| 429 | Terlalu banyak request (lihat batas kecepatan) | Tunggu sesuai header Retry-After, lalu ulangi. |
| 500 | Gangguan di sisi kami ({"message":"Server Error"}) | Ulangi dengan jeda (mis. 1, 5, 30 detik). |
| 503 | REST API dimatikan sementara oleh admin Kirim.id | Ulangi beberapa saat lagi. |
Batas kecepatan
Request API
- 60 request per menit per API key (tanpa key: per alamat IP). Kuota ini dipakai bersama oleh semua endpoint.
- Setiap jawaban membawa header
X-RateLimit-LimitdanX-RateLimit-Remaining. - Kalau terlampaui:
429 {"message":"Too Many Attempts."}dengan headerRetry-After(detik) danX-RateLimit-Reset(unix time).
Kecepatan kirim email
Terpisah dari batas request: email yang diterima API (201 queued) dikirim bertahap sesuai jenis akun. Kalau batasnya penuh, email menunggu di antrean — tidak ditolak, tidak perlu dikirim ulang.
| Jenis akun | Kecepatan |
|---|---|
| Free (belum pernah beli koin) | 10 email/menit, maksimal 300 email/hari |
| Premium (punya koin hasil beli yang masih berlaku) | 60 email/menit, tanpa batas harian |
Batas ini berlaku bersama untuk semua jalur kirim (aplikasi, API, SMTP relay, journey). Selama server pengiriman kami dalam masa pemanasan IP, ada juga batas harian se-platform; kelebihannya dikirim hari berikutnya (hari berganti pukul 00.00 WIB).
Kirim email
Mengirim satu email ke satu penerima. Cocok untuk email transaksional: kode verifikasi, reset password, invoice, notifikasi pesanan.
| Field | Tipe | Wajib | Aturan |
|---|---|---|---|
to | string | ya | Satu alamat email, maks. 255 karakter. Bukan array. |
subject | string | ya | Maks. 255 karakter. Boleh berisi merge tag. |
html | string | ya | Isi HTML, maks. 500.000 karakter. Boleh berisi merge tag. Versi teks biasa dibuat otomatis dari HTML. |
sending_domain_id | integer | ya | ID domain milikmu yang berstatus active (lihat GET /v1/domains). |
Pengirim otomatis no-reply@domainmu dengan nama tampilan = nama bisnismu. Field lain (mis. from, cc, lampiran) diabaikan — lihat batasan.
Urutan pemeriksaan
- Alamat
torusak (bukan format email / lebih dari 255 karakter) → penalti 4 koin, lalu 422. - Validasi field (422).
- Domain harus milikmu & aktif → kalau tidak: 422
Domain pengirim belum aktif/terverifikasi. - Saldo koin minimal 1 → kalau tidak: 422
Saldo koin habis — top-up koin untuk lanjut mengirim. - Penerima ada di suppression list → penalti + 422
Penerima ada di suppression list. - Penerima jadi kontak (dibuat otomatis bila belum ada). Kalau kontak sudah berhenti langganan / bounce / melapor spam → penalti + 422
Penerima sudah unsubscribe. - Email masuk antrean →
201.
GET /v1/suppressions?email= sebelum mengirim ke alamat yang belum pasti. Penalti yang sama (alamat + alasan) hanya dikenakan sekali per 24 jam.Jawaban
201 Created
{"message_id":"9b1c7e2a-5d0f-4b8e-9a51-3f0c2d6e7a10","status":"queued"}
queued berarti email diterima dan menunggu giliran kirim. Koin dipotong saat email benar-benar diserahkan ke server pengiriman (bukan saat diterima API). Simpan message_id untuk mengecek status dan mencocokkan webhook.
Contoh kode
// PHP (Laravel HTTP client)
$res = Http::withToken(env('KIRIM_API_KEY'))
->post('https://api.kirim.id/v1/messages', [
'to' => '[email protected]',
'subject' => 'Kode verifikasi kamu',
'html' => '<p>Kode: <b>481920</b>. Berlaku 10 menit.</p>',
'sending_domain_id' => 1,
]);
if ($res->failed()) {
logger()->warning('Kirim email gagal', ['status' => $res->status(), 'body' => $res->json()]);
}
$messageId = $res->json('message_id');
// Node.js 18+ (fetch bawaan)
const res = await fetch('https://api.kirim.id/v1/messages', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.KIRIM_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
to: '[email protected]',
subject: 'Kode verifikasi kamu',
html: '<p>Kode: <b>481920</b>. Berlaku 10 menit.</p>',
sending_domain_id: 1,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${data.message}`);
console.log(data.message_id);
# Python (requests)
import os, requests
r = requests.post(
"https://api.kirim.id/v1/messages",
headers={"Authorization": f"Bearer {os.environ['KIRIM_API_KEY']}"},
json={
"to": "[email protected]",
"subject": "Kode verifikasi kamu",
"html": "<p>Kode: <b>481920</b>. Berlaku 10 menit.</p>",
"sending_domain_id": 1,
},
timeout=15,
)
r.raise_for_status()
print(r.json()["message_id"])
Status pesan
200 OK
{
"message_id": "9b1c7e2a-5d0f-4b8e-9a51-3f0c2d6e7a10",
"status": "delivered",
"failed_reason": null,
"dispatched_at": "2026-09-22T03:04:07.000000Z"
}
message_id harus UUID milik akunmu; selain itu 404.
| Status | Arti |
|---|---|
queued | Diterima, menunggu giliran (termasuk saat menunggu batas kecepatan). Kalau failed_reason terisi, penolakan sementara sedang dicoba ulang otomatis. |
sending | Sedang diserahkan ke server pengiriman (sesaat). |
sent_to_mta | Sudah diserahkan ke server pengiriman (dispatched_at terisi). Kalau failed_reason diawali "Ditunda sementara", server penerima minta ditunda dan akan dicoba lagi. |
delivered | Diterima server email penerima. |
bounced | Ditolak server penerima. failed_reason berisi kode & alasan dari server penerima, mis. 550 5.1.1 user unknown. |
failed | Tidak jadi dikirim — lihat failed_reason di bawah. |
Isi failed_reason untuk status failed:
Domain pengirim tidak aktif.,Workspace disuspend.Kontak sudah dihapus.,Kontak sudah tidak berlangganan.,Penerima ada di suppression list.— kondisi berubah setelah API menerima email.Saldo koin habis — top-up koin untuk lanjut mengirim.— saldo habis saat giliran kirim tiba.- Penolakan permanen dari server pengiriman, atau
MTA menolak sementara terus-menerus: …setelah percobaan ulang habis (maks. 3 hari). Tidak ada catatan pengiriman di server MTA — status akhir tidak diketahui. Koin dikembalikan.
Personalisasi (merge tag)
Tulis {{nama_tag}} di subject atau html. Tambahkan teks cadangan dengan {{nama_tag|Cadangan}} — dipakai kalau datanya kosong. Nama tag tidak peka huruf besar/kecil; tag yang tidak dikenal dibiarkan apa adanya. Nilai otomatis di-escape untuk HTML.
| Tag (dan alias) | Isi |
|---|---|
{{email}} | Email penerima |
{{nama}} · {{name}} | Nama kontak. Kontak yang dibuat otomatis oleh POST /messages belum punya nama — tambahkan dulu lewat POST /contacts, atau pakai cadangan: {{nama|Kak}}. |
{{nama_depan}} · {{first_name}} | Kata pertama dari nama |
{{bisnis}} · {{nama_bisnis}} · {{perusahaan}} · {{company}} | Nama bisnismu |
{{alamat_bisnis}} · {{alamat_perusahaan}} | Alamat bisnismu (Pengaturan Akun) |
{{unsubscribe_url}} · {{link_berhenti_langganan}} · {{link_unsubscribe}} | Link berhenti langganan khusus penerima ini |
{{tahun}} | Tahun sekarang (WIB) |
{{judul_email}} · {{subjek}} | Subjek email (untuk dipakai di isi) |
{{link_browser}} · {{versi_web}} · {{preheader}} | Hanya untuk campaign — kosong di email API (pakai cadangan atau jangan dipakai) |
Pelacakan, unsubscribe & header
Pelacakan buka & klik (selalu aktif)
- Klik: setiap link
href="http(s)://…"dengan tanda kutip ganda diganti link pelacakapp.kirim.id/t/c/…yang langsung meneruskan ke tujuan asli. Linkmailto:,tel:, atau tanpa tanda kutip ganda tidak dilacak. - Buka: gambar 1×1 piksel ditambahkan di akhir email. Sebagian aplikasi (mis. Apple Mail) memuat gambar otomatis, jadi "dibuka" bisa sedikit lebih tinggi dari kenyataan. Klik otomatis juga dihitung sebagai dibuka.
- Kalau pelacakan konversi aktif di akunmu, link tujuan mendapat parameter
kirim_mid=<message_id>(lihat konversi).
Unsubscribe (wajib ada)
- Kalau isi email belum memuat link unsubscribe (lewat
{{unsubscribe_url}}atau link bertuliskan "unsubscribe" / "berhenti langganan"), kami menambahkan footer berisi nama & alamat bisnis dan link "Berhenti berlangganan". - Email juga membawa header
List-Unsubscribe+List-Unsubscribe-Post: List-Unsubscribe=One-Click(syarat Gmail & Yahoo untuk pengirim massal). - Penerima yang berhenti langganan berstatus
unsubscribed; pengiriman berikutnya ke alamat itu ditolak (dan dikenai penalti kalau dipaksa).
Header yang ditambahkan
Message-ID: <message_id@domainmu>, List-Unsubscribe, List-Unsubscribe-Post, X-Campaign-id: api, X-Subscriber-id, X-Message-id. Return-Path memakai subdomain bounce domainmu, dan email ditandatangani DKIM domainmu oleh server pengiriman.
Domain pengirim
200 OK
[{"id":1,"domain":"tokomu.com","status":"active"}]
Urut abjad, tanpa paginasi. Status: pending_dns, dns_verifying, provisioning, active, dns_failed, suspended. Hanya active yang bisa dipakai mengirim.
Cek boleh dikirimi
200 OK
{"email":"[email protected]","can_send":false,"reason":"hard_bounce"}
reason | Arti |
|---|---|
null | Boleh dikirimi (can_send: true) |
hard_bounce | Alamat tidak ada / ditolak permanen |
complaint | Penerima pernah melapor spam |
manual | Kamu memblokirnya di Daftar Filter |
global | Diblokir untuk semua pengirim Kirim.id |
unsubscribed · bounced · complained | Status kontak di akunmu |
Kontak
50 kontak per halaman, urut ID. Format paginasi:
{
"current_page": 1,
"data": [
{"id":12,"workspace_id":3,"email":"[email protected]","name":"Rina","status":"subscribed",
"subscribed_at":"2026-09-20T02:11:00.000000Z","unsubscribed_at":null,
"created_at":"2026-09-20T02:11:00.000000Z","updated_at":"2026-09-20T02:11:00.000000Z"}
],
"first_page_url": "https://api.kirim.id/v1/contacts?page=1",
"from": 1, "last_page": 3, "last_page_url": "https://api.kirim.id/v1/contacts?page=3",
"links": [...], "next_page_url": "https://api.kirim.id/v1/contacts?page=2",
"path": "https://api.kirim.id/v1/contacts", "per_page": 50, "prev_page_url": null, "to": 50, "total": 123
}
Status kontak: subscribed, unsubscribed, bounced, complained.
| Field | Wajib | Aturan |
|---|---|---|
email | ya | Email valid, maks. 255 |
name | tidak | Maks. 255 |
consent | ya | Harus true — kontak sudah memberi izin (opt-in) menerima email darimu |
Jawaban 201 berisi objek kontak. Kalau email sudah ada, kontak lama dikembalikan tanpa diubah (nama tidak diperbarui, status tidak dikembalikan ke subscribed). Kontak tidak otomatis masuk audience mana pun.
Menghapus kontak permanen; riwayat pesannya dianonimkan. Jawaban 200 {"deleted":true}. Menghapus kontak tidak memasukkannya ke suppression list — kalau ingin memblokir, pakai Daftar Filter di dashboard.
Kontak teraktif
Kontak yang paling rajin membuka atau mengklik emailmu. Dihitung per email unik: satu email yang dibuka berkali-kali tetap dihitung 1. Hanya email yang sudah diserahkan ke server pengiriman yang dihitung sebagai "diterima".
| Parameter | Bawaan | Keterangan |
|---|---|---|
by | opens | opens = paling rajin buka, clicks = paling rajin klik |
days | 90 | Periode ke belakang (hari), 1–3650. 0 = semua waktu |
audience_id | — | Hanya kontak di audience ini (404 kalau bukan milikmu) |
per_page · page | 50 · 1 | per_page maks. 200 |
200 OK
{
"data": [
{"rank":1,"contact_id":12,"email":"[email protected]","name":"Rina","status":"subscribed",
"received":24,"opened":21,"open_rate":87.5,"clicked":9,"click_rate":37.5,"total_clicks":14,
"last_activity_at":"2026-09-21T13:40:02+00:00"}
],
"meta": {"by":"opens","days":90,"audience_id":null,"current_page":1,"last_page":4,"per_page":50,"total":187}
}
Urutan: by=opens → jumlah email dibuka, lalu diklik, lalu total klik. by=clicks → jumlah email diklik, lalu total klik, lalu dibuka. Kontak yang tidak pernah membuka/mengklik pada periode itu tidak muncul. open_rate dan click_rate dalam persen.
Audience
200 OK
[{"id":4,"workspace_id":3,"name":"Pelanggan","description":null,"public_token":"…","opt_in_mode":"single",
"created_at":"…","updated_at":"…","contacts_count":320}]
opt_in_mode: single atau double (anggota wajib konfirmasi lewat email sebelum menerima campaign). public_token dipakai di link form subscribe publik audience.
Field: name (wajib, maks. 255), description (opsional, maks. 1000). Audience baru selalu single opt-in. Jawaban 201 berisi audience yang dibuat.
Campaign
200 OK
[{"id":28,"subject":"Promo September","status":"draft","scheduled_at":null,"sent_at":null,"created_at":"…"}]
Terbaru dulu, tanpa paginasi. Status: draft, scheduled, live, paused, completed.
Mengirim campaign berstatus draft yang sudah kamu siapkan di dashboard (isi, audience, pengirim) — cocok untuk memicu campaign dari sistemmu sendiri.
200 OK
{"id":28,"status":"live"} // atau "completed" kalau tidak ada penerima yang memenuhi syarat
Penerima = anggota audience yang subscribed, tidak di suppression list, dan sudah konfirmasi (untuk audience double opt-in). Email dikirim bertahap sesuai kecepatan akunmu. Penolakan (422, pesan pertama yang cocok):
Alamat bisnis belum diisi. …— wajib untuk email marketing (isi di Pengaturan Akun).Domain pengirim belum aktif/terverifikasi.Konten email campaign ini belum diisi.Saldo koin tidak cukup: butuh N koin, saldo M. …— seluruh penerima harus tertutup saldo di awal.Campaign ini bukan draft.— yang terjadwal/berjalan/selesai tidak bisa dikirim lewat API.
Journey
200 OK
[{"id":5,"name":"Welcome Series","status":"active","trigger_type":"api"}]
status: draft, active, paused. trigger_type: form_subscribe, audience_join, api.
Memasukkan kontak ke journey yang pemicunya API (mis. pelanggan baru mendaftar di websitemu).
| Field | Wajib | Aturan |
|---|---|---|
email | ya | Email valid, maks. 255 |
name | tidak | Maks. 255 (hanya untuk kontak baru) |
consent | ya | Harus true |
curl -X POST https://api.kirim.id/v1/journeys/5/enroll \
-H "Authorization: Bearer $KIRIM_API_KEY" -H "Content-Type: application/json" \
-d '{"email":"[email protected]","name":"Budi","consent":true}'
201 {"journey_id":5,"email":"[email protected]","enrolled":true,"message":"Kontak masuk journey."}
200 {"journey_id":5,"email":"[email protected]","enrolled":false,"message":"Kontak sudah pernah masuk journey ini (tidak diulang)."}
Satu kontak hanya bisa masuk satu journey sekali. Langkah journey diproses tiap menit. Penolakan 422: Journey ini tidak dipicu lewat API., Journey ini belum aktif., Penerima ada di suppression list. (tanpa penalti), Kontak ini sudah berhenti berlangganan.
Konversi (revenue)
Catat penjualan dari sistemmu supaya dashboard bisa menunjukkan berapa rupiah yang datang dari email. Penjualan dihubungkan ke klik email terakhir dalam 7 hari sebelum waktu penjualan.
| Field | Wajib | Aturan |
|---|---|---|
amount | ya | Rupiah bulat, 0 s.d. 10.000.000.000 |
email | salah satu | Email pembeli (dicocokkan ke kontak) |
message_id | salah satu | UUID pesan yang diklik — paling akurat. Didapat dari parameter kirim_mid di URL tujuan klik. Kalau diisi, email diabaikan. |
order_id | tidak | Maks. 191. Kalau sama dengan yang sudah pernah dicatat → dianggap duplikat (aman untuk diulang). |
occurred_at | tidak | Waktu penjualan, tidak boleh di masa depan. Sertakan zona waktu (mis. 2026-09-22T10:00:00+07:00); tanpa zona dianggap UTC. |
@__raw_block_16__{{ $convDays }} hari terakhir — tidak dihitung sebagai revenue email."}
Konversi unattributed tidak disimpan.
Webhook
Kami mengirim POST ke URL-mu setiap ada kejadian pada email dari akunmu — dari semua jalur (campaign, journey, API, SMTP relay).
| Event | Kapan |
|---|---|
delivered | Email diterima server penerima. occurred_at = waktu email diserahkan ke server pengiriman. |
bounce | Ditolak server penerima (sementara maupun permanen) |
complaint | Penerima melapor spam |
open | Setiap kali email dibuka (bisa lebih dari sekali per email) |
click | Setiap klik link yang dilacak |
unsubscribe | Penerima berhenti langganan lewat link di email |
Mendaftarkan webhook
curl -X POST https://api.kirim.id/v1/webhooks \
-H "Authorization: Bearer $KIRIM_API_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://tokomu.com/webhook/kirim","event_types":["delivered","bounce","complaint","click"]}'
201 {"id":3,"url":"https://tokomu.com/webhook/kirim","secret":"q8Xk…40 karakter…","event_types":["delivered","bounce","complaint","click"]}
secret sekarang — hanya ditampilkan sekali. Untuk ganti secret: hapus webhook lalu daftarkan lagi.url: wajibhttps://, maks. 255, tidak boleh berisi username/password, host harus bisa ditemukan di DNS dan tidak boleh mengarah ke jaringan internal/privat.event_types: minimal satu dari daftar di atas.- Maksimal 10 webhook per akun (422
Maksimal 10 webhook per workspace. …).
200 [{"id":3,"url":"https://tokomu.com/webhook/kirim","event_types":["delivered","bounce"],"is_active":true,"created_at":"…"}]
Jawaban 200 {"deleted":true}. Pengiriman yang masih antre untuk webhook itu dibatalkan.
Isi yang kami kirim
POST https://tokomu.com/webhook/kirim
Content-Type: application/json
X-Kirim-Signature: 5d41402abc4b2a76b9719d911017c592… (hex HMAC-SHA256)
{"event":"click","message_id":"9b1c7e2a-5d0f-4b8e-9a51-3f0c2d6e7a10","occurred_at":"2026-09-22T03:10:44+00:00"}
Payload hanya berisi jenis event, message_id, dan waktu. Simpan message_id saat mengirim supaya bisa dicocokkan ke pesanan/pengguna di sistemmu.
Verifikasi tanda tangan (wajib)
Hitung HMAC-SHA256 dari body mentah (byte persis seperti diterima — jangan parse lalu encode ulang) memakai secret, ubah ke hex huruf kecil, lalu bandingkan dengan header X-Kirim-Signature memakai perbandingan waktu-konstan.
// PHP
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, getenv('KIRIM_WEBHOOK_SECRET'));
if (! hash_equals($expected, $_SERVER['HTTP_X_KIRIM_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true); // proses, lalu jawab 2xx secepatnya
http_response_code(200);
// Node.js (Express) — pakai body mentah, bukan express.json()
const crypto = require('crypto');
app.post('/webhook/kirim', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto.createHmac('sha256', process.env.KIRIM_WEBHOOK_SECRET).update(req.body).digest('hex');
const got = req.get('X-Kirim-Signature') || '';
if (got.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200);
// proses event di sini (setelah menjawab)
});
# Python (Flask)
import hmac, hashlib, os
from flask import request, abort
@app.post("/webhook/kirim")
def kirim_webhook():
raw = request.get_data()
expected = hmac.new(os.environ["KIRIM_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Kirim-Signature", "")):
abort(401)
event = request.get_json()
return "", 200
Pengiriman ulang
- Jawab dengan status 2xx dalam 10 detik (koneksi maks. 5 detik). Redirect tidak diikuti.
- Kalau gagal/bukan 2xx, kami mencoba lagi dengan jeda 30, 60, 90, 120 detik — maksimal 5 percobaan.
- Event bisa datang lebih dari sekali atau tidak berurutan. Buat pemrosesanmu aman terhadap duplikat (mis. simpan kombinasi
message_id+event+occurred_at). - Tidak ada stempel waktu anti-replay di tanda tangan — jaga
secrettetap rahasia dan pakai HTTPS.
Webhook dari langkah Journey
Langkah "Webhook" di journey mengirim ke URL yang kamu atur di dashboard, ditandatangani dengan secret journey (lihat halaman detail journey) memakai cara verifikasi yang sama:
{"event":"journey.webhook","journey":{"id":5,"name":"Welcome Series"},"contact":{"email":"[email protected]","name":"Budi"},
"step":3,"goal_reached":false,"occurred_at":"2026-09-22T03:12:00+00:00"}
Dicoba hingga 5 kali (jeda 30 detik, 2 menit, 10 menit, 30 menit). Journey tetap berlanjut apa pun hasilnya.
SMTP relay
Alternatif tanpa coding untuk aplikasi yang sudah bisa kirim lewat SMTP (WordPress, WooCommerce, Laravel, Odoo, dll.). Kredensial dibuat di Settings → Kredensial SMTP.
| Pengaturan | Nilai |
|---|---|
| Host | mail.kirim.id |
| Port | 587 (STARTTLS) atau 465 (SSL/TLS). Login wajib lewat koneksi terenkripsi. |
| Username | kir + 6 angka (dari dashboard) |
| Password | 32 karakter dari dashboard (bisa diganti kapan saja; username tetap) |
| Metode login | PLAIN atau LOGIN |
| Pengirim | Alamat bebas, asal domainnya aktif di akunmu (header From & MAIL FROM) |
| Batas | Maks. 50 penerima per email, 10 MB per email, kecepatan per menit = kecepatan akunmu. Lampiran tidak dikirim. |
MAIL_MAILER=smtp MAIL_HOST=mail.kirim.id MAIL_PORT=587 MAIL_ENCRYPTION=tls MAIL_USERNAME=kir123456 MAIL_PASSWORD=password-dari-dashboard [email protected]
Setiap penerima menjadi satu pesan dengan fitur yang sama seperti API (merge tag, pelacakan, footer unsubscribe, webhook). Kode balasan yang perlu dikenali: 535 login salah, 550 5.7.1 pengirim bukan domain aktif / penerima di suppression list / sudah berhenti langganan / saldo koin habis, 553 5.1.3 alamat penerima rusak, 452 batas kecepatan per menit tercapai (coba lagi), 451 gangguan sementara (kirim ulang).
Koin & biaya
| Kejadian | Koin |
|---|---|
| Email diserahkan ke server pengiriman | 1 koin (= Rp0,5). Dipotong saat giliran kirim, bukan saat API menerima. |
| Server pengiriman menolak (sementara/permanen) atau tidak ada catatan pengiriman | Koin dikembalikan |
| Bounce dari server penerima | Tidak dikembalikan |
| Hard bounce atau laporan spam | Penalti 4 koin (sekali per pesan); alamat otomatis masuk suppression list |
| Kirim paksa lewat API/SMTP ke alamat rusak, di suppression list, atau yang sudah berhenti langganan | Penalti 4 koin (sekali per alamat + alasan per 24 jam) |
Saldo bisa minus karena penalti; pengiriman berhenti sampai saldo kembali positif. Harga & aturan terbaru ada di halaman harga.
Batasan saat ini
Supaya tidak ada kejutan saat integrasi:
POST /messages: satu penerima per request; pengirim selaluno-reply@domainmu; belum mendukung nama pengirim/Reply-To sendiri, CC/BCC, lampiran, header kustom, atau isi teks terpisah (teks dibuat dari HTML). Butuh alamat pengirim bebas? Pakai SMTP relay.- Pelacakan buka/klik dan footer unsubscribe tidak bisa dimatikan.
- Belum ada API untuk memasukkan kontak ke audience atau mengatur double opt-in — lakukan dari dashboard (impor CSV / form subscribe).
POST /contactsselalu menjawab 201, termasuk untuk kontak yang sudah ada.- Payload webhook belum membawa email penerima, URL yang diklik, atau alasan bounce — pakai
message_id+GET /messages/{id}. - Kontak yang dihapus lalu dikirimi lagi lewat API akan dibuat ulang sebagai subscribed. Untuk memblokir alamat, tambahkan ke Daftar Filter.
Riwayat perubahan
- 22 Sep 2026 — Baru:
GET /v1/contacts/engagement(kontak teraktif). ID berbentuk salah di URL kini menjawab 404.GET /v1/contactskini selalu urut ID. - 21 Sep 2026 — Batas kapasitas harian selama pemanasan IP berlaku untuk semua jalur kirim.
- Sep 2026 — Rilis API Email v1.
Ada pertanyaan integrasi? Hubungi tim kami — kami bantu sampai jalan.