Nadi API Documentation

Integrate Nadi's WhatsApp AI engine into your product. RESTful JSON API with Bearer token authentication, real-time webhooks, and multi-tenant AI chat processing.

Base URL https://getnadi.id

Overview

Nadi is a multi-tenant WhatsApp AI platform. Each tenant (customer) has their own AI agent configuration, contact list, templates, and usage quota. The API supports:

All requests and responses use Content-Type: application/json. Max request body size: 1 MB.

Authentication

Authenticate by including a Bearer token in the Authorization header. Obtain a token via POST /api/auth/login.

Authorization: Bearer {your-token-here}

Tokens expire after 8 hours. There are no refresh tokens — re-authenticate when expired.

Role-Based Access

RoleAccessDescription
adminAdminFull platform management — all customers, billing, analytics
customerCustomerOwn tenant only — agent config, contacts, logs, broadcasting
PublicLogin, register, forgot password, webhook

Error Handling

All errors return a JSON object with error.code and error.message:

{
  "error": {
    "code": "INVALID_LOGIN",
    "message": "Nomor atau password salah."
  }
}

HTTP Status Codes

StatusMeaningCommon Codes
200Success
201Created
400Validation errorVALIDATION_ERROR, MISSING_FIELDS, INVALID_PHONE
401UnauthorizedUNAUTHORIZED, INVALID_LOGIN, SESSION_EXPIRED
403ForbiddenFORBIDDEN, LOGIN_DISABLED, PLAN_GATE
404Not foundNOT_FOUND
409ConflictDUPLICATE, PHONE_TAKEN, NAME_TAKEN
429Rate limitedRATE_LIMITED, CHAT_LIMIT_REACHED, TOO_MANY_OTP
500Server errorSERVER_ERROR

Rate Limiting

