{
  "info": {
    "name": "Dynamic WhatsApp Evolution",
    "_postman_id": "a1b2c3d4-5e6f-4a5b-9c7d-0e1f2a3b4c5d",
    "description": "Thin queue in front of Evolution API — **admin** surface (global `{{apiKey}}`).\n\nCovers: register/delete instances, full Evolution lifecycle (create/delete/connect/restart/logout + the new provision-bundle), tune rate limits, mint/rotate/revoke per-instance scoped keys, browse every instance's queue, manage any instance's webhook config.\n\n**For the scoped (instance-owner) surface**, import the separate `dynamic-whatsapp-evolution-owner.postman_collection.json` — it's what to hand a SaaS-provisioned tenant alongside their `wq_…` key.\n\nSet collection variables: `baseUrl`, `apiKey`, `instanceId`, `instanceName`, `webhookSecret`.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    { "key": "baseUrl", "value": "http://localhost:8081", "type": "string" },
    { "key": "apiKey", "value": "dev-api-key-change-me", "type": "string" },
    { "key": "instanceId", "value": "1", "type": "string" },
    { "key": "instanceName", "value": "prod1", "type": "string" },
    { "key": "evolutionInstanceId", "value": "", "type": "string" },
    { "key": "instanceApiKey", "value": "", "type": "string" },
    { "key": "messageId", "value": "1", "type": "string" },
    { "key": "remoteJid", "value": "201012345678@s.whatsapp.net", "type": "string" }
  ],
  "auth": {
    "type": "apikey",
    "apikey": [
      { "key": "key", "value": "X-API-Key", "type": "string" },
      { "key": "value", "value": "{{apiKey}}", "type": "string" },
      { "key": "in", "value": "header", "type": "string" }
    ]
  },
  "item": [
    {
      "name": "Health (no auth)",
      "item": [
        {
          "name": "Liveness",
          "request": {
            "method": "GET",
            "auth": { "type": "noauth" },
            "url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], "path": ["health"] }
          }
        },
        {
          "name": "Live alias",
          "request": {
            "method": "GET",
            "auth": { "type": "noauth" },
            "url": { "raw": "{{baseUrl}}/health/live", "host": ["{{baseUrl}}"], "path": ["health", "live"] }
          }
        },
        {
          "name": "Readiness (DB + Redis)",
          "request": {
            "method": "GET",
            "auth": { "type": "noauth" },
            "url": { "raw": "{{baseUrl}}/health/ready", "host": ["{{baseUrl}}"], "path": ["health", "ready"] }
          }
        }
      ]
    },
    {
      "name": "Admin (uses {{apiKey}})",
      "description": "Full surface. Authed with the global API key (collection default).",
      "item": [
        {
          "name": "Instances",
          "item": [
            {
              "name": "Create instance",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) {",
                      "  const body = pm.response.json();",
                      "  pm.collectionVariables.set('instanceId', String(body.id));",
                      "  pm.test('id saved', () => pm.expect(body.id).to.be.a('number'));",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"evolution_instance_id\": \"{{evolutionInstanceId}}\",\n  \"warmup_days\": 7\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/instances", "host": ["{{baseUrl}}"], "path": ["api", "instances"] }
              }
            },
            {
              "name": "List instances",
              "request": {
                "method": "GET",
                "url": { "raw": "{{baseUrl}}/api/instances", "host": ["{{baseUrl}}"], "path": ["api", "instances"] }
              }
            },
            {
              "name": "Get instance",
              "request": {
                "method": "GET",
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}"]
                }
              }
            },
            {
              "name": "Update instance (is_active / warmup_until)",
              "request": {
                "method": "PUT",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": { "mode": "raw", "raw": "{\n  \"is_active\": true,\n  \"warmup_until\": null\n}" },
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}"]
                }
              }
            },
            {
              "name": "Mint / rotate scoped API key",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 201) {",
                      "  pm.collectionVariables.set('instanceApiKey', pm.response.json().api_key);",
                      "  pm.test('api_key saved', () => pm.expect(pm.collectionVariables.get('instanceApiKey')).to.match(/^wq_/));",
                      "}"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}/api-key",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}", "api-key"]
                }
              }
            },
            {
              "name": "Revoke scoped API key",
              "request": {
                "method": "DELETE",
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}/api-key",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}", "api-key"]
                }
              }
            },
            {
              "name": "List groups (simplified)",
              "request": {
                "method": "GET",
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}/groups?page=1&page_size=50&all=false",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}", "groups"],
                  "query": [
                    { "key": "page", "value": "1" },
                    { "key": "page_size", "value": "50", "description": "1..200" },
                    { "key": "all", "value": "false", "description": "true → ignore pagination, return every group" }
                  ]
                },
                "description": "Thin read-through to Evolution's /group/fetchAllGroups, simplified to {id, subject}. 400 if the instance isn't linked to Evolution; 502 if Evolution is unreachable. Scoped key works too — this is how a user discovers their groups without holding the Evolution token."
              }
            },
            {
              "name": "Delete instance",
              "request": {
                "method": "DELETE",
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}"]
                }
              }
            }
          ]
        },
        {
          "name": "Evolution lifecycle",
          "item": [
            {
              "name": "List Evolution instances (with linked flag)",
              "request": {
                "method": "GET",
                "url": { "raw": "{{baseUrl}}/api/evolution/instances", "host": ["{{baseUrl}}"], "path": ["api", "evolution", "instances"] },
                "description": "Admin-only inventory browse — Evolution's own instances overlaid with linked/local_instance_id."
              }
            },
            {
              "name": "Create Evolution instance (+ local row)",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"prod1\",\n  \"warmup_days\": 7\n  // \"number\": \"+201012345678\"          // optional: pairing-code flow\n  // \"mint_scoped_key\": true,             // optional: also mint a wq_ scoped key (returned once)\n  // \"webhook\": {                          // optional: also register webhook + repoint Evolution\n  //   \"target_url\": \"https://saas.example.com/wa-cb/123\",\n  //   \"events\": [\"MESSAGES_UPSERT\",\"MESSAGES_UPDATE\"],\n  //   \"secret\": \"min8chars\",\n  //   \"is_active\": true\n  // }\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/evolution/instances", "host": ["{{baseUrl}}"], "path": ["api", "evolution", "instances"] },
                "description": "Creates the instance on Evolution AND registers the local catalog row + rate-limit config. Returns qrcode_base64 (PNG) or pairing_code for the operator to pair the phone.\n\nOPTIONAL BUNDLE FIELDS — SaaS provisions a tenant in one call:\n- `mint_scoped_key: true` mints a wq_ scoped key on the new row; response carries `scoped_key` plaintext ONCE — persist server-side.\n- `webhook` registers the webhook config and points Evolution at `${PUBLIC_URL}/webhook/evolution/{name}`. Same body as PUT /api/instances/{id}/webhook. Requires PUBLIC_URL set on the gateway.\n\nUpfront validation: bad webhook input (missing PUBLIC_URL / secret) returns 400/422 BEFORE any side effects. Webhook step runs before mint, so a webhook failure can't strand a key the caller never received."
              }
            },
            {
              "name": "Provision tenant (full bundle: create + mint + webhook)",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"tenant-acme\",\n  \"warmup_days\": 7,\n  \"mint_scoped_key\": true,\n  \"webhook\": {\n    \"target_url\": \"https://saas.example.com/wa-cb/acme\",\n    \"events\": [\"MESSAGES_UPSERT\", \"MESSAGES_UPDATE\", \"CONNECTION_UPDATE\"],\n    \"secret\": \"{{webhookSecret}}\",\n    \"is_active\": true\n  }\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/evolution/instances", "host": ["{{baseUrl}}"], "path": ["api", "evolution", "instances"] },
                "description": "Single-call tenant provisioning for a SaaS / CRM integration. Returns:\n- `local_instance_id` — gateway-side id for subsequent calls\n- `qrcode_base64` / `pairing_code` — hand to the customer to bring their phone online\n- `scoped_key` — plaintext wq_ key, returned ONCE. Persist server-side (encrypted) on the tenant row.\n- `webhook: \"set\"` — confirms Evolution is now pointing at our receiver\n\nPUBLIC_URL must be set on the gateway. Webhook secret min length 8 chars."
              }
            },
            {
              "name": "Connect (fresh QR / re-pair)",
              "request": {
                "method": "POST",
                "url": { "raw": "{{baseUrl}}/api/evolution/instances/{{instanceName}}/connect", "host": ["{{baseUrl}}"], "path": ["api", "evolution", "instances", "{{instanceName}}", "connect"] },
                "description": "Returns a fresh QR base64 for re-pairing a disconnected instance."
              }
            },
            {
              "name": "Restart",
              "request": {
                "method": "POST",
                "url": { "raw": "{{baseUrl}}/api/evolution/instances/{{instanceName}}/restart", "host": ["{{baseUrl}}"], "path": ["api", "evolution", "instances", "{{instanceName}}", "restart"] },
                "description": "Restart the Evolution-side process for this instance. WA session usually survives."
              }
            },
            {
              "name": "Logout (end WA session)",
              "request": {
                "method": "POST",
                "url": { "raw": "{{baseUrl}}/api/evolution/instances/{{instanceName}}/logout", "host": ["{{baseUrl}}"], "path": ["api", "evolution", "instances", "{{instanceName}}", "logout"] },
                "description": "Ends the WhatsApp session. Instance row stays in Evolution; re-pair via Connect."
              }
            },
            {
              "name": "Delete (Evolution + local cascade)",
              "request": {
                "method": "DELETE",
                "url": { "raw": "{{baseUrl}}/api/evolution/instances/{{instanceName}}", "host": ["{{baseUrl}}"], "path": ["api", "evolution", "instances", "{{instanceName}}"] },
                "description": "Deletes on Evolution AND drops the local catalog row (messages + contacts + rate-limit config cascade away). Cannot be undone."
              }
            }
          ]
        },
        {
          "name": "Rate limits",
          "item": [
            {
              "name": "Get rate-limit config",
              "request": {
                "method": "GET",
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}/rate-limit",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}", "rate-limit"]
                }
              }
            },
            {
              "name": "Update rate-limit config",
              "request": {
                "method": "PUT",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"msgs_per_minute\": 8,\n  \"msgs_per_hour\": 200,\n  \"msgs_per_day\": 1000,\n  \"min_delay_seconds\": 5.0,\n  \"max_delay_seconds\": 12.0,\n  \"jitter_factor_min\": 0.8,\n  \"jitter_factor_max\": 1.4,\n  \"new_recipient_penalty\": 2.0,\n  \"warmup_days\": 7,\n  \"warmup_msgs_per_day\": 50,\n  \"circuit_breaker_threshold\": 5,\n  \"circuit_breaker_pause_seconds\": 300\n}"
                },
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}/rate-limit",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}", "rate-limit"]
                }
              }
            },
            {
              "name": "Get live status (counters + CB)",
              "request": {
                "method": "GET",
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}/rate-limit/status",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}", "rate-limit", "status"]
                }
              }
            }
          ]
        },
        {
          "name": "Messages",
          "item": [
            {
              "name": "Send text",
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 202) { pm.collectionVariables.set('messageId', String(pm.response.json().id)); }"
                    ]
                  }
                }
              ],
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"instance_id\": {{instanceId}},\n  \"recipient\": \"01012345678\",\n  \"message_type\": \"text\",\n  \"payload\": { \"text\": \"Hello from the queue\" },\n  \"priority\": 5\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/messages", "host": ["{{baseUrl}}"], "path": ["api", "messages"] }
              }
            },
            {
              "name": "Send media",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"instance_id\": {{instanceId}},\n  \"recipient\": \"01012345678\",\n  \"message_type\": \"media\",\n  \"payload\": {\n    \"mediatype\": \"image\",\n    \"mimetype\": \"image/png\",\n    \"caption\": \"Optional caption\",\n    \"media\": \"https://picsum.photos/600/400.png\",\n    \"fileName\": \"sample.png\"\n  },\n  \"priority\": 5\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/messages", "host": ["{{baseUrl}}"], "path": ["api", "messages"] }
              }
            },
            {
              "name": "Send audio",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"instance_id\": {{instanceId}},\n  \"recipient\": \"01012345678\",\n  \"message_type\": \"audio\",\n  \"payload\": { \"audio\": \"https://example.com/voice.ogg\" }\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/messages", "host": ["{{baseUrl}}"], "path": ["api", "messages"] }
              }
            },
            {
              "name": "Send location",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"instance_id\": {{instanceId}},\n  \"recipient\": \"01012345678\",\n  \"message_type\": \"location\",\n  \"payload\": { \"latitude\": 37.7749, \"longitude\": -122.4194, \"name\": \"SF\", \"address\": \"California\" }\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/messages", "host": ["{{baseUrl}}"], "path": ["api", "messages"] }
              }
            },
            {
              "name": "Send poll",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"instance_id\": {{instanceId}},\n  \"recipient\": \"01012345678\",\n  \"message_type\": \"poll\",\n  \"payload\": { \"name\": \"Favourite color?\", \"values\": [\"red\", \"green\", \"blue\"], \"selectableCount\": 1 }\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/messages", "host": ["{{baseUrl}}"], "path": ["api", "messages"] }
              }
            },
            {
              "name": "Send template (Cloud API)",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"instance_id\": {{instanceId}},\n  \"recipient\": \"01012345678\",\n  \"message_type\": \"template\",\n  \"payload\": {\n    \"name\": \"hello_world\",\n    \"language\": \"en_US\",\n    \"components\": [\n      { \"type\": \"body\", \"parameters\": [ { \"type\": \"text\", \"text\": \"Ahmed\" } ] }\n    ]\n  }\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/messages", "host": ["{{baseUrl}}"], "path": ["api", "messages"] },
                "description": "WhatsApp Business Cloud API only. Business-initiated (outside the 24h window) sends must use a pre-approved template. `name` + `language` are required; `components` fills template variables/buttons. Proxies Evolution's POST /message/sendTemplate/{instance}."
              }
            },
            {
              "name": "Send scheduled",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"instance_id\": {{instanceId}},\n  \"recipient\": \"01012345678\",\n  \"message_type\": \"text\",\n  \"payload\": { \"text\": \"Scheduled message\" },\n  \"scheduled_at\": \"2026-12-31T09:00:00Z\"\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/messages", "host": ["{{baseUrl}}"], "path": ["api", "messages"] }
              }
            },
            {
              "name": "Bulk send",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"instance_id\": {{instanceId}},\n  \"recipients\": [\"01012345678\", \"01112345679\"],\n  \"message_type\": \"text\",\n  \"payload\": { \"text\": \"Bulk broadcast\" },\n  \"priority\": 7\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/messages/bulk", "host": ["{{baseUrl}}"], "path": ["api", "messages", "bulk"] }
              }
            },
            {
              "name": "Batch send (per-recipient payload)",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"instance_id\": {{instanceId}},\n  \"messages\": [\n    { \"recipient\": \"01012345678\", \"message_type\": \"text\",  \"payload\": { \"text\": \"Hi Ahmed, order #1004 shipped\" } },\n    { \"recipient\": \"01112345679\", \"message_type\": \"media\", \"payload\": { \"mediatype\": \"image\", \"mimetype\": \"image/png\", \"media\": \"https://picsum.photos/600/400.png\", \"fileName\": \"receipt.png\", \"caption\": \"Mona — your receipt\" } },\n    { \"recipient\": \"01212345680\", \"message_type\": \"text\",  \"payload\": { \"text\": \"Hi Karim\" }, \"priority\": 3 }\n  ]\n}"
                },
                "url": { "raw": "{{baseUrl}}/api/messages/batch", "host": ["{{baseUrl}}"], "path": ["api", "messages", "batch"] },
                "description": "Per-recipient personalization — each entry carries its own message_type + payload + priority. All-or-nothing: a single malformed item rejects the whole batch (422 with the failing index). Response: { batch_id, queued_count, estimated_completion_at }."
              }
            },
            {
              "name": "List (filters + paging)",
              "request": {
                "method": "GET",
                "url": {
                  "raw": "{{baseUrl}}/api/messages?instance_id={{instanceId}}&status=pending&page=1&page_size=20",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "messages"],
                  "query": [
                    { "key": "instance_id", "value": "{{instanceId}}" },
                    { "key": "status", "value": "pending" },
                    { "key": "recipient", "value": "", "disabled": true },
                    { "key": "batch_id", "value": "", "disabled": true },
                    { "key": "from_date", "value": "", "disabled": true },
                    { "key": "to_date", "value": "", "disabled": true },
                    { "key": "page", "value": "1" },
                    { "key": "page_size", "value": "20" }
                  ]
                }
              }
            },
            {
              "name": "Stats (counts by status)",
              "request": {
                "method": "GET",
                "url": {
                  "raw": "{{baseUrl}}/api/messages/stats?instance_id={{instanceId}}",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "messages", "stats"],
                  "query": [{ "key": "instance_id", "value": "{{instanceId}}" }]
                }
              }
            },
            {
              "name": "Get message",
              "request": {
                "method": "GET",
                "url": {
                  "raw": "{{baseUrl}}/api/messages/{{messageId}}",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "messages", "{{messageId}}"]
                }
              }
            },
            {
              "name": "Retry message",
              "request": {
                "method": "POST",
                "url": {
                  "raw": "{{baseUrl}}/api/messages/{{messageId}}/retry",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "messages", "{{messageId}}", "retry"]
                }
              }
            },
            {
              "name": "Cancel message",
              "request": {
                "method": "POST",
                "url": {
                  "raw": "{{baseUrl}}/api/messages/{{messageId}}/cancel",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "messages", "{{messageId}}", "cancel"]
                }
              }
            },
            {
              "name": "Retry range",
              "request": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"from_date\": \"2026-04-01T00:00:00Z\",\n  \"to_date\": \"2026-04-30T23:59:59Z\",\n  \"status\": \"failed\",\n  \"instance_id\": {{instanceId}}\n}"
                },
                "url": {
                  "raw": "{{baseUrl}}/api/messages/retry-range",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "messages", "retry-range"]
                }
              }
            }
          ]
        },
        {
          "name": "Webhooks",
          "description": "Mediator: Evolution -> us, we audit + filter + sign + forward to the user. PUT requires PUBLIC_URL set on the gateway. Secret is required on first create, optional on subsequent PUTs.",
          "item": [
            {
              "name": "Get webhook config",
              "request": {
                "method": "GET",
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}/webhook",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}", "webhook"]
                }
              }
            },
            {
              "name": "Set webhook config (PUT)",
              "request": {
                "method": "PUT",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"target_url\": \"https://my-app.example.com/hook\",\n  \"events\": [\"MESSAGES_UPDATE\", \"MESSAGES_UPSERT\", \"CONNECTION_UPDATE\"],\n  \"secret\": \"a-long-random-string-min-8-chars\",\n  \"is_active\": true\n}"
                },
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}/webhook",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}", "webhook"]
                }
              }
            },
            {
              "name": "Delete webhook config",
              "request": {
                "method": "DELETE",
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}/webhook",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}", "webhook"]
                }
              }
            },
            {
              "name": "List webhook events (audit)",
              "request": {
                "method": "GET",
                "url": {
                  "raw": "{{baseUrl}}/api/instances/{{instanceId}}/webhook-events?state=failed&page_size=20",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "instances", "{{instanceId}}", "webhook-events"],
                  "query": [
                    { "key": "state", "value": "failed", "description": "received | forwarded | failed | skipped" },
                    { "key": "event", "value": "messages.update", "disabled": true },
                    { "key": "page_size", "value": "20" }
                  ]
                }
              }
            },
            {
              "name": "Get one webhook event",
              "request": {
                "method": "GET",
                "url": {
                  "raw": "{{baseUrl}}/api/webhook-events/1",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "webhook-events", "1"]
                }
              }
            },
            {
              "name": "Replay webhook event",
              "request": {
                "method": "POST",
                "url": {
                  "raw": "{{baseUrl}}/api/webhook-events/1/replay",
                  "host": ["{{baseUrl}}"],
                  "path": ["api", "webhook-events", "1", "replay"]
                }
              }
            },
            {
              "name": "(Reference) Inbound receiver shape — Evolution -> us",
              "description": "Evolution POSTs here automatically once PUT registers it. Auth: 'apikey' header == the instance's evolution_token. You usually don't invoke this manually — included for visibility.",
              "request": {
                "method": "POST",
                "header": [
                  { "key": "Content-Type", "value": "application/json" },
                  { "key": "apikey", "value": "{{evolutionToken}}", "description": "Evolution's per-instance token (Evolution sends it automatically)" }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"event\": \"messages.update\",\n  \"instance\": \"{{instanceName}}\",\n  \"data\": {\n    \"key\": { \"id\": \"3EB0...\", \"remoteJid\": \"201012345678@s.whatsapp.net\", \"fromMe\": true },\n    \"status\": \"DELIVERY_ACK\"\n  },\n  \"date_time\": \"2026-05-14T12:00:00Z\"\n}"
                },
                "url": {
                  "raw": "{{baseUrl}}/webhook/evolution/{{instanceName}}",
                  "host": ["{{baseUrl}}"],
                  "path": ["webhook", "evolution", "{{instanceName}}"]
                }
              }
            }
          ]
        }
      ]
    },
    {
      "name": "API Keys (named, permission-scoped)",
      "description": "Many named keys per instance, each scoped to a permission subset (`send`/`read`/`chat`/`webhooks`/`manage`). Plaintext is returned exactly once on create; only the SHA-256 is stored.\n\nOwners (a key with `read`) can LIST their instance's keys — minting and revoking stay admin-only so a scoped key can never widen its own reach.",
      "item": [
        {
          "name": "List keys",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/api/instances/{{instanceId}}/api-keys", "host": ["{{baseUrl}}"], "path": ["api", "instances", "{{instanceId}}", "api-keys"] },
            "description": "Fingerprints + permissions only — never plaintext or hashes."
          }
        },
        {
          "name": "Create key (admin)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 201) {",
                  "  pm.collectionVariables.set('instanceApiKey', pm.response.json().api_key);",
                  "  pm.test('api_key saved', () => pm.expect(pm.collectionVariables.get('instanceApiKey')).to.match(/^wq_/));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\n  \"name\": \"crm-backend\",\n  \"permissions\": [\"send\", \"read\"]\n}" },
            "url": { "raw": "{{baseUrl}}/api/instances/{{instanceId}}/api-keys", "host": ["{{baseUrl}}"], "path": ["api", "instances", "{{instanceId}}", "api-keys"] },
            "description": "Admin only. 409 on a duplicate name (cap: 20 keys per instance). The plaintext key is in the response body and is never retrievable again."
          }
        },
        {
          "name": "Revoke key (admin)",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": { "raw": "{{baseUrl}}/api/instances/{{instanceId}}/api-keys/1", "host": ["{{baseUrl}}"], "path": ["api", "instances", "{{instanceId}}", "api-keys", "1"] },
            "description": "Admin only. Anything using the key stops working immediately."
          }
        }
      ]
    },
    {
      "name": "Chats (live support)",
      "description": "Conversation proxy + direct sends. Needs the `chat` permission (or an admin key).\n\nDirect sends bypass the Celery queue but NOT the rate limiter — a slot is consumed atomically, so a capped instance returns `429` with `wait_seconds` + `Retry-After` and writes no row. A paused instance returns `409`. Every send lands a `messages` row either way, so the webhook status-sync advances it to delivered/read exactly like a queued send.\n\n`{{remoteJid}}` accepts `+E164`, bare digits, `<digits>@s.whatsapp.net`, `...@g.us` or `...@lid`.",
      "item": [
        {
          "name": "List chats",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/instances/{{instanceId}}/chats?page=1&page_size=50",
              "host": ["{{baseUrl}}"],
              "path": ["api", "instances", "{{instanceId}}", "chats"],
              "query": [{ "key": "page", "value": "1" }, { "key": "page_size", "value": "50" }]
            },
            "description": "Normalized conversation list, newest activity first. Cached ~15s per instance."
          }
        },
        {
          "name": "Chat messages",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/instances/{{instanceId}}/chats/{{remoteJid}}/messages?page=1&page_size=50",
              "host": ["{{baseUrl}}"],
              "path": ["api", "instances", "{{instanceId}}", "chats", "{{remoteJid}}", "messages"],
              "query": [{ "key": "page", "value": "1" }, { "key": "page_size", "value": "50" }]
            }
          }
        },
        {
          "name": "Send text reply (direct)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\n  \"type\": \"text\",\n  \"text\": \"Thanks for reaching out — how can we help?\"\n}" },
            "url": { "raw": "{{baseUrl}}/api/instances/{{instanceId}}/chats/{{remoteJid}}/messages", "host": ["{{baseUrl}}"], "path": ["api", "instances", "{{instanceId}}", "chats", "{{remoteJid}}", "messages"] },
            "description": "201 on success. Optional: `quoted_message_id` to reply to a specific message, `presence_typing: true` to show 'typing…' first."
          }
        },
        {
          "name": "Send media reply (direct)",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\n  \"type\": \"media\",\n  \"media\": {\n    \"mediatype\": \"image\",\n    \"mimetype\": \"image/png\",\n    \"media\": \"https://example.com/pic.png\",\n    \"caption\": \"Here you go\",\n    \"file_name\": \"pic.png\"\n  }\n}" },
            "url": { "raw": "{{baseUrl}}/api/instances/{{instanceId}}/chats/{{remoteJid}}/messages", "host": ["{{baseUrl}}"], "path": ["api", "instances", "{{instanceId}}", "chats", "{{remoteJid}}", "messages"] },
            "description": "`media` is an http(s) URL or a base64 payload (<= ~12MB decoded). Evolution fetches URLs server-side, so private/link-local hosts are rejected with 422. Base64 is redacted in the stored audit row."
          }
        },
        {
          "name": "Mark read",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\n  \"messages\": [\n    { \"id\": \"3EB0...\", \"from_me\": false }\n  ]\n}" },
            "url": { "raw": "{{baseUrl}}/api/instances/{{instanceId}}/chats/{{remoteJid}}/read", "host": ["{{baseUrl}}"], "path": ["api", "instances", "{{instanceId}}", "chats", "{{remoteJid}}", "read"] }
          }
        },
        {
          "name": "Avatar URL",
          "request": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/api/instances/{{instanceId}}/chats/{{remoteJid}}/avatar", "host": ["{{baseUrl}}"], "path": ["api", "instances", "{{instanceId}}", "chats", "{{remoteJid}}", "avatar"] },
            "description": "{ \"url\": null } when the peer has no picture. Cached ~5min server-side; never 502s."
          }
        }
      ]
    }
  ]
}
