{
  "info": {
    "name": "Wardia — WhatsApp Gateway API v1",
    "description": "Kontrak API publik Wardia (v1).\n\nSetup sekali:\n1. Buka tab Variables di collection ini.\n2. Isi `api_key` dengan key dari Portal → Gateway → API Keys (format ardw_live_xxx).\n3. Jalankan `Devices / Create device (QR)` — script-nya otomatis mengisi variabel `device_uuid`.\n4. Jalankan `Devices / Get pairing QR` berulang sampai device connected, lalu `Messages / Send text`.\n\nCatatan: `Idempotency-Key` di POST /messages WAJIB. Request di collection ini mengisinya otomatis dengan UUID acak lewat pre-request script — di sistemmu sendiri, pakai ID pesan logis (mis. order-1042), bukan acak, supaya retry benar-benar idempoten.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [{ "key": "token", "value": "{{api_key}}", "type": "string" }]
  },
  "variable": [
    { "key": "base_url", "value": "https://wardia.id", "type": "string" },
    { "key": "api_key", "value": "ardw_live_ganti_dengan_key_kamu", "type": "string" },
    { "key": "device_uuid", "value": "", "type": "string" },
    { "key": "message_id", "value": "", "type": "string" },
    { "key": "broadcast_id", "value": "", "type": "string" },
    { "key": "to", "value": "6281290008646", "type": "string" }
  ],
  "item": [
    {
      "name": "Devices",
      "item": [
        {
          "name": "Create device (QR / jalur hemat)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201 created', () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "if (body.device_uuid) {",
                  "  pm.collectionVariables.set('device_uuid', body.device_uuid);",
                  "  console.log('device_uuid disimpan:', body.device_uuid);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": { "raw": "{{base_url}}/v1/devices", "host": ["{{base_url}}"], "path": ["v1", "devices"] },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"label\": \"toko-sari-admin\"\n}"
            },
            "description": "→ 201 { device_uuid, status: \"provisioned\" }. Lanjut ambil QR.\n402 device_limit_reached = sudah di batas max_devices paket."
          }
        },
        {
          "name": "Create device (Meta Cloud API resmi)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": { "raw": "{{base_url}}/v1/devices", "host": ["{{base_url}}"], "path": ["v1", "devices"] },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"engine\": \"meta_cloud\",\n  \"label\": \"toko-sari-admin\",\n  \"meta_phone_number_id\": \"123456789012345\",\n  \"meta_access_token\": \"EAAxxxxxxxxxxxx\"\n}"
            },
            "description": "Kredensial diverifikasi live ke Graph API sebelum device dibuat → langsung status connected, tanpa QR.\n422 invalid_meta_credentials = verifikasi gagal, device tidak dibuat."
          }
        },
        {
          "name": "List devices",
          "request": {
            "method": "GET",
            "url": { "raw": "{{base_url}}/v1/devices", "host": ["{{base_url}}"], "path": ["v1", "devices"] }
          }
        },
        {
          "name": "Get device",
          "request": {
            "method": "GET",
            "url": { "raw": "{{base_url}}/v1/devices/{{device_uuid}}", "host": ["{{base_url}}"], "path": ["v1", "devices", "{{device_uuid}}"] }
          }
        },
        {
          "name": "Get pairing QR",
          "request": {
            "method": "GET",
            "url": { "raw": "{{base_url}}/v1/devices/{{device_uuid}}/qr", "host": ["{{base_url}}"], "path": ["v1", "devices", "{{device_uuid}}", "qr"] },
            "description": "200 = qr berisi data URL PNG (berlaku ~90 detik, jangan di-cache).\n202 = QR belum siap, polling lagi.\n409 already_connected = device sudah tersambung."
          }
        },
        {
          "name": "Rotate Meta access token",
          "request": {
            "method": "PATCH",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": { "raw": "{{base_url}}/v1/devices/{{device_uuid}}/meta-credentials", "host": ["{{base_url}}"], "path": ["v1", "devices", "{{device_uuid}}", "meta-credentials"] },
            "body": { "mode": "raw", "raw": "{\n  \"meta_access_token\": \"EAA-token-baru\"\n}" },
            "description": "Hanya untuk device engine=meta_cloud. Dipanggil ke device jalur hemat → 422 not_meta_cloud_device."
          }
        },
        {
          "name": "Delete device (logout)",
          "request": {
            "method": "DELETE",
            "url": { "raw": "{{base_url}}/v1/devices/{{device_uuid}}", "host": ["{{base_url}}"], "path": ["v1", "devices", "{{device_uuid}}"] }
          }
        }
      ]
    },
    {
      "name": "Messages",
      "item": [
        {
          "name": "Send text",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Demo saja: key acak per klik supaya bisa kirim berkali-kali.",
                  "// Di sistemmu, pakai ID pesan logis (mis. order-1042-resi) —",
                  "// itu yang bikin retry benar-benar tidak dobel kirim.",
                  "pm.collectionVariables.set('idempotency_key', 'postman-' + Date.now() + '-' + Math.random().toString(16).slice(2));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('202 accepted', () => pm.response.to.have.status(202));",
                  "const body = pm.response.json();",
                  "if (body.id) { pm.collectionVariables.set('message_id', body.id); }",
                  "// Sisa jatah per menit dari plan — 429 kalau habis.",
                  "console.log('rate limit sisa:', pm.response.headers.get('X-RateLimit-Remaining'), '/', pm.response.headers.get('X-RateLimit-Limit'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Idempotency-Key", "value": "{{idempotency_key}}" }
            ],
            "url": { "raw": "{{base_url}}/v1/messages", "host": ["{{base_url}}"], "path": ["v1", "messages"] },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"device_uuid\": \"{{device_uuid}}\",\n  \"to\": \"{{to}}\",\n  \"type\": \"text\",\n  \"body\": \"Halo, pesanan Anda sudah dikirim.\"\n}"
            },
            "description": "202 = diterima & diantrikan (bukan 'sudah sampai'). Status akhir lewat webhook message.status atau GET /messages/{id}.\n\nRate limit per menit sesuai plan — respons bawa X-RateLimit-Limit / X-RateLimit-Remaining. Lewat batas → 429 rate_limited + header Retry-After (jendela 60 detik, tunggu jangan retry ketat). Request yang kena 429 tidak membuat pesan & tidak memotong kuota bulanan.\n\n422 idempotency_key_required = header Idempotency-Key hilang.\n409 device_disconnected · 402 quota_exceeded · 422 invalid_phone."
          }
        },
        {
          "name": "Send image (media)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": ["pm.collectionVariables.set('idempotency_key', 'postman-img-' + Date.now());"]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Idempotency-Key", "value": "{{idempotency_key}}" }
            ],
            "url": { "raw": "{{base_url}}/v1/messages", "host": ["{{base_url}}"], "path": ["v1", "messages"] },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"device_uuid\": \"{{device_uuid}}\",\n  \"to\": \"{{to}}\",\n  \"type\": \"image\",\n  \"body\": \"Katalog Agustus\",\n  \"media_url\": \"https://example.com/katalog.jpg\"\n}"
            },
            "description": "type: text | image | document | audio | video. Untuk media, body jadi caption."
          }
        },
        {
          "name": "Send buttons (meta_cloud only)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": ["pm.collectionVariables.set('idempotency_key', 'postman-btn-' + Date.now());"]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Idempotency-Key", "value": "{{idempotency_key}}" }
            ],
            "url": { "raw": "{{base_url}}/v1/messages", "host": ["{{base_url}}"], "path": ["v1", "messages"] },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"device_uuid\": \"{{device_uuid}}\",\n  \"to\": \"{{to}}\",\n  \"type\": \"button\",\n  \"body\": \"Pesanan #1042 sudah sampai?\",\n  \"interactive\": {\n    \"footer\": \"Toko Sari\",\n    \"buttons\": [\n      { \"id\": \"yes\", \"title\": \"Sudah\" },\n      { \"id\": \"no\", \"title\": \"Belum\" }\n    ]\n  }\n}"
            },
            "description": "Maksimum 3 tombol, title maks 20 karakter. body WAJIB (teks di atas tombol).\n\nHANYA device engine=meta_cloud → device jalur hemat (baileys) ditolak 422 interactive_not_supported.\n\nTap tombol balik ke webhook message.received sebagai pesan TEKS berisi judul tombol ('Sudah') — rule auto-reply keyword langsung bekerja tanpa kode tambahan."
          }
        },
        {
          "name": "Send list (meta_cloud only)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": ["pm.collectionVariables.set('idempotency_key', 'postman-list-' + Date.now());"]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Idempotency-Key", "value": "{{idempotency_key}}" }
            ],
            "url": { "raw": "{{base_url}}/v1/messages", "host": ["{{base_url}}"], "path": ["v1", "messages"] },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"device_uuid\": \"{{device_uuid}}\",\n  \"to\": \"{{to}}\",\n  \"type\": \"list\",\n  \"body\": \"Mau lihat produk yang mana?\",\n  \"interactive\": {\n    \"button_text\": \"Lihat katalog\",\n    \"sections\": [\n      {\n        \"title\": \"Skincare\",\n        \"rows\": [\n          { \"id\": \"sku-1\", \"title\": \"Serum\", \"description\": \"30ml\" },\n          { \"id\": \"sku-2\", \"title\": \"Toner\" }\n        ]\n      }\n    ]\n  }\n}"
            },
            "description": "Maks 10 section, 10 baris/section. row.title maks 24 karakter, description maks 72."
          }
        },
        {
          "name": "Get message status",
          "request": {
            "method": "GET",
            "url": { "raw": "{{base_url}}/v1/messages/{{message_id}}", "host": ["{{base_url}}"], "path": ["v1", "messages", "{{message_id}}"] },
            "description": "status: queued → sent → delivered → read, atau failed."
          }
        }
      ]
    },
    {
      "name": "Webhooks",
      "item": [
        {
          "name": "Register webhook",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": { "raw": "{{base_url}}/v1/webhooks", "host": ["{{base_url}}"], "path": ["v1", "webhooks"] },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://tokomu.example.com/wa-webhook\",\n  \"events\": [\"message.received\", \"message.status\", \"device.status\"]\n}"
            },
            "description": "PENTING: `secret` di respons ini cuma muncul SEKALI — simpan sekarang. GET /v1/webhooks tidak mengembalikannya lagi.\n\nVerifikasi tiap event: X-Ardia-Signature = 'sha256=' + hash_hmac('sha256', RAW_BODY, secret)."
          }
        },
        {
          "name": "List webhooks",
          "request": {
            "method": "GET",
            "url": { "raw": "{{base_url}}/v1/webhooks", "host": ["{{base_url}}"], "path": ["v1", "webhooks"] }
          }
        }
      ]
    },
    {
      "name": "Contacts",
      "item": [
        {
          "name": "Create / update contact (upsert)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": { "raw": "{{base_url}}/v1/contacts", "host": ["{{base_url}}"], "path": ["v1", "contacts"] },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"phone\": \"081290008646\",\n  \"name\": \"Budi\",\n  \"tags\": [\"vip\", \"reseller\"]\n}"
            },
            "description": "Upsert by nomor — aman dipanggil berkali-kali. 201 baru, 200 update. Tag yang belum ada dibuat otomatis."
          }
        },
        {
          "name": "List contacts",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{base_url}}/v1/contacts?tag=vip&per_page=50",
              "host": ["{{base_url}}"],
              "path": ["v1", "contacts"],
              "query": [
                { "key": "tag", "value": "vip", "description": "opsional" },
                { "key": "per_page", "value": "50", "description": "default 50, maksimum 200" }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Broadcasts",
      "item": [
        {
          "name": "Create broadcast",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "if (body.id) { pm.collectionVariables.set('broadcast_id', body.id); }"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": { "raw": "{{base_url}}/v1/broadcasts", "host": ["{{base_url}}"], "path": ["v1", "broadcasts"] },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"device_uuid\": \"{{device_uuid}}\",\n  \"name\": \"Promo Agustus\",\n  \"message_template\": \"Halo {nama}, ada {promo|diskon|penawaran} spesial buat kamu!\",\n  \"tags\": [\"vip\"],\n  \"delay_min_seconds\": 5,\n  \"delay_max_seconds\": 20,\n  \"scheduled_at\": null\n}"
            },
            "description": "Target: tags ATAU contact_ids (wajib salah satu).\nmessage_template: spintax {a|b|c} + variabel {nama}.\ndelay_min/max: jeda acak antar pesan — guardrail anti-banned, jangan diisi 0.\nscheduled_at diisi → status scheduled, dijalankan penjadwal.\n\n422 no_recipients_targeted · 422 no_recipients_matched."
          }
        },
        {
          "name": "Get broadcast report",
          "request": {
            "method": "GET",
            "url": { "raw": "{{base_url}}/v1/broadcasts/{{broadcast_id}}", "host": ["{{base_url}}"], "path": ["v1", "broadcasts", "{{broadcast_id}}"] }
          }
        },
        {
          "name": "Resume paused broadcast",
          "request": {
            "method": "POST",
            "url": { "raw": "{{base_url}}/v1/broadcasts/{{broadcast_id}}/resume", "host": ["{{base_url}}"], "path": ["v1", "broadcasts", "{{broadcast_id}}", "resume"] },
            "description": "Broadcast auto-pause kalau device putus di tengah jalan. Resume butuh status paused DAN device connected lagi.\n422 broadcast_not_paused · 409 device_not_connected."
          }
        }
      ]
    },
    {
      "name": "Usage",
      "item": [
        {
          "name": "Get monthly usage",
          "request": {
            "method": "GET",
            "url": { "raw": "{{base_url}}/v1/usage", "host": ["{{base_url}}"], "path": ["v1", "usage"] },
            "description": "messages_remaining: null = tanpa batas."
          }
        }
      ]
    }
  ]
}
