API Documentation

REST API untuk integrasi CRM, e-commerce, atau sistem internal Anda dengan WhatsApp melalui AxisFlow — kirim template/pesan teks, sinkronisasi kontak, dan terima event real-time via webhook.

Base URL: https://myaccount.axisflow.id/api/v1

Format: semua request/response berupa JSON (kecuali dinyatakan lain).

Autentikasi
Setiap request ke /api/v1/* wajib membawa API key di header Authorization.

Header

Authorization: Bearer axf_...

Buat dan kelola API key di Settings → API Keys. Key ditampilkan satu kali saat dibuat — simpan dengan aman. Key yang dicurigai bocor sebaiknya langsung di-revoke dan diganti.

Setiap key punya satu atau lebih scope yang membatasi endpoint apa saja yang bisa diakses:

ScopeDipakai untuk
contacts:readList / lihat kontak
contacts:writeMembuat / update kontak
messages:readList percakapan
messages:sendKirim pesan (teks atau template)
payments:writeMembuat & mengecek payment link
broadcasts:readDicadangkan — belum ada endpoint yang memakainya
broadcasts:writeDicadangkan — belum ada endpoint yang memakainya
templates:readDicadangkan — belum ada endpoint yang memakainya

Request tanpa scope yang sesuai mendapat 403 dengan {"error":"insufficient_scope","required":"..."}.

Nomor WhatsApp Anda
GET /api/v1/phone-numbers — scope messages:read

Kalau akun Anda punya lebih dari satu nomor WhatsApp terhubung, pakai endpoint ini untuk mendapatkan id tiap nomor.

phoneNumberId fleksibel: field phoneNumberId di Mengirim Pesan menerima dua bentuk idid dari response di bawah (id internal AxisFlow), atau nomor Meta's phone_number_id langsung dari Meta Business Manager Anda. Tidak perlu pilih salah satu — keduanya jalan, jadi kalau Anda sudah punya phone_number_id dari sisi Meta, langsung pakai itu saja tanpa perlu memanggil endpoint ini.

Request

curl -H "Authorization: Bearer axf_..." \
  https://myaccount.axisflow.id/api/v1/phone-numbers

# Response
{
  "data": [
    {
      "id": "cme...",
      "displayNumber": "+62 812-3456-7890",
      "verifiedName": "Toko Anyar",
      "status": "REGISTERED",
      "qualityRating": "GREEN",
      "wabaId": "cme..."
    }
  ]
}

Kalau Anda cuma punya 1 nomor, cukup ambil data[0].id sekali dan simpan — tidak perlu memanggil endpoint ini di setiap kirim pesan.

Mengirim Pesan
POST /api/v1/messages — scope messages:send. Satu endpoint untuk kirim template maupun teks bebas, dibedakan lewat field type. Setiap request wajib menyertakan phoneNumberId — boleh id dari Nomor WhatsApp Anda atau phone_number_id langsung dari Meta (penting kalau akun Anda punya lebih dari satu nomor).

Kirim template message

Untuk memulai percakapan baru atau mengirim di luar jendela layanan 24 jam, gunakan template yang sudah APPROVED di Meta. Kalau template tidak punya variabel {{1}}/header media, cukup kirim components: [].

Request — template tanpa variabel

curl -X POST https://myaccount.axisflow.id/api/v1/messages \
  -H "Authorization: Bearer axf_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "<PhoneNumber.id>",
    "to": "6281234567890",
    "type": "template",
    "template": {
      "name": "welcome_message",
      "language": "id",
      "components": []
    }
  }'

Response 200

{
  "id": "cme...",
  "wamid": "wamid.HBg...",
  "conversationId": "cme..."
}

Template dengan variabel {{1}} {{2}} ...

Isi variabel body/header persis seperti format Meta sendiri — components di request ini diteruskan langsung/mentah ke Meta Cloud API, tidak diubah bentuknya oleh kami. Kalau template Anda punya body seperti "Halo {{1}}, invoice {{2}} sudah lunas", isi tepat 2 parameter berurutan — parameter pertama mengisi {{1}}, kedua mengisi {{2}}, dst.

Request — body 2 variabel

curl -X POST https://myaccount.axisflow.id/api/v1/messages \
  -H "Authorization: Bearer axf_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "<PhoneNumber.id>",
    "to": "6281234567890",
    "type": "template",
    "template": {
      "name": "invoice_paid",
      "language": "id",
      "components": [
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "Budi Santoso" },
            { "type": "text", "text": "INV-2026-0001" }
          ]
        }
      ]
    }
  }'

Kalau template-nya juga punya header media (gambar/video/dokumen), tambahkan satu component lagi bertipe header sebelum body, dengan parameter berisi link (bukan perlu upload dulu — sama seperti media message di bawah):

Request — header image + body 2 variabel

curl -X POST https://myaccount.axisflow.id/api/v1/messages \
  -H "Authorization: Bearer axf_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "<PhoneNumber.id>",
    "to": "6281234567890",
    "type": "template",
    "template": {
      "name": "promo_banner",
      "language": "id",
      "components": [
        {
          "type": "header",
          "parameters": [
            { "type": "image", "image": { "link": "https://contoh.com/banner.jpg" } }
          ]
        },
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "Budi" },
            { "type": "text", "text": "50%" }
          ]
        }
      ]
    }
  }'

Cek jumlah & urutan variabel yang dibutuhkan template Anda di Templates — buka template-nya, lihat teks body aslinya (dengan {{n}}-nya masih ada). Jumlah objek di parameters harus sama dengan jumlah {{n}} di body, urut dari {{1}}.

Kirim pesan teks bebas

Hanya berlaku dalam jendela layanan pelanggan 24 jam sejak pesan masuk terakhir dari kontak tersebut — di luar itu Meta akan menolak (muncul sebagai meta_api_error di response).

Request

curl -X POST https://myaccount.axisflow.id/api/v1/messages \
  -H "Authorization: Bearer axf_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "<PhoneNumber.id>",
    "to": "6281234567890",
    "type": "text",
    "text": "Halo, pesanan Anda sudah kami proses."
  }'

Response 200 (sama untuk text, media, dan template)

{
  "id": "cme...",
  "wamid": "wamid.HBg...",
  "conversationId": "cme..."
}

wamid di response ini adalah nilai yang sama dengan id pada webhook message.status — simpan untuk mencocokkan status pengiriman (sent/delivered/read/failed) nanti.

Kirim media (gambar/video/audio/dokumen)

Cukup kirim URL publik lewat media.link tidak perlu upload dulu, Meta yang langsung mengambil file dari URL tersebut. Sama seperti teks bebas, hanya berlaku dalam jendela layanan 24 jam. media.filename wajib untuk type: "document" (supaya file punya ekstensi yang benar di chat) — opsional untuk tipe lainnya.

Request — kirim gambar

curl -X POST https://myaccount.axisflow.id/api/v1/messages \
  -H "Authorization: Bearer axf_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "<PhoneNumber.id>",
    "to": "6281234567890",
    "type": "image",
    "media": {
      "link": "https://contoh.com/produk-baru.jpg",
      "caption": "Produk baru sudah tersedia!"
    }
  }'

Request — kirim dokumen (filename wajib)

curl -X POST https://myaccount.axisflow.id/api/v1/messages \
  -H "Authorization: Bearer axf_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "<PhoneNumber.id>",
    "to": "6281234567890",
    "type": "document",
    "media": {
      "link": "https://contoh.com/files/invoice-2026-0001.pdf",
      "filename": "invoice-2026-0001.pdf"
    }
  }'

type juga bisa video atau audio dengan bentuk media yang sama (audio tidak mendukung caption — akan diabaikan Meta kalau dikirim). Response 200-nya sama seperti pesan teks di atas — {id, wamid, conversationId}.

Kemungkinan error

StatuserrorKeterangan
400invalid_requestBody tidak valid (lihat field issues)
404phone_not_foundphoneNumberId tidak ditemukan / bukan milik organisasi Anda
400waba_not_provisionedNomor WhatsApp belum selesai setup
400template_not_approvedTemplate belum berstatus APPROVED di Meta
402wallet_insufficient_balanceSaldo wallet tidak cukup — lihat available/required di response
400meta_api_errorDitolak oleh Meta — lihat code/message dari Meta
500send_failedKegagalan tak terduga di sisi kami
Kontak
Sinkronisasi dua arah dengan CRM Anda. Kontak bersifat satu database untuk seluruh akun — tidak terpisah per nomor WhatsApp, jadi tidak ada filter phoneNumberId di sini walau Anda punya beberapa nomor.

List kontak

GET /api/v1/contacts — scope contacts:read

curl -H "Authorization: Bearer axf_..." \
  "https://myaccount.axisflow.id/api/v1/contacts?limit=100&lifecycle=CUSTOMER&tag=vip"

# Response
{ "data": [ { "id": "...", "waId": "...", "name": "...", "lifecycle": "CUSTOMER", ... } ], "nextCursor": "..." }

Query params: cursor, limit (maks 500, default 100), lifecycle (LEAD|CUSTOMER|CHURNED|BLOCKED), tag.

Membuat / update kontak (upsert by waId)

POST /api/v1/contacts — scope contacts:write

curl -X POST https://myaccount.axisflow.id/api/v1/contacts \
  -H "Authorization: Bearer axf_..." \
  -H "Content-Type: application/json" \
  -d '{
    "waId": "6281234567890",
    "name": "Budi Santoso",
    "email": "budi@contoh.com",
    "lifecycle": "CUSTOMER",
    "tags": ["vip"],
    "attributes": { "company": "PT Contoh" }
  }'

Field wajib: waId (6-20 karakter). Semua field lain opsional. attributes berupa map string-ke-string bebas.

Percakapan
GET /api/v1/conversations — scope messages:read

Request

curl -H "Authorization: Bearer axf_..." \
  "https://myaccount.axisflow.id/api/v1/conversations?status=OPEN&limit=50"

# Response
{
  "data": [
    {
      "id": "...", "contactWaId": "...", "contactName": "...",
      "status": "OPEN", "tags": [], "unreadCount": 2,
      "assignedToId": "...", "lastMessageAt": "...", "fromPhone": "...",
      "phoneNumberId": "..."
    }
  ],
  "nextCursor": "..."
}

Query params: cursor, limit (maks 200, default 50), status (OPEN|PENDING|RESOLVED), waId (nomor kontak), phoneNumberId (nomor bisnis Anda — pakai kalau punya lebih dari satu nomor dan mau lihat percakapan dari satu nomor tertentu saja, lihat Nomor WhatsApp Anda).

Tanpa phoneNumberId, hasil menggabungkan percakapan dari semua nomor di akun Anda — respons tetap menyertakan phoneNumberId & fromPhone per baris supaya Anda bisa memisahkannya sendiri kalau perlu.

Isi Pesan / Histori Percakapan
GET /api/v1/conversations/:id/messages — scope messages:read

Baca seluruh isi pesan dalam satu percakapan — termasuk histori sebelum Anda mendaftarkan webhook, karena ini membaca langsung dari database kami, bukan dari log webhook. Ambil id percakapan dari GET /api/v1/conversations atau conversationId pada event message.received.

Request

curl -H "Authorization: Bearer axf_..." \
  "https://myaccount.axisflow.id/api/v1/conversations/<conversationId>/messages?limit=100"

# Response
{
  "data": [
    {
      "id": "cme...",
      "conversationId": "cme...",
      "wamid": "wamid.HBg...",
      "direction": "INBOUND",
      "type": "text",
      "text": { "body": "Halo, saya ingin tanya produk." },
      "status": "DELIVERED",
      "sentAt": "2026-08-10T02:00:00.000Z",
      "deliveredAt": "2026-08-10T02:00:01.000Z",
      "readAt": null,
      "createdAt": "2026-08-10T02:00:00.000Z"
    },
    {
      "id": "cme...",
      "conversationId": "cme...",
      "wamid": "wamid.HBg...",
      "direction": "OUTBOUND",
      "type": "image",
      "image": { "link": "https://contoh.com/produk-baru.jpg", "caption": "Produk baru!" },
      "status": "READ",
      "sentAt": "2026-08-10T02:01:00.000Z",
      "deliveredAt": "2026-08-10T02:01:05.000Z",
      "readAt": "2026-08-10T02:03:00.000Z",
      "createdAt": "2026-08-10T02:01:00.000Z"
    }
  ],
  "nextCursor": "cme..."
}

Diurutkan kronologis (pesan terlama dulu). Query params: cursor, limit (maks 500, default 100). type dan objek bertipe sama (text.body, image, document, dst.) mengikuti bentuk yang sama dengan webhook message.received — kode parsing bisa dipakai ulang untuk keduanya. wamid null untuk pesan keluar yang belum terkirim (status PENDING).

Pesan bertipe media di sini tidak menyertakan URL file — persis seperti webhook, Anda perlu mengambil bytes-nya sendiri lewat Meta Graph API kalau pesan itu masuk (media id Meta, kedaluwarsa cepat) atau lewat URL yang Anda kirim sendiri kalau pesan itu keluar.

Unduh Media Pesan Masuk
GET /api/v1/media/{mediaId} — scope messages:read

Mengunduh bytes media (gambar/video/audio/dokumen/stiker) dari pesan masuk, dengan API key Anda saja — tidak perlu access token WABA/Meta terpisah. mediaId adalah nilai image.id / document.id dkk. dari webhook message.received atau dari GET /api/v1/conversations/:id/messages.

Request

curl -H "Authorization: Bearer axf_..." \
  "https://myaccount.axisflow.id/api/v1/media/1234567890123456" \
  -o downloaded-file

# Response: binary body, dengan header
# Content-Type: image/jpeg
# Content-Disposition: inline; filename="..."

Kami menyalin media dari Meta ke penyimpanan kami sendiri saat pesan masuk diterima — persis karena media id dan URL unduhan sementara dari Meta kedaluwarsa cepat. Kalau salinannya belum selesai tersimpan (jarang, biasanya karena delay singkat saat ingest), respons 404 dengan {"error":"media_not_available"} — coba lagi beberapa saat kemudian. mediaId yang tidak dikenal atau bukan milik organisasi Anda juga mengembalikan 404 dengan {"error":"not_found"}.

Tandai Pesan Sebagai Dibaca
POST /api/v1/conversations/:id/read — scope messages:send

Menandai pesan masuk terakhir pada percakapan ini sebagai dibaca di sisi Meta — efeknya sama seperti saat staff membuka percakapan itu di dashboard: pelanggan akan melihat centang biru (read receipt resmi WhatsApp) pada pesan mereka.

Request

curl -X POST https://myaccount.axisflow.id/api/v1/conversations/<conversationId>/read \
  -H "Authorization: Bearer axf_..."

# Response
{
  "success": true,
  "wamid": "wamid.HBg..."
}

Tidak perlu body — cukup conversationId di path. wamid pada respons adalah pesan masuk terakhir yang ditandai (null kalau belum ada pesan masuk sama sekali pada percakapan ini). Kalau panggilan ke Meta gagal, kami tetap mengembalikan success: true dan mereset unread count di sisi kami (sama seperti perilaku dashboard) — cek wamid untuk memastikan ada pesan yang sebenarnya ditandai.

Menerima Event (Webhooks)
Supaya CRM Anda bisa reaktif — pesan masuk, status terkirim, kontak baru, dll — tanpa polling.

Registrasi endpoint webhook dilakukan di Settings → Webhooks (belum ada endpoint API untuk mendaftarkan webhook secara programatik — ini murni lewat dashboard). Maksimal 5 webhook per organisasi. Setiap webhook punya signingSecret sendiri, ditampilkan sekali saat dibuat.

Bentuk payload

Envelope

{
  "id": "evt_...",
  "type": "message.received",
  "createdAt": "2026-08-14T03:12:00.000Z",
  "organizationId": "...",
  "data": { /* lihat contoh per event di bawah */ }
}

