{
  "info": {
    "name": "WA Enhancement API",
    "description": "Channel-scoped API. Every request authenticates with a Laravel Passport token issued for a **channel** (guard `api-channel`, provider `channels`), so the authenticated entity *is* the channel — `channel_id` is never a request parameter and a token cannot reach another channel's data.\n\nSet `base_url` and `channel_token` in the collection variables before sending anything.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{channel_token}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://wa-enhancement.ddev.site",
      "type": "string",
      "description": "No trailing slash. Local ddev default; swap for staging or production."
    },
    {
      "key": "channel_token",
      "value": "",
      "type": "string",
      "description": "Passport access token issued for the channel (api-channel guard)."
    }
  ],
  "item": [
    {
      "name": "Recipients",
      "description": "Inbound recipient push from an external app.",
      "item": [
        {
          "name": "Create recipient",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/recipients",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "api",
                "v1",
                "recipients"
              ]
            },
            "body": {
              "mode": "raw",
              "options": {
                "raw": {
                  "language": "json"
                }
              },
              "raw": "{\n  \"mobile\": \"639171234567\",\n  \"username\": \"anacruz\",\n  \"user_id\": \"bsuid-999\",\n  \"name\": \"Ana Cruz\",\n  \"email\": \"ana@example.com\",\n  \"profile_name\": \"Ana\",\n  \"locale\": \"en\",\n  \"user_guid\": \"abc-123\",\n  \"login_url\": \"https://es.example.com/login/xyz\"\n}"
            },
            "description": "Create a recipient on the authenticated channel, or match and top up an existing one. **Creation only** — no message is sent, no workflow is started, and the outbound `type='user'` integration is never dispatched.\n\n## Identity — at least one of these three is required\n\n| Field | Rules | Stored as |\n|---|---|---|\n| `mobile` | `nullable`, `string`, `max:20` | `channel_recipients.recipient_mobile` (and `whatsapp_id`) |\n| `username` | `nullable`, `string`, `max:100` | `channel_recipients.username` |\n| `user_id` | `nullable`, `string`, `max:100` | `channel_recipients.user_id` — Meta's business-scoped ID |\n\nOmitting all three returns 422 with the error reported on `mobile`.\n\nSend `mobile` as digits with the country code and no `+` — matching is an exact ciphertext comparison, so a differently formatted number creates a duplicate instead of matching.\n\n## Profile — all optional\n\n| Field | Rules |\n|---|---|\n| `name` | `nullable`, `string`, `max:255` |\n| `email` | `nullable`, `email`, `max:255` |\n| `profile_name` | `nullable`, `string`, `max:255` |\n| `locale` | `nullable`, `string`, `max:10` |\n\nThese **fill blanks only**. A value the recipient already has is never overwritten, so this call cannot clobber what they told us themselves through a flow.\n\n## External app linkage — optional\n\n| Field | Rules |\n|---|---|\n| `user_guid` | `nullable`, `string`, `max:255`, `required_with:login_url` |\n| `login_url` | `nullable`, `url`, `max:2048` |\n\nSupplying `user_guid` writes the `es_recipient_identifiers` row, which is what marks the recipient as linked to the external app. `login_url` needs a `user_guid` to hang off. Write-once: an existing link is left alone rather than overwritten, so a corrected guid will **not** propagate.\n\n## Responses\n\n| Status | When |\n|---|---|\n| `201` | A new recipient was created |\n| `200` | An existing recipient matched (by `user_id`, then `mobile`, then `username`) and any missing identifiers were backfilled |\n| `422` | No identifier, `login_url` without `user_guid`, or a malformed `email` / `login_url` |\n| `401` | Missing or invalid channel token |\n\nBody on success:\n\n```json\n{\n  \"success\": true,\n  \"message\": \"Recipient created\",\n  \"data\": { \"uuid\": \"…\", \"created\": true, \"es_linked\": true }\n}\n```\n\n`uuid` is the recipient's UUID, not the numeric key. `created` distinguishes 201 from 200 in the body so a retry is easy to interpret. `es_linked` reports whether the recipient has an external-app identifier row after the call, whether or not this request wrote it.\n\nRetrying the same payload is safe — it matches instead of duplicating."
          },
          "response": []
        }
      ]
    }
  ]
}