EndpointLimitWindowKey
POST /api/auth/login10 requests15 minIP address
POST /api/register10 requests1 hourIP address
POST /api/auth/forgot3 OTPs15 minPhone number
All write ops (/api/*)60 requests5 minAuth token

When rate limited, the API returns 429 with code RATE_LIMITED.

Auth Endpoints

POST /api/auth/login Authenticate and get token Public

Request Body

FieldTypeDescription
phonestringPhone number (e.g., 628123456789)
passwordstringAccount password

Response (200)

{
  "data": {
    "token": "a1b2c3d4-uuid",
    "role": "customer",
    "category": "Gym",
    "categoryType": 1,
    "profile": {
      "id": "cust_abc12345",
      "name": "Fitness Pro",
      "phone": "628123456789"
    }
  }
}

Errors

StatusCodeWhen
401INVALID_LOGINWrong phone or password
403LOGIN_DISABLEDAccount access revoked by admin
429RATE_LIMITEDToo many attempts (10/15min)

cURL Example

curl -X POST https://getnadi.id/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"phone":"628123456789","password":"mypassword"}'

MFA (Two-Factor Authentication)

Google Authenticator / Authy compatible TOTP. Mandatory for all users. Secrets encrypted at rest with AES-256-GCM.

POST /api/auth/mfa/setup Generate QR + recovery codes Partial Token

Generates TOTP secret, QR code (data URL), and 10 one-time recovery codes. Requires a partial session token (obtained after login for users with mfaStatus: not_setup).

Response (200)

{
  "data": {
    "qrCode": "data:image/png;base64,iVBOR...",
    "secret": "BWHS5EZNN6DEWFAHXIAU7UAV6W",
    "recoveryCodes": ["FC2A-25BF", "7873-B7B1", ...],
    "uri": "otpauth://totp/Nadi:628xxx?secret=...&issuer=Nadi"
  }
}
POST /api/auth/mfa/confirm Verify first code, enable MFA Partial Token

Request Body

FieldTypeDescription
codestring6-digit TOTP from authenticator app

Response (200)

{ "data": { "ok": true, "message": "MFA berhasil diaktifkan!", "mfaStatus": "enabled" } }

Errors

StatusCodeWhen
400INVALID_CODECode not 6 digits or wrong
POST /api/auth/mfa/verify Login MFA challenge Partial Token (5min TTL)

Verify TOTP code or recovery code during login. On success, upgrades partial token to full session. Rate limited: 5 attempts per 15 minutes.

Request Body (TOTP)

{ "code": "123456" }

Request Body (Recovery)

{ "recoveryCode": "FC2A-25BF" }

Response (200)

{
  "data": {
    "ok": true,
    "token": "uuid",
    "role": "customer",
    "profile": { "id": "cust_abc", "name": "Budi", "phone": "628xxx" },
    "forceResetup": false
  }
}

Errors

StatusCodeWhen
400WRONG_CODEInvalid TOTP or recovery code
401MFA_EXPIREDPartial token expired (5 min)
423MFA_LOCKEDToo many attempts (15 min lockout)
POST /api/auth/mfa/disable Disable MFA (requires current code) Full Token

Request Body

{ "code": "123456" }

Response (200)

{ "data": { "ok": true, "message": "MFA berhasil dinonaktifkan." } }

AI / Chat Endpoints

POST /api/ai/chat Send message to AI agent Customer

Core AI endpoint. Processes a message through the tenant's AI agent — checks templates, cache, then Nadi AI engine. Enforces plan quota limits.

Request Body

FieldTypeDescription
customerIdstringTenant ID (must match authenticated user)
messagestringUser's message text
phonestringSender's phone for whitelist lookup
conversationHistoryarrayPrevious messages [{role, content}]
isPreviewbooleanTest mode — uses separate quota

Response (200)

{
  "data": {
    "reply": "Halo Kak! Selamat datang di Fitness Pro...",
    "source": "ai",
    "model": "nadi-ai-v4",
    "tokens": { "input": 850, "output": 120 },
    "cost": "0.004350",
    "usage": { "chats": 42, "chatLimit": 500 }
  }
}

source values: ai (Nadi AI engine), template (matched template, zero cost), cache (cached response, zero cost)

Errors

StatusCodeWhen
403FORBIDDENcustomerId doesn't match session
429CHAT_LIMIT_REACHEDMonthly chat quota exceeded
429TOKEN_LIMIT_REACHEDMonthly token quota exceeded
POST /api/webhook/{customerId} External integration webhook Public

Send messages from external systems (CRM, website chat widget, etc.) to be processed by the tenant's AI agent. No authentication required — tenant is identified by the URL.

Request Body

FieldTypeDescription
phone | from | senderstringSender's phone number
message | text | bodystringMessage content

Response (200)

{
  "data": {
    "ok": true,
    "reply": "Terima kasih sudah menghubungi...",
    "source": "ai"
  }
}

Integration Example

// Website chat widget integration
const response = await fetch('https://getnadi.id/api/webhook/cust_abc12345', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    phone: '628123456789',
    message: 'Halo, saya mau tanya tentang membership'
  })
});
const { data } = await response.json();
// data.reply = AI-generated response

Customer Endpoints

GET /api/customer/dashboard Full dashboard data Customer

Returns all data needed to render the customer dashboard — profile, contacts, limits, templates, policies, activity, and plan gates.

Response (200)

{
  "data": {
    "customer": { "id", "name", "plan", "status", ... },
    "bundle": {
      "contacts": [...],
      "limitVal": 25,
      "activeCount": 12,
      "unlimited": false
    },
    "limits": {
      "chatLimit": 500,
      "chatsUsed": 42,
      "chatLimitReached": false
    },
    "gates": {
      "salesPlaybook": false,
      "escalation": false,
      "advancedAnalytics": false,
      "exportCsv": true
    },
    "templates": [...],
    "policies": [...],
    "activity": [...]
  }
}
GET /api/customer/whitelist List contacts Customer

Response (200)

{
  "data": {
    "contacts": [
      {
        "id": "wl_a1b2c3",
        "phone": "628123456789",
        "name": "Budi",
        "label": "Member Gold",
        "leadStatus": "warm",
        "chatCount": 15,
        "isActive": true
      }
    ],
    "limitVal": 25,
    "activeCount": 12,
    "unlimited": false
  }
}
POST /api/customer/whitelist Add contact Customer

Request Body

FieldTypeDescription
phonestringValid phone (min 10 digits)
namestringContact name (min 2 chars)
labelstringCustom label (e.g., "Member Gold")
notesstringNotes about the contact

Errors

StatusCodeWhen
400DUPLICATEPhone already in contacts
403WHITELIST_LIMIT_REACHEDPlan limit (25/50/unlimited)
GET /api/customer/subscription Subscription info Customer

Response (200)

{
  "data": {
    "plan": "growth",
    "billingCycle": "monthly",
    "subscriptionStart": "2026-04-01T00:00:00Z",
    "subscriptionEnd": "2026-05-01T00:00:00Z",
    "daysRemaining": 14,
    "billingAmount": 449000,
    "subscriptionStatus": "active"
  }
}

WhatsApp Connection

POST /api/customer/wa/connect Start QR session Customer

Initiates a WhatsApp Web session. Returns a QR code to scan from your phone's WhatsApp app.

Response (200)

{
  "data": {
    "status": "connecting",
    "message": "Scan QR code dari WhatsApp di HP Anda."
  }
}
GET /api/customer/wa/status Connection status + QR Customer

Response (200)

{
  "data": {
    "status": "connecting",
    "qr": "data:image/png;base64,iVBOR..."
  }
}

status values: connected, connecting, disconnected. QR is only present during connecting state.

WhatsApp Webhook (Meta Cloud API)

GET /webhook/whatsapp Meta verification handshake Public

Used by Meta to verify your webhook URL during setup. Configure in Meta Developer Console → WhatsApp → Configuration → Callback URL.

Query Parameters

ParamTypeDescription
hub.modestringMust be "subscribe"
hub.verify_tokenstringMust match WA_VERIFY_TOKEN env
hub.challengestringChallenge to echo back
POST /webhook/whatsapp Receive incoming messages HMAC-SHA256

Receives incoming WhatsApp messages from Meta Cloud API. Automatically resolves the tenant, processes through AI, and sends the reply. Returns 200 immediately — processing is async.

Security

Payload signature verified via X-Hub-Signature-256 header using META_APP_SECRET. Message deduplication prevents duplicate processing on Meta retries.

Processing Pipeline

Incoming Message
  → Deduplication check (skip if already processed)
  → Tenant resolution (by phone_number_id)
  → Guard stack: bot enabled? active? whitelisted? quota?
  → Template matching (zero cost) → Cache check → Nadi AI engine
  → Send reply via WhatsApp → Log usage

Health Check

GET /health Server health Public
{
  "status": "ok",
  "version": "3.0.0",
  "uptime": 86400
}

© 2026 Nadi Platform — API v3.0 — getnadi.id