Isi data untuk message.received dan message.status sengaja mengikuti bentuk messages[]/statuses[] milik WhatsApp Cloud API sendiri (field from, id, text.body, objek media, recipient_id, dst) — kalau Anda sudah pernah integrasi langsung dengan Meta, parsernya bisa dipakai ulang. organizationId, conversationId, dan messageId tidak ada di Meta — itu tambahan milik AxisFlow untuk referensi internal Anda.

Kunci pencocokan antar event: data.id (wamid dari WhatsApp) adalah satu-satunya field yang muncul konsisten di message.received, message.status, dan di response POST /api/v1/messages (sebagai wamid). Simpan nilai ini untuk mencocokkan pesan masuk/keluar dengan status pengirimannya nanti — data.messageId hanya ada di message.received dan tidak berguna untuk pencocokan ini.

data — message.received (teks)

{
  "organizationId": "cme...",
  "conversationId": "cme...",
  "messageId": "cme...",
  "from": "628123456789",
  "id": "wamid.HBgLxxxxxxxxxxxxxxxxxxxxxxxxxxx=",
  "timestamp": "2026-08-21T10:17:15.000Z",
  "type": "text",
  "text": { "body": "Halo, saya ingin tanya produk." },
  "contacts": [
    { "profile": { "name": "Budi Santoso" }, "wa_id": "628123456789" }
  ]
}

