{
  "info": {
    "_postman_id": "8c2bfd5e-ff8d-4158-b971-4e5516796f3d",
    "name": "CloudMe Notify API",
    "description": "Multi-channel notification API - SMS, Telegram, WhatsApp, Voice, Email, and Push through one interface - plus scheduled (one-off and recurring) messages and debt collection (channel-by-channel reminders until paid).\n\nThis collection mirrors `docs/openapi.yaml` exactly (endpoints, request/response fields, error codes). See `docs/postman/README.md` before your first request - authentication requires an RSA signature that must be computed outside Postman (see the Authentication folder below and `docs/postman/sign-token-request.php`).\n\nImport both `Notify-Sandbox.postman_environment.json` and `Notify-Production.postman_environment.json`, fill in your own `client_id` / `api_key` / `private_key` (never commit real values), and start with the Sandbox environment - it never sends real messages or charges your balance.\n\nSpending controls: `INSUFFICIENT_BALANCE`: insufficient wallet funds. `SPENDING_LIMIT_EXCEEDED`: a company daily or monthly message spending cap would be exceeded (HTTP 402). Sandbox is exempt. Gross message debits count toward calendar limits; refunds do not reset the allowance. Change limits in the company balance panel or wait for the next period.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{access_token}}",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Baseline checks applied to every request in this collection.",
          "pm.test(\"Response time is reasonable (< 5000ms)\", function () {",
          "    pm.expect(pm.response.responseTime).to.be.below(5000);",
          "});",
          "",
          "pm.test(\"Response is JSON\", function () {",
          "    pm.response.to.be.json;",
          "});"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "Authentication",
      "description": "Neither endpoint in this folder uses Bearer auth - both are how you obtain the token every other request needs.",
      "item": [
        {
          "name": "Get Access Token",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// timestamp, nonce, and signature must all come from ONE signing operation -",
                  "// generating a fresh timestamp/nonce here would no longer match a signature",
                  "// you computed externally, and the request would fail with SIGNATURE_INVALID.",
                  "//",
                  "// Run docs/postman/sign-token-request.php (see docs/postman/README.md), then",
                  "// either paste its JSON output directly as this request's body, or set",
                  "// timestamp / nonce / signature as environment variables and leave the body",
                  "// as the {{variable}} template below.",
                  "if (!pm.environment.get(\"signature\")) {",
                  "    console.warn(",
                  "        'Notify: `signature` is not set in this environment. Run ' +",
                  "        'docs/postman/sign-token-request.php first and either paste its JSON ' +",
                  "        'output as this request\\'s body, or copy timestamp/nonce/signature into ' +",
                  "        'this environment - see docs/postman/README.md.'",
                  "    );",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "if (pm.response.code === 200) {",
                  "    const json = pm.response.json();",
                  "",
                  "    pm.test(\"Response has the documented token fields\", function () {",
                  "        pm.expect(json).to.have.property(\"token_type\", \"Bearer\");",
                  "        pm.expect(json).to.have.property(\"access_token\").that.is.a(\"string\");",
                  "        pm.expect(json).to.have.property(\"expires_in\").that.is.a(\"number\");",
                  "        pm.expect(json).to.have.property(\"refresh_token\").that.is.a(\"string\");",
                  "    });",
                  "",
                  "    pm.environment.set(\"access_token\", json.access_token);",
                  "    pm.environment.set(\"refresh_token\", json.refresh_token);",
                  "    console.log(\"Notify: access_token and refresh_token saved to the environment.\");",
                  "} else {",
                  "    const json = pm.response.json();",
                  "",
                  "    pm.test(\"Error response has the documented shape\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", false);",
                  "        pm.expect(json.error).to.have.property(\"code\");",
                  "    });",
                  "",
                  "    console.warn(\"Notify: token request failed with \" + (json.error && json.error.code) + \" - \" + (json.error && json.error.message));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"client_id\": \"{{client_id}}\",\n    \"api_key\": \"{{api_key}}\",\n    \"timestamp\": {{timestamp}},\n    \"nonce\": \"{{nonce}}\",\n    \"signature\": \"{{signature}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/oauth/token",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "oauth",
                "token"
              ]
            },
            "description": "Exchanges a signed client_id/api_key for an access_token + refresh_token.\n\n`signature` is a base64-encoded RSA-SHA256 (RSASSA-PKCS1-v1_5) signature over `\"{client_id}\\n{timestamp}\\n{nonce}\\n{sha256(api_key)}\"`, produced with your API client's RSA private key. Postman's scripting sandbox cannot generate this (no RSA primitive is available) - run `docs/postman/sign-token-request.php --client-id=... --api-key=... --private-key=...` locally and paste its output here. See docs/postman/README.md.\n\nRate limited to 10 requests/minute per client_id. `nonce` (8-64 chars) can only be used once - a replayed nonce is rejected with `NONCE_REPLAYED`.\n\nErrors: `INVALID_CLIENT`, `INVALID_CREDENTIALS`, `TIMESTAMP_INVALID` (outside ±60s), `NONCE_REPLAYED`, `SIGNATURE_INVALID` (401) - `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403)."
          },
          "response": []
        },
        {
          "name": "Refresh Access Token",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "if (pm.response.code === 200) {",
                  "    const json = pm.response.json();",
                  "",
                  "    pm.test(\"Response has the documented token fields\", function () {",
                  "        pm.expect(json).to.have.property(\"token_type\", \"Bearer\");",
                  "        pm.expect(json).to.have.property(\"access_token\").that.is.a(\"string\");",
                  "        pm.expect(json).to.have.property(\"expires_in\").that.is.a(\"number\");",
                  "        pm.expect(json).to.have.property(\"refresh_token\").that.is.a(\"string\");",
                  "    });",
                  "",
                  "    // Refresh tokens rotate on every use - the old one is now revoked, so",
                  "    // the environment must always keep the newest one.",
                  "    pm.environment.set(\"access_token\", json.access_token);",
                  "    pm.environment.set(\"refresh_token\", json.refresh_token);",
                  "    console.log(\"Notify: access_token and refresh_token rotated and saved.\");",
                  "} else {",
                  "    const json = pm.response.json();",
                  "",
                  "    pm.test(\"Error response has the documented shape\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", false);",
                  "        pm.expect(json.error).to.have.property(\"code\");",
                  "    });",
                  "",
                  "    console.warn(\"Notify: refresh failed with \" + (json.error && json.error.code) + \" - if this is REFRESH_TOKEN_REUSED, the whole token family was revoked; call Get Access Token again.\");",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"refresh_token\": \"{{refresh_token}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/oauth/refresh",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "oauth",
                "refresh"
              ]
            },
            "description": "Exchanges a refresh_token for a new access_token/refresh_token pair. Refresh tokens rotate on every use (the one you send is invalidated) and are valid for 14 days.\n\nErrors: `INVALID_REFRESH_TOKEN`, `REFRESH_TOKEN_EXPIRED`, `REFRESH_TOKEN_REUSED` (401 - the entire token family was just revoked, call Get Access Token again) - `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH` (403)."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Messages",
      "description": "All requests here require Bearer auth (collection-level - {{access_token}}) and the matching `{channel}:send` scope on your API client.\n\nFree text (`message`) that matches none of the system/company templates is held for moderation: the response `status` is `moderation` (already charged). It later becomes `queued` (approved) or `rejected` (refunded, `message.rejected` webhook). Send with `template_id` to skip moderation.",
      "item": [
        {
          "name": "Send SMS",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"Message accepted (send-message response shape)\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json).to.have.property(\"message_id\").that.is.a(\"string\");",
                  "        pm.expect(json).to.have.property(\"environment\");",
                  "        pm.expect(json).to.have.property(\"status\");",
                  "        pm.expect(json).to.have.property(\"price\");",
                  "        pm.expect(json).to.have.property(\"currency\");",
                  "        pm.expect(json).to.have.property(\"balance\");",
                  "    });",
                  "    pm.environment.set(\"message_id\", json.message_id);",
                  "} else if (pm.response.code === 402) {",
                  "    pm.test(\"Insufficient balance error shape is correct\", function () {",
                  "        pm.expect(json.success).to.eql(false);",
                  "        pm.expect(json.error.code).to.eql(\"INSUFFICIENT_BALANCE\");",
                  "    });",
                  "} else if (pm.response.code === 422) {",
                  "    pm.test(\"Validation/price error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "} else if (pm.response.code === 403) {",
                  "    pm.test(\"Forbidden error shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", false);",
                  "        pm.expect(json.error).to.have.property(\"code\");",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Optional but recommended - retrying with the same value returns the original result instead of sending twice."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"to\": \"+998901234567\",\n    \"message\": \"Test message\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/messages/sms",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "messages",
                "sms"
              ]
            },
            "description": "Requires scope `sms:send`.\n\nTo send from a template instead of a literal `message`:\n```json\n{\n    \"to\": \"+998901234567\",\n    \"template_id\": 42,\n    \"variables\": { \"order_id\": \"A-12891\" }\n}\n```\n`sms_type` (`service` or `advertisement`) may also be set - it affects pricing.\n\nErrors: `INSUFFICIENT_BALANCE` (402) - `SCOPE_FORBIDDEN`, `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403) - `PRICE_NOT_CONFIGURED` or validation failure (422).\n\n`INSUFFICIENT_BALANCE`: insufficient wallet funds. `SPENDING_LIMIT_EXCEEDED`: a company daily or monthly message spending cap would be exceeded (HTTP 402). Sandbox is exempt. Gross message debits count toward calendar limits; refunds do not reset the allowance. Change limits in the company balance panel or wait for the next period."
          },
          "response": []
        },
        {
          "name": "Send Telegram",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"Message accepted (send-message response shape)\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json).to.have.property(\"message_id\").that.is.a(\"string\");",
                  "        pm.expect(json).to.have.property(\"status\");",
                  "    });",
                  "    pm.environment.set(\"message_id\", json.message_id);",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"to\": \"+998901234567\",\n  \"message\": \"<b>Hurmatli Aziz</b>, buyurtmangiz tayyor.\",\n  \"photo_url\": \"https://example.com/images/order.jpg\",\n  \"channel_account_id\": {{channel_account_id}}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/messages/telegram",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "messages",
                "telegram"
              ]
            },
            "description": "Requires scope `telegram:send`.\n\n`to` must be a phone number that has already started your Telegram bot (bound via the deep-link flow in the company dashboard) - Telegram has no concept of messaging a phone number directly. An unbound phone fails with provider error `CHAT_NOT_BOUND` (the send itself still returns 200/queued - delivery fails asynchronously).\n\nThe text supports HTML: `<b>`, `<i>`, `<u>`, `<a href=\"...\">` (invalid HTML falls back to plain text).\n\n`photo_url` (optional) - public JPEG/PNG URL; the message is sent as the photo's caption (text over 1024 characters follows as a separate message).\n\nErrors: same as Send SMS.\n\n`INSUFFICIENT_BALANCE`: insufficient wallet funds. `SPENDING_LIMIT_EXCEEDED`: a company daily or monthly message spending cap would be exceeded (HTTP 402). Sandbox is exempt. Gross message debits count toward calendar limits; refunds do not reset the allowance. Change limits in the company balance panel or wait for the next period.\nOptional channel_account_id chooses a company Telegram bot or Firebase project. Leave null for the current default. Register each FCM token with the same project ID used when sending; one token is stored per company/project/phone."
          },
          "response": []
        },
        {
          "name": "Send WhatsApp",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"Message accepted (send-message response shape)\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json).to.have.property(\"message_id\").that.is.a(\"string\");",
                  "        pm.expect(json).to.have.property(\"status\");",
                  "    });",
                  "    pm.environment.set(\"message_id\", json.message_id);",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"to\": \"+998901234567\",\n    \"message\": \"*Hurmatli Aziz*, buyurtmangiz tayyor.\",\n    \"photo_url\": \"https://example.com/images/order.jpg\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/messages/whatsapp",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "messages",
                "whatsapp"
              ]
            },
            "description": "Requires scope `whatsapp:send`.\n\nFree text (and `photo_url`) is only delivered inside the 24-hour window after the customer last wrote to you; outside it use a Meta-approved template (`template_id` + `variables`). With a template, `photo_url` fills the template's IMAGE header.\n\nFormatting: `*bold*`, `_italic_`, `~strike~` - HTML is not rendered.\n\nErrors: same as Send SMS.\n\n`INSUFFICIENT_BALANCE`: insufficient wallet funds. `SPENDING_LIMIT_EXCEEDED`: a company daily or monthly message spending cap would be exceeded (HTTP 402). Sandbox is exempt. Gross message debits count toward calendar limits; refunds do not reset the allowance. Change limits in the company balance panel or wait for the next period."
          },
          "response": []
        },
        {
          "name": "Send Voice",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"Message accepted (send-message response shape)\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json).to.have.property(\"message_id\").that.is.a(\"string\");",
                  "        pm.expect(json).to.have.property(\"status\");",
                  "    });",
                  "    pm.environment.set(\"message_id\", json.message_id);",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"to\": \"+998901234567\",\n    \"message\": \"This is a test voice call from Notify.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/messages/voice",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "messages",
                "voice"
              ]
            },
            "description": "Requires scope `voice:send`. `message` is read aloud via text-to-speech.\n\nErrors: same as Send SMS.\n\n`INSUFFICIENT_BALANCE`: insufficient wallet funds. `SPENDING_LIMIT_EXCEEDED`: a company daily or monthly message spending cap would be exceeded (HTTP 402). Sandbox is exempt. Gross message debits count toward calendar limits; refunds do not reset the allowance. Change limits in the company balance panel or wait for the next period."
          },
          "response": []
        },
        {
          "name": "Send Email",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"Message accepted (send-message response shape)\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json).to.have.property(\"message_id\").that.is.a(\"string\");",
                  "        pm.expect(json).to.have.property(\"status\");",
                  "    });",
                  "    pm.environment.set(\"message_id\", json.message_id);",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"to\": \"customer@example.com\",\n    \"message\": \"Your invoice is attached.\",\n    \"subject\": \"Invoice #A-12891\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/messages/email",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "messages",
                "email"
              ]
            },
            "description": "Requires scope `email:send`. `to` is an email address; `subject` is commonly used here.\n\nErrors: same as Send SMS.\n\n`INSUFFICIENT_BALANCE`: insufficient wallet funds. `SPENDING_LIMIT_EXCEEDED`: a company daily or monthly message spending cap would be exceeded (HTTP 402). Sandbox is exempt. Gross message debits count toward calendar limits; refunds do not reset the allowance. Change limits in the company balance panel or wait for the next period."
          },
          "response": []
        },
        {
          "name": "Send Push",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"Message accepted (send-message response shape)\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json).to.have.property(\"message_id\").that.is.a(\"string\");",
                  "        pm.expect(json).to.have.property(\"status\");",
                  "    });",
                  "    pm.environment.set(\"message_id\", json.message_id);",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"to\": \"+998901234567\",\n  \"message\": \"Your order shipped.\",\n  \"subject\": \"Order update\",\n  \"channel_account_id\": {{channel_account_id}}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/messages/push",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "messages",
                "push"
              ]
            },
            "description": "Requires scope `push:send`. `to` is the customer's phone number - register their device first with Push > Register Device. `subject` is used as the notification title.\n\nErrors: same as Send SMS.\n\n`INSUFFICIENT_BALANCE`: insufficient wallet funds. `SPENDING_LIMIT_EXCEEDED`: a company daily or monthly message spending cap would be exceeded (HTTP 402). Sandbox is exempt. Gross message debits count toward calendar limits; refunds do not reset the allowance. Change limits in the company balance panel or wait for the next period.\nOptional channel_account_id chooses a company Telegram bot or Firebase project. Leave null for the current default. Register each FCM token with the same project ID used when sending; one token is stored per company/project/phone."
          },
          "response": []
        },
        {
          "name": "Universal Send",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"Message accepted (send-message response shape)\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json).to.have.property(\"message_id\").that.is.a(\"string\");",
                  "        pm.expect(json).to.have.property(\"status\");",
                  "    });",
                  "    pm.environment.set(\"message_id\", json.message_id);",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"channel\": \"sms\",\n  \"recipient\": \"+998901234567\",\n  \"message\": \"Test message via the universal endpoint\",\n  \"channel_account_id\": {{channel_account_id}}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/messages",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "messages"
              ]
            },
            "description": "Same behavior as the per-channel endpoints above, but the channel is given in the body (`channel`) instead of the URL, and the recipient field is named `recipient` (not `to`) - useful when your own code picks the channel at runtime. Still requires the matching `{channel}:send` scope.\n\n`channel` must be one of: `sms`, `telegram`, `whatsapp`, `voice`, `email`, `push`.\n\nErrors: `SCOPE_FORBIDDEN` (403, missing the scope for the chosen channel) plus the same set as Send SMS.\n\n`INSUFFICIENT_BALANCE`: insufficient wallet funds. `SPENDING_LIMIT_EXCEEDED`: a company daily or monthly message spending cap would be exceeded (HTTP 402). Sandbox is exempt. Gross message debits count toward calendar limits; refunds do not reset the allowance. Change limits in the company balance panel or wait for the next period.\nOptional channel_account_id chooses a company Telegram bot or Firebase project. Leave null for the current default. Register each FCM token with the same project ID used when sending; one token is stored per company/project/phone."
          },
          "response": []
        },
        {
          "name": "Get Message Status",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is 200 or 404\", function () {",
                  "    pm.expect([200, 404]).to.include(pm.response.code);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"Message status response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json).to.have.property(\"message_id\");",
                  "        pm.expect(json).to.have.property(\"channel\");",
                  "        pm.expect(json).to.have.property(\"recipient\");",
                  "        pm.expect(json).to.have.property(\"status\");",
                  "        pm.expect(json).to.have.property(\"price\");",
                  "        pm.expect(json).to.have.property(\"currency\");",
                  "    });",
                  "} else {",
                  "    pm.test(\"NOT_FOUND error shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", false);",
                  "        pm.expect(json.error.code).to.eql(\"NOT_FOUND\");",
                  "    });",
                  "    console.warn(\"Notify: no {{message_id}} set yet, or it belongs to another company - run one of the Send * requests first.\");",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/messages/{{message_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "messages",
                "{{message_id}}"
              ]
            },
            "description": "Requires scope `messages:read`. `{{message_id}}` is set automatically by the Send * requests' test scripts - run one of those first, or set it manually to a `message_id` you already have.\n\nErrors: `SCOPE_FORBIDDEN`, `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403) - `NOT_FOUND` (404, unknown uuid or it belongs to another company)."
          },
          "response": []
        }
      ]
    },
    {
      "name": "OTP",
      "description": "One-time codes generated, delivered and verified by the platform. Code length, validity and attempts come from your company OTP settings (Notify cabinet), or the platform defaults. Sending needs the `{channel}:send` scope of the chosen channel. OTP messages are never held for moderation.",
      "item": [
        {
          "name": "Send OTP",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"OTP generated (otp-send response shape)\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json).to.have.property(\"otp_id\").that.is.a(\"string\");",
                  "        pm.expect(json).to.have.property(\"status\", \"pending\");",
                  "        pm.expect(json).to.have.property(\"code_length\");",
                  "        pm.expect(json).to.have.property(\"max_attempts\");",
                  "        pm.expect(json).to.have.property(\"expires_in\");",
                  "    });",
                  "    pm.environment.set(\"otp_id\", json.otp_id);",
                  "    // Only the sandbox returns the code - lets \"Verify OTP\" run as-is.",
                  "    if (json.code) {",
                  "        pm.environment.set(\"otp_code\", json.code);",
                  "    }",
                  "} else if (pm.response.code === 429) {",
                  "    pm.test(\"Resend cooldown error shape is correct\", function () {",
                  "        pm.expect(json.error.code).to.eql(\"OTP_RESEND_TOO_SOON\");",
                  "        pm.expect(json).to.have.property(\"retry_after\");",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"to\": \"+998901234567\",\n    \"channel\": \"sms\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/otp/send",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "otp",
                "send"
              ]
            },
            "description": "`channel`: `sms` (default), `telegram`, `whatsapp`, `voice` or `email`. On `voice` the code is read digit by digit (6-5-4-9-8-7) in a call placed 09:00-21:00 Tashkent time.\n\nSaves `otp_id` to the environment; in the sandbox also `otp_code` (the sandbox returns the code, production never does).\n\nA new code for the same recipient cancels the previous one. Requesting again too soon: `OTP_RESEND_TOO_SOON` (429, `retry_after` + `Retry-After` header).\n\nNot idempotent - do not blindly retry: a retry delivers a second code.\n\nOther errors: `INSUFFICIENT_BALANCE` (402) - `SCOPE_FORBIDDEN`, `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE` (403) - `PRICE_NOT_CONFIGURED` or validation failure (422).\n\n`INSUFFICIENT_BALANCE`: insufficient wallet funds. `SPENDING_LIMIT_EXCEEDED`: a company daily or monthly message spending cap would be exceeded (HTTP 402). Sandbox is exempt. Gross message debits count toward calendar limits; refunds do not reset the allowance. Change limits in the company balance panel or wait for the next period."
          }
        },
        {
          "name": "Verify OTP",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"Code verified\", function () {",
                  "        pm.expect(json).to.have.property(\"verified\", true);",
                  "        pm.expect(json).to.have.property(\"status\", \"verified\");",
                  "    });",
                  "} else if (pm.response.code === 422 && json.error) {",
                  "    pm.test(\"Failed verification has a recognized OTP error code\", function () {",
                  "        pm.expect([\"OTP_INVALID\", \"OTP_EXPIRED\", \"OTP_ATTEMPTS_EXCEEDED\", \"OTP_ALREADY_VERIFIED\"]).to.include(json.error.code);",
                  "        pm.expect(json).to.have.property(\"attempts_left\");",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"otp_id\": \"{{otp_id}}\",\n    \"code\": \"{{otp_code}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/otp/verify",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "otp",
                "verify"
              ]
            },
            "description": "Every wrong code counts as an attempt; when the attempts run out the code is burnt. A verified code cannot be reused.\n\n422 error codes: `OTP_INVALID` (see `attempts_left`) - `OTP_EXPIRED` - `OTP_ATTEMPTS_EXCEEDED` - `OTP_ALREADY_VERIFIED`. Unknown `otp_id` or another company's: `OTP_NOT_FOUND` (404)."
          }
        }
      ]
    },
    {
      "name": "Push",
      "item": [
        {
          "name": "Register Device",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is 200 or 422\", function () {",
                  "    pm.expect([200, 422]).to.include(pm.response.code);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if (pm.response.code === 200) {",
                  "    pm.test(\"Registration succeeded\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "    });",
                  "} else {",
                  "    pm.test(\"INVALID_PHONE error shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", false);",
                  "        pm.expect(json.error.code).to.eql(\"INVALID_PHONE\");",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"phone\": \"+998901234567\",\n  \"fcm_token\": \"sample-fcm-device-token\",\n  \"channel_account_id\": {{channel_account_id}}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/push/devices",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "push",
                "devices"
              ]
            },
            "description": "Requires scope `push:register`. Call this whenever your own mobile app obtains or rotates an FCM token for a logged-in customer, before sending them a Push notification via Messages > Send Push.\n\nErrors: `SCOPE_FORBIDDEN`, `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403) - `INVALID_PHONE` (422).\nOptional channel_account_id chooses a company Telegram bot or Firebase project. Leave null for the current default. Register each FCM token with the same project ID used when sending; one token is stored per company/project/phone."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Templates",
      "item": [
        {
          "name": "List Templates",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test(\"Response lists templates\", function () {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json).to.have.property(\"success\", true);",
                  "    pm.expect(json.templates).to.be.an(\"array\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/templates?channel=sms",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "templates"
              ],
              "query": [
                {
                  "key": "channel",
                  "value": "sms",
                  "description": "Optional - only this channel's templates."
                }
              ]
            },
            "description": "System templates plus your company's own approved templates, only for channels this token has a `{channel}:send` scope for (no separate scope is needed). Send with `template_id` = a template's `id` and a value in `variables` for every name the template lists.\n\nErrors: `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403) - unknown `channel` (422)."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Telegram",
      "item": [
        {
          "name": "Create Binding Link",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "pm.test(\"Either a link or bound: true\", function () {",
                  "    pm.expect(json).to.have.property(\"success\", true);",
                  "    pm.expect(json.bound ? json.link === null : typeof json.link === \"string\").to.be.true;",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"phone\": \"+998901234567\",\n  \"channel_account_id\": {{channel_account_id}}\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/telegram/bindings",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "telegram",
                "bindings"
              ]
            },
            "description": "Requires scope `telegram:send`. Telegram cannot message a phone number directly - send the returned `link` to the customer (SMS, email, your app); once they open it and press Start, Telegram messages to that phone are delivered. The same phone gets the same link again (valid 7 days). `bound: true` with `link: null` means the customer already opened your link for this bot.\n\nOptional channel_account_id picks your own Telegram bot profile; leave null for your default bot or the platform bot.\n\nErrors: `SCOPE_FORBIDDEN`, `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403) - `INVALID_PHONE`, `TELEGRAM_BOT_NOT_CONFIGURED` or validation failure (422)."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Account",
      "item": [
        {
          "name": "Get Balance",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test(\"Balance response shape is correct\", function () {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json).to.have.property(\"success\", true);",
                  "    pm.expect(json).to.have.property(\"balance\");",
                  "    pm.expect(json).to.have.property(\"reserved_balance\");",
                  "    pm.expect(json).to.have.property(\"currency\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/balance",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "balance"
              ]
            },
            "description": "Requires scope `balance:read`.\n\nErrors: `SCOPE_FORBIDDEN`, `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403)."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Reports",
      "item": [
        {
          "name": "Daily Report",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test(\"Report response shape is correct\", function () {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json).to.have.property(\"success\", true);",
                  "    pm.expect(json.data).to.be.an(\"array\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/reports/daily?from={{report_from}}&to={{report_to}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "reports",
                "daily"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{report_from}}",
                  "description": "YYYY-MM-DD, optional - defaults to the last 7 days",
                  "disabled": true
                },
                {
                  "key": "to",
                  "value": "{{report_to}}",
                  "description": "YYYY-MM-DD, optional",
                  "disabled": true
                }
              ]
            },
            "description": "Requires scope `reports:read`. Defaults to the last 7 days when `from`/`to` are omitted (both query params are disabled by default in this request - enable them to set a custom range).\n\nErrors: `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403)."
          },
          "response": []
        },
        {
          "name": "Monthly Report",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is 200\", function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test(\"Report response shape is correct\", function () {",
                  "    const json = pm.response.json();",
                  "    pm.expect(json).to.have.property(\"success\", true);",
                  "    pm.expect(json.data).to.be.an(\"array\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/reports/monthly?from={{report_from}}&to={{report_to}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "reports",
                "monthly"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "{{report_from}}",
                  "description": "YYYY-MM-DD, optional - defaults to the current calendar year",
                  "disabled": true
                },
                {
                  "key": "to",
                  "value": "{{report_to}}",
                  "description": "YYYY-MM-DD, optional",
                  "disabled": true
                }
              ]
            },
            "description": "Requires scope `reports:read`. Defaults to the current calendar year when `from`/`to` are omitted.\n\nErrors: `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403)."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Debts",
      "description": "Debt collection. Requires scope `debts:manage`. Create a debt, then report payments - reminders go out channel by channel following the collection strategy until the debt is fully paid.",
      "item": [
        {
          "name": "Create Debt",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([200, 201].includes(pm.response.code)) {",
                  "    pm.test(\"Debt response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.debt).to.have.property(\"id\").that.is.a(\"string\");",
                  "        pm.expect(json.debt).to.have.property(\"status\");",
                  "        pm.expect(json.debt).to.have.property(\"remaining_amount\");",
                  "    });",
                  "    pm.environment.set(\"debt_id\", json.debt.id);",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/debts",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "debts"
              ]
            },
            "description": "Requires scope `debts:manage`.\n\nHands a debt over for collection. Reminders start automatically following `strategy_id` (Company panel -> Collection -> Strategies -> API ID) or your company's default strategy; step days count from `due_date`.\n\n`external_id` makes the call idempotent: sending it again returns the existing debt with `200` instead of `201`.\n\nErrors: `STRATEGY_NOT_FOUND` (422), `SCOPE_FORBIDDEN`, `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403).",
            "body": {
              "mode": "raw",
              "raw": "{\n    \"name\": \"Aziz Karimov\",\n    \"phone\": \"998901234567\",\n    \"amount\": 1250000,\n    \"due_date\": \"2026-10-15\",\n    \"external_id\": \"INV-{{$timestamp}}\",\n    \"description\": \"Mart oyi uchun\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Get Debt",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([200].includes(pm.response.code)) {",
                  "    pm.test(\"Debt response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.debt).to.have.property(\"id\").that.is.a(\"string\");",
                  "        pm.expect(json.debt).to.have.property(\"status\");",
                  "        pm.expect(json.debt).to.have.property(\"remaining_amount\");",
                  "    });",
                  "    pm.test(\"Payments list is present\", function () {",
                  "        pm.expect(json.payments).to.be.an(\"array\");",
                  "    });",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/debts/{{debt_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "debts",
                "{{debt_id}}"
              ]
            },
            "description": "Requires scope `debts:manage`. `{{debt_id}}` is set automatically by Create Debt.\n\nReturns the debt with all its payments. Errors: `NOT_FOUND` (404)."
          },
          "response": []
        },
        {
          "name": "Record Payment",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([200, 201].includes(pm.response.code)) {",
                  "    pm.test(\"Payment response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.payment).to.have.property(\"attributed_channel\");",
                  "        pm.expect(json.payment).to.have.property(\"days_late\");",
                  "        pm.expect(json.debt).to.have.property(\"remaining_amount\");",
                  "    });",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/debts/{{debt_id}}/payments",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "debts",
                "{{debt_id}}",
                "payments"
              ]
            },
            "description": "Requires scope `debts:manage`.\n\nReports a (partial) payment. When payments add up to the full amount the debt becomes `paid` and reminders stop. `external_id` makes it idempotent (a repeat returns the original payment with `200`).\n\nErrors: `EXCEEDS_REMAINING`, `DEBT_CLOSED`, `INVALID_AMOUNT` (422), `NOT_FOUND` (404).",
            "body": {
              "mode": "raw",
              "raw": "{\n    \"amount\": 250000,\n    \"external_id\": \"PAY-{{$timestamp}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Cancel Debt",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([200].includes(pm.response.code)) {",
                  "    pm.test(\"Debt response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.debt).to.have.property(\"id\").that.is.a(\"string\");",
                  "        pm.expect(json.debt).to.have.property(\"status\");",
                  "        pm.expect(json.debt).to.have.property(\"remaining_amount\");",
                  "    });",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{base_url}}/debts/{{debt_id}}/cancel",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "debts",
                "{{debt_id}}",
                "cancel"
              ]
            },
            "description": "Requires scope `debts:manage`.\n\nStops collection - no further reminders, no further payments. Errors: `DEBT_CLOSED` (422), `NOT_FOUND` (404)."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Scheduled Messages",
      "description": "Scheduled messages - send later, once or on a recurring timetable (every N hours/days, weekly on chosen weekdays, monthly on chosen days). Requires scope `schedules:manage` plus the channel's send scope. The same schedules as the dashboard's Scheduled messages page.",
      "item": [
        {
          "name": "Create Scheduled Message",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// starts_at must not be in the past - schedule one hour from now.",
                  "const startsAt = new Date(Date.now() + 60 * 60 * 1000);",
                  "startsAt.setSeconds(0, 0);",
                  "pm.environment.set(\"schedule_starts_at\", startsAt.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([201].includes(pm.response.code)) {",
                  "    pm.test(\"Scheduled message response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.scheduled_message).to.have.property(\"id\").that.is.a(\"string\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"status\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"next_run_at\");",
                  "    });",
                  "    pm.environment.set(\"scheduled_message_id\", json.scheduled_message.id);",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/scheduled-messages",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "scheduled-messages"
              ]
            },
            "description": "Requires scope `schedules:manage` and the channel's send scope (e.g. `sms:send`).\n\nSchedules a message once (`schedule_type: once`, sent at `starts_at`) or recurring (`recurrence_type`: `interval` + `interval_value`/`interval_unit`, `weekly` + `weekdays` 1-7, `monthly` + `month_days` 1-31; weekly/monthly sends use the time of day of `starts_at`). Send `recipient` for one person or `contact_group_id` for a contact group (production only; `{{name}}` is replaced with each contact's name). The pre-request script sets `starts_at` one hour ahead.\n\nErrors: `TEMPLATE_NOT_FOUND`, `CONTACT_GROUP_NOT_FOUND`, `SANDBOX_GROUP_UNSUPPORTED` (422), Errors: `SCOPE_FORBIDDEN`, `CLIENT_REVOKED`, `COMPANY_NOT_ACTIVE`, `ENVIRONMENT_MISMATCH`, `IP_NOT_ALLOWED` (403).",
            "body": {
              "mode": "raw",
              "raw": "{\n    \"channel\": \"sms\",\n    \"recipient\": \"998901234567\",\n    \"message\": \"Eslatma: ertaga soat 10:00 da uchrashuv\",\n    \"schedule_type\": \"recurring\",\n    \"starts_at\": \"{{schedule_starts_at}}\",\n    \"recurrence_type\": \"weekly\",\n    \"weekdays\": [1, 3, 5]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "List Scheduled Messages",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([200].includes(pm.response.code)) {",
                  "    pm.test(\"Scheduled message list shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.scheduled_messages).to.be.an(\"array\");",
                  "        pm.expect(json.meta).to.have.property(\"total\");",
                  "    });",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/scheduled-messages?status=active&per_page=20",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "scheduled-messages"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "active",
                  "description": "active, paused, completed or cancelled (optional)"
                },
                {
                  "key": "per_page",
                  "value": "20",
                  "description": "1-100 (optional)"
                }
              ]
            },
            "description": "Requires scope `schedules:manage`.\n\nNewest first, paginated (`page`, `per_page`), optionally filtered by `status` and `channel`. Only schedules of this API client's environment."
          },
          "response": []
        },
        {
          "name": "Get Scheduled Message",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([200].includes(pm.response.code)) {",
                  "    pm.test(\"Scheduled message response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.scheduled_message).to.have.property(\"id\").that.is.a(\"string\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"status\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"next_run_at\");",
                  "    });",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/scheduled-messages/{{scheduled_message_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "scheduled-messages",
                "{{scheduled_message_id}}"
              ]
            },
            "description": "Requires scope `schedules:manage`. `{{scheduled_message_id}}` is set automatically by Create Scheduled Message.\n\nErrors: `NOT_FOUND` (404)."
          },
          "response": []
        },
        {
          "name": "Update Scheduled Message",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// starts_at must not be in the past - schedule one hour from now.",
                  "const startsAt = new Date(Date.now() + 60 * 60 * 1000);",
                  "startsAt.setSeconds(0, 0);",
                  "pm.environment.set(\"schedule_starts_at\", startsAt.toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([200].includes(pm.response.code)) {",
                  "    pm.test(\"Scheduled message response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.scheduled_message).to.have.property(\"id\").that.is.a(\"string\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"status\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"next_run_at\");",
                  "    });",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/scheduled-messages/{{scheduled_message_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "scheduled-messages",
                "{{scheduled_message_id}}"
              ]
            },
            "description": "Requires scope `schedules:manage` and the channel's send scope.\n\nReplaces the whole schedule - send every field, as on create. Recomputes `next_run_at` and clears `last_error`.\n\nErrors: `SCHEDULE_STATE_INVALID` (completed or cancelled), `TEMPLATE_NOT_FOUND`, `CONTACT_GROUP_NOT_FOUND`, `SANDBOX_GROUP_UNSUPPORTED` (422), `NOT_FOUND` (404).",
            "body": {
              "mode": "raw",
              "raw": "{\n    \"channel\": \"sms\",\n    \"recipient\": \"998901234567\",\n    \"message\": \"Eslatma: uchrashuv vaqti o'zgardi\",\n    \"schedule_type\": \"once\",\n    \"starts_at\": \"{{schedule_starts_at}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "Pause Scheduled Message",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([200].includes(pm.response.code)) {",
                  "    pm.test(\"Scheduled message response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.scheduled_message).to.have.property(\"id\").that.is.a(\"string\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"status\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"next_run_at\");",
                  "    });",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{base_url}}/scheduled-messages/{{scheduled_message_id}}/pause",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "scheduled-messages",
                "{{scheduled_message_id}}",
                "pause"
              ]
            },
            "description": "Requires scope `schedules:manage`. Only an `active` schedule can be paused.\n\nErrors: `SCHEDULE_STATE_INVALID` (422), `NOT_FOUND` (404)."
          },
          "response": []
        },
        {
          "name": "Resume Scheduled Message",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([200].includes(pm.response.code)) {",
                  "    pm.test(\"Scheduled message response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.scheduled_message).to.have.property(\"id\").that.is.a(\"string\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"status\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"next_run_at\");",
                  "    });",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{base_url}}/scheduled-messages/{{scheduled_message_id}}/resume",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "scheduled-messages",
                "{{scheduled_message_id}}",
                "resume"
              ]
            },
            "description": "Requires scope `schedules:manage`. Only a `paused` schedule can be resumed; it continues at its next occurrence from now (missed ones are skipped).\n\nErrors: `SCHEDULE_STATE_INVALID`, `SCHEDULE_START_PASSED` (422), `NOT_FOUND` (404)."
          },
          "response": []
        },
        {
          "name": "Cancel Scheduled Message",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status code is not 5xx\", function () {",
                  "    pm.expect(pm.response.code).to.be.below(500);",
                  "});",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "if ([200].includes(pm.response.code)) {",
                  "    pm.test(\"Scheduled message response shape is correct\", function () {",
                  "        pm.expect(json).to.have.property(\"success\", true);",
                  "        pm.expect(json.scheduled_message).to.have.property(\"id\").that.is.a(\"string\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"status\");",
                  "        pm.expect(json.scheduled_message).to.have.property(\"next_run_at\");",
                  "    });",
                  "} else {",
                  "    pm.test(\"Error response has a recognized shape\", function () {",
                  "        const isErrorResponse = json.success === false && json.error && json.error.code;",
                  "        const isValidationErrorResponse = typeof json.message === \"string\" && typeof json.errors === \"object\";",
                  "        pm.expect(isErrorResponse || isValidationErrorResponse).to.be.true;",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{base_url}}/scheduled-messages/{{scheduled_message_id}}/cancel",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "scheduled-messages",
                "{{scheduled_message_id}}",
                "cancel"
              ]
            },
            "description": "Requires scope `schedules:manage`. Nothing more is sent; a cancelled schedule cannot be changed or resumed.\n\nErrors: `SCHEDULE_STATE_INVALID` (422), `NOT_FOUND` (404)."
          },
          "response": []
        }
      ]
    }
  ]
}