{
  "info": {
    "name": "StaqOn External API",
    "_postman_id": "staqon-external-api-v1",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "description": "# StaqOn External API\n\nIntegrate an external system (e.g. a CRM) with a StaqOn **external team**. Authenticate every request with the API key issued for that team (StaqOn → Workspace Settings → API).\n\n## Two-gate model\n\n- **Key-gated (writes + directory):** `add-member`, `remove-member`, `message`, `comment`, `escalation`, `members`, `member`, `status` need only a valid **X-API-Key** whose team the member belongs to. Writing *into* a member's thread is the workspace's own authority.\n- **Consent-gated (chat read):** `messages` (reading a member's chat history *out*) additionally requires an **APPROVED account link** — the member must consent. Request it with `link-request`, the member approves in their StaqOn account, and can **revoke** anytime (read access cuts off immediately → 403).\n\n## Typical flow\n\n1. `add-member` — onboard the member into your external team.\n2. `message` / `comment` — push updates into their StaqOn thread (Direction In, key-only).\n3. `link-request` — ask the member to link → they **approve** in StaqOn.\n4. `messages?username=…&after=<last_id>` — pull new replies back (Direction Out, poll the delta).\n5. Member can **revoke** consent anytime → `messages` returns 403.\n\n## Auth\n\nSet the collection variables once: **`apiKey`** (your `sk_live_…` key) and **`baseUrl`** (`https://staqon.one/api/v1/external`). Every request sends `X-API-Key: {{apiKey}}`.\n\n_(Note: `announce.php` is retired and omitted.)_"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "X-API-Key",
        "type": "string"
      },
      {
        "key": "value",
        "value": "{{apiKey}}",
        "type": "string"
      },
      {
        "key": "in",
        "value": "header",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://staqon.one/api/v1/external",
      "type": "string"
    },
    {
      "key": "apiKey",
      "value": "sk_live_your_key_here",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "Members",
      "item": [
        {
          "name": "Status (key context + usage)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/status.php",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "status.php"
              ]
            },
            "description": "Returns this API key's context and usage. **Key-only.**\n\n**Params:** none.\n**Success 200:** `{ok, workspace, team, team_type, total_members, active_members, api_key_usage, rate_limit, rate_remaining}`.\n**Errors:** 401 invalid key · 400 key's team is not an active external team."
          }
        },
        {
          "name": "List members",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/members.php?page=1&limit=50&search=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "members.php"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "50"
                },
                {
                  "key": "search",
                  "value": ""
                }
              ]
            },
            "description": "Paginated member directory for your external team. **Key-only.** Metadata only (no message content).\n\n**Query:** `page` (default 1) · `limit` (default 50, max 200) · `search` (optional, name/username).\n**Success 200:** `{ok, members:[{id,name,email,status,joined_at,last_message_at,messages_sent}], total, page, pages}`.\n**Errors:** 401 · 400 (non-external key)."
          }
        },
        {
          "name": "Get member",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/member.php?email=rahul@abc.com",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "member.php"
              ],
              "query": [
                {
                  "key": "email",
                  "value": "rahul@abc.com"
                }
              ]
            },
            "description": "Single member lookup by email (or `member_id`). **Key-only.**\n\n**Query:** `email` (or `member_id`).\n**Success 200:** `{ok, member:{id,name,email,joined_at,last_active,messages_sent,has_mpin}}`.\n**Errors:** 404 'Member not found in this team' · 401 · 400."
          }
        },
        {
          "name": "Add member",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/add-member.php",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "add-member.php"
              ]
            },
            "description": "Add an external member to your team (creates the user + isolated thread if new). **Key-only.**\n\n**Body:** `name` (required) · `email` (required) · `phone?` · `metadata?`.\n**Success 200:** `{ok, member_id, conversation_id, status, login_url}` — or `{ok, member_id, status:'existing', message}` if already in the team.\n**Errors:** 400 'name and email are required' · 400 'Invalid email format' · 401.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Rahul Sharma\",\n  \"email\": \"rahul@abc.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Remove member",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/remove-member.php",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "remove-member.php"
              ]
            },
            "description": "Remove a member from your team (archives their thread, keeps history). **Key-only.**\n\n**Body:** `email` (required).\n**Success 200:** `{ok, removed:true, member_id}`.\n**Errors:** 400 'email is required' · 404 'Member not found' · 400 'Member is not in this external team' · 401.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"rahul@abc.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "Messaging",
      "item": [
        {
          "name": "Send message (Direction In)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/message.php",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "message.php"
              ]
            },
            "description": "Send a message into a member's external thread, attributed to your workspace. **Key-only** (no approved link needed).\n\n**Body:** `email` (required) · `message` (required, max 60000).\n**Success 200:** `{ok, message_id, conversation_id}`.\n**Errors:** 400 'email and message are required' · 400 'message is too long (max 60000 chars)' · 404 'Member not found' · 404 'No active thread for this member' · 401.\n\nFires the `member.message` webhook + delivers to the member (in-app + FCM + real-time).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"rahul@abc.com\",\n  \"message\": \"Your loan is approved!\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Post comment (Direction In, tagged api)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/comment.php",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "comment.php"
              ]
            },
            "description": "Post a comment into a member's external thread by **username**, tagged `source=api` + your `external_ref` (for echo-dedup on read-back). **Key-only write** — NO approved link required (writing in is the workspace's authority). Delivered identically to a human message (in-app + FCM + Soketi real-time) and fires `member.message` (origin:api).\n\n**Body:** `username` (required) · `comment` (required, max 60000) · `external_ref?` (your id for this post).\n**Success 200:** `{ok, message_id, conversation_id, sent_at}`.\n**Errors:** 400 'username and comment are required' · 400 'comment is too long (max 60000 chars)' · 404 'No member with that username' · 404 'That member is not in your team' · 401.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"username\": \"4821k9037\",\n  \"comment\": \"Your documents are verified.\",\n  \"external_ref\": \"crm-msg-1001\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Read conversation (Direction Out, LINK-GATED)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/messages.php?username=4821k9037&limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "messages.php"
              ],
              "query": [
                {
                  "key": "username",
                  "value": "4821k9037"
                },
                {
                  "key": "limit",
                  "value": "50"
                }
              ]
            },
            "description": "Read the full chat thread between your team and a member. **Requires an APPROVED account link (member consent)** — see Account Linking → Link request. Returns 403 until the member approves, and again the moment they revoke (kill-switch).\n\n**Query:** `username` (required) · `limit` (default 50, max 200) · `before=<id>` (older page, newest-first) **or** `after=<id>` (incremental delta, oldest-first) — mutually exclusive.\n**Success 200:** `{ok, conversation_id, member:{username,name}, messages:[{id, sender_side(member|team|system), sender_name, content, content_type, source, external_ref, sent_at, edited, deleted, attachments:[{name,mime,size,is_image,url,url_available}]}], paging:{mode,limit,returned,has_more,next_before|next_after}}`.\n**Attachments:** `url` is a 1-day signed CDN link when `url_available:true`; deleted messages have blanked content.\n**Errors:** 400 'username is required' · 400 'use either before or after, not both' · 404 'No member with that username' · 404 'That member is not in your team' · **403 'No approved link for this member'** · 401."
          }
        }
      ]
    },
    {
      "name": "Escalations",
      "item": [
        {
          "name": "Raise escalation",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/escalation.php",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "escalation.php"
              ]
            },
            "description": "Raise an escalation case against a team member (on behalf of the workspace owner). **Key-only.**\n\n**Body:** `email` (required) · `title` (required, ≥2 chars) · `description` (required) · `due_date` (required, YYYY-MM-DD, ≥ tomorrow) · `priority?` (low|medium|high|critical, default medium).\n**Success 200:** `{ok, matter_id}`.\n**Errors:** 400 'email, title and description are required' · 400 'due_date must be a future date (YYYY-MM-DD, at least tomorrow)' · 404 'Member not found in this team' · 401.\n\nResolution follows the workspace's escalation resolve-policy (Settings → Escalation).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"rahul@abc.com\",\n  \"title\": \"Missing documents\",\n  \"description\": \"KYC docs not uploaded\",\n  \"priority\": \"high\",\n  \"due_date\": \"2026-12-31\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "Account Linking",
      "item": [
        {
          "name": "Link request (consent handshake)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/link-request.php",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "link-request.php"
              ]
            },
            "description": "Request to link a StaqOn member (by unique username) to your system. The member approves/declines in their StaqOn account (Account → Linking Requests). **Requests expire 15 minutes** after creation. On approval you may read their chat via `messages.php`; you're notified via `link.approved`/`link.declined`/`link.revoked` webhooks. **Key-only to request** (the member must already be in your team).\n\n**Body:** `username` (required) · `external_ref?` (unique within your team).\n**Success 200:** `{ok, link_id, status:'pending', expires_at}` — idempotent (repeat while pending returns the same link_id).\n**Errors:** 400 'username is required' · 404 'No member with that username' · 404 'That member is not in your team' · 409 'Already linked' · 409 'external_ref already linked to another member' · 401.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"username\": \"4821k9037\",\n  \"external_ref\": \"crm-cust-123\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "Webhooks",
      "description": "# Webhooks (StaqOn → your server)\n\nStaqOn POSTs a JSON callback to your subscribed URL when an external-team event fires. Subscribe and pick which events in StaqOn: **Workspace → Settings → API → Webhooks** (per external team). Each webhook has its own secret (`whsec_…`).\n\n## Delivery\n- Method **POST**, `Content-Type: application/json`.\n- Headers: `X-StaqOn-Event: <event>` and `X-StaqOn-Signature: <hex hmac>`.\n- Best-effort, **single attempt**, 5s timeout, no redirects (http/https only). Respond **2xx** to acknowledge.\n- **10 consecutive non-2xx** responses auto-disable the webhook (re-enable in Settings).\n\n## Signature — verify every request\nHMAC-**SHA256** of the **raw request body**, keyed by your webhook secret, **hex**-encoded:\n```\nexpected = hmac_sha256(raw_body, webhook_secret)      # lowercase hex\nvalid    = constant_time_equals(expected, X-StaqOn-Signature header)\n```\nReject the request if it doesn't match.\n\n## Envelope (all events)\n```\n{ \"event\": \"<name>\", \"timestamp\": \"<ISO-8601>\", \"data\": { … } }\n```\n\n## The 8 events + their `data`\n| event | fires when | data fields |\n|---|---|---|\n| `member.joined` | external member self-joins a team | `member_id`, `name`, `team_id` |\n| `member.message` | a message is posted into a member's external thread (outbound to you) | `member_id`, `email` **or** `username`, `message`, `direction:\"outbound\"`, `message_id` — plus `origin:\"api\"` + `external_ref` when posted via the Comment API |\n| `member.removed` | a member is removed from the team | `member_id`, `email`, `team_id` |\n| `escalation.created` | an escalation case is raised | `matter_id`, `title`, `priority`, `status:\"open\"`, `scope`, `target_member_id`, `target_email` |\n| `escalation.resolved` | an escalation case is resolved | `matter_id`, `status:\"resolved\"`, `resolved_by`, `resolution_notes` |\n| `link.approved` | member approves an account link | `user_id`, `username`, `link_id`, `external_ref` |\n| `link.declined` | member declines an account link | `user_id`, `username`, `link_id`, `external_ref` |\n| `link.revoked` | member revokes a previously-approved link (kill-switch) | `user_id`, `username`, `link_id`, `external_ref` |\n\n## Example payloads\n**member.message** (via Comment API — tagged origin:api):\n```\n{ \"event\":\"member.message\", \"timestamp\":\"2026-07-13T13:00:00+00:00\",\n  \"data\":{ \"member_id\":42, \"username\":\"8225d3073\", \"message\":\"Docs received, thanks.\",\n           \"direction\":\"outbound\", \"origin\":\"api\", \"external_ref\":\"crm-9931\", \"message_id\":7781 } }\n```\n**escalation.created**:\n```\n{ \"event\":\"escalation.created\", \"timestamp\":\"2026-07-13T13:00:00+00:00\",\n  \"data\":{ \"matter_id\":15, \"title\":\"Docs pending\", \"priority\":\"high\", \"status\":\"open\",\n           \"scope\":\"member\", \"target_member_id\":42, \"target_email\":\"rahul@abc.com\" } }\n```\n**link.revoked** (link.approved / link.declined share this shape):\n```\n{ \"event\":\"link.revoked\", \"timestamp\":\"2026-07-13T13:00:00+00:00\",\n  \"data\":{ \"user_id\":42, \"username\":\"8225d3073\", \"link_id\":17, \"external_ref\":\"crm-9931\" } }\n```\n",
      "item": []
    }
  ]
}