data — message.received (gambar)

{
  "organizationId": "cme...",
  "conversationId": "cme...",
  "messageId": "cme...",
  "from": "628123456789",
  "id": "wamid.HBgLxxxxxxxxxxxxxxxxxxxxxxxxxxx=",
  "timestamp": "2026-08-21T10:17:15.000Z",
  "type": "image",
  "image": {
    "id": "1234567890123456",
    "mime_type": "image/jpeg",
    "sha256": "...",
    "caption": "Cek produk baru ini"
  },
  "contacts": [
    { "profile": { "name": "Budi Santoso" }, "wa_id": "628123456789" }
  ]
}

Untuk tipe media (image/video/document/audio/sticker), field image.id dkk. adalah media id milik Meta, bukan URL yang bisa langsung diunduh. Anda tidak perlu access token WABA/Meta terpisah untuk mengambil bytes-nya — cukup panggil GET /api/v1/media/{mediaId} dengan API key Anda, kami sudah menyimpan salinannya saat pesan masuk.

data — message.status

{
  "organizationId": "cme...",
  "id": "wamid.HBgLxxxxxxxxxxxxxxxxxxxxxxxxxxx=",
  "status": "read",
  "timestamp": "2026-08-21T10:17:21.000Z",
  "recipient_id": "628123456789"
}

data — message.status (gagal terkirim)

{
  "organizationId": "cme...",
  "id": "wamid.HBgLxxxxxxxxxxxxxxxxxxxxxxxxxxx=",
  "status": "failed",
  "timestamp": "2026-08-21T10:17:21.000Z",
  "recipient_id": "628123456789",
  "errors": [{ "code": 131026, "title": "Message undeliverable" }]
}

status memakai nilai mentah dari Meta (huruf kecil): sent/delivered/read/failed. errors hanya muncul kalau status adalah failed.

Verifikasi signature

Setiap request membawa header X-Axisflow-Signature: t=<unix_seconds>,v1=<hex>, HMAC-SHA-256 dari string {t}.{raw_body} memakai signingSecret Anda — pola yang sama dengan Stripe.

Verifikasi (Node.js)

const crypto = require("crypto");

function verifyAxisflowSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("="))
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return expected === parts.v1;
}

Request yang gagal (5xx, timeout, atau 408/429) diulang otomatis hingga 3x (jeda 1 detik, 5 detik, 25 detik). Status 4xx lain tidak diulang. Timeout kirim: 10 detik.

Daftar event

EventTerjadi saat
message.receivedPesan masuk baru dari kontak
message.statusStatus pesan keluar berubah (sent/delivered/read/failed)
conversation.createdPercakapan baru dibuat
conversation.assignedPercakapan di-assign ke agent
conversation.status_changedStatus percakapan berubah (open/pending/resolved)
contact.createdKontak baru dibuat
contact.updatedData kontak diperbarui
broadcast.completedBroadcast selesai dikirim
wallet.low_balanceSaldo wallet di bawah ambang batas
wallet.topped_upTop-up wallet berhasil
template.status_changedTerdaftar tapi belum aktif dipicu di sistem — jangan diandalkan dulu
phone_number.quality_changedTerdaftar tapi belum aktif dipicu di sistem — jangan diandalkan dulu

Anda bisa mengirim contoh payload untuk tiap event dari halaman Webhooks (tombol "Send test event") untuk melihat bentuk data sebenarnya sebelum integrasi.

Error & Batasan

Semua error berbentuk {"error":"<kode>", ...detail}. Kode umum di seluruh endpoint: unauthorized (401, key hilang/ tidak valid/revoked/kedaluwarsa), insufficient_scope (403).

Rate limit: belum ada pembatasan rate di sisi kami saat ini — mohon tetap terapkan retry/backoff yang wajar di sisi Anda karena ini bisa berubah kapan saja tanpa pemberitahuan.

Versi: API ini di-versikan lewat prefix path (/api/v1). Belum ada v2.