{
  "formatVersion": 1,
  "language": "en",
  "instructions": [
    "Choose the reader’s language, read the relevant workflow and endpoint, then consult its exact OpenAPI contract. Cite the sources you used. These exports do not grant permission to make live business requests.",
    "Preserve the published hosts, methods, paths, authentication, field names, types, required and nullable fields, enums, defaults, limits, and response codes. Translations describe the same API."
  ],
  "schemaScope": "This corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.",
  "specs": [
    {
      "title": "Bridge API",
      "url": "https://docs.bridge.new/api-reference/openapi.yaml",
      "source": "api-reference/openapi.yaml"
    },
    {
      "title": "Bridge Campaign Leads API",
      "url": "https://docs.bridge.new/api-reference/campaign-leads.openapi.yaml",
      "source": "api-reference/campaign-leads.openapi.yaml"
    }
  ],
  "format": "bridge-documentation-collection",
  "title": "Bridge Collected documentation — English",
  "description": "This corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.",
  "indexUrl": "https://docs.bridge.new/llms-en.json",
  "pages": [
    {
      "title": "Integrate with Bridge",
      "source": "https://docs.bridge.new/index",
      "markdownUrl": "https://docs.bridge.new/index.md",
      "description": "Connect your CRM, automation, or landing page with the REST API, webhooks, and Custom Connections.",
      "kind": "page",
      "contentMarkdown": "Build with your preferred coding tool\n\n## Connect your AI coding assistant\n\nGive your coding tool the current Bridge docs. Choose it below for setup, an example prompt and a connection check.\n\n  [Codex →](https://docs.bridge.new/connect-your-agent.md#codex)\n  [Claude Code →](https://docs.bridge.new/connect-your-agent.md#claude-code)\n  [Cursor →](https://docs.bridge.new/connect-your-agent.md#cursor)\n  [GitHub Copilot →](https://docs.bridge.new/connect-your-agent.md#github-copilot)\n\nDocs access needs no Bridge API Key. Workspace access is separate.\n\n## Choose your integration\n\n  \n\n    \n\n### [REST API](https://docs.bridge.new/credentials.md)\n\n      Create contacts, conversations and messages from your server.\n\n      **Access:** server-side `x-api-key`.\n    \n\n    \n\n### [Webhooks](https://docs.bridge.new/webhooks.md)\n\n      Receive Bridge events at your HTTPS endpoint.\n\n      **Access:** verify webhook credentials and freshness.\n    \n\n    \n\n### [Custom Connections](https://docs.bridge.new/lead-capture/index.md)\n\n      Capture landing-page leads and queue an opening WhatsApp message.\n\n      **Access:** `captureKey` and reCAPTCHA; no REST API Key.\n    \n\n  \n\n## Make your first REST API request\n\n  \n\n### Get your API credentials\n\n    Follow [Authentication](https://docs.bridge.new/credentials.md) to generate an API key in the Bridge Admin Console. Ask your Bridge administrator for access if needed. Keep the key on your server.\n  \n\n  \n\n### Check the connection\n\n    Use your `x-api-key` header to call the authenticated [health check](https://docs.bridge.new/api-reference/health/health-check.md) at `https://api-connect-us.bridge.new/bridge/api/health`.\n\n    Continue in the **REST API** tab. Each endpoint lists its fields, responses, and examples.\n  \n\n**Note**\n\n  Custom Connections uses a separate endpoint and public `captureKey`. Follow its [integration guide](https://docs.bridge.new/lead-capture/integration.md). A `202` confirms acceptance, not WhatsApp delivery.\n\n## Build from the published contract\n\n- [Create a Contact and send a message](https://docs.bridge.new/guides/first-conversation.md) — follow the complete Contact → conversation → message workflow.\n- [Send a PDF or attachment](https://docs.bridge.new/guides/send-attachments.md) — checksum, upload URL, file upload and message.\n- [Send a WhatsApp template](https://docs.bridge.new/guides/send-template.md) — discover variables and supply caller-required values.\n- [Start Agent outbound and hand off](https://docs.bridge.new/guides/agent-outbound.md) — choose an Agent, understand `forceSend`, reuse and recovery.\n- [Look up identifiers](https://docs.bridge.new/guides/identifiers.md) and [page through results](https://docs.bridge.new/guides/pagination.md) — use the right IDs and cursors.\n- [Rate limits](https://docs.bridge.new/rate-limits.md) — share the request budget and handle `429` responses.\n- [Error codes](https://docs.bridge.new/error-codes.md) — interpret REST API errors. Custom Connections has a separate [error reference](https://docs.bridge.new/lead-capture/errors.md).\n- [WhatsApp template parameters](https://docs.bridge.new/templates-and-parameters.md) — open conversations with WhatsApp templates and supply their variables.\n- [Changelog](https://docs.bridge.new/changelog.md) — review changes that affect integrations.\n\nRead without MCP using the [trilingual documentation index](https://docs.bridge.new/llms.txt), [English index](https://docs.bridge.new/llms-en.json), [English collection](https://docs.bridge.new/llms-full-en.json), or [integration guidance for AI agents](https://docs.bridge.new/skill.md). Use the exact [REST Integrations OpenAPI](https://docs.bridge.new/api-reference/openapi.yaml) or [Custom Connections OpenAPI](https://docs.bridge.new/api-reference/campaign-leads.openapi.yaml) for schemas.\n\nFor help using the Bridge application, visit the [Help Center](https://help.bridge.new).\n"
    },
    {
      "title": "Connect your AI coding assistant",
      "source": "https://docs.bridge.new/connect-your-agent",
      "markdownUrl": "https://docs.bridge.new/connect-your-agent.md",
      "description": "Connect Codex, Claude Code, Cursor or GitHub Copilot to current Bridge documentation, then build from the exact API contract.",
      "kind": "page",
      "contentMarkdown": "Give your coding assistant access to the current Bridge documentation before asking it to build your integration. Choose your tool below, add the public MCP endpoint, and run the connection check.\n\n| Setting | Value |\n| --- | --- |\n| Server URL | `https://docs.bridge.new/mcp` |\n| Connection | Remote HTTP / Streamable HTTP |\n| Suggested local name | `bridgeDocs` |\n| Bridge credentials | None for this documentation server |\n\n**Note**\n\n  This connects your assistant to **Bridge documentation**. It does not connect it to your workspace or give it permission to create Contacts or send messages. Your integration uses the separate [REST API credentials](https://docs.bridge.new/credentials.md), or the public [Custom Connections](https://docs.bridge.new/lead-capture/index.md) setup. The documentation server also exposes an optional feedback tool that sends documentation comments to Bridge.\n\n## Choose your coding assistant\n\n  \n\n### [Codex](https://docs.bridge.new/connect-your-agent.md#codex)\n\nCLI and local IDE configuration.\n\n  \n\n### [Claude Code](https://docs.bridge.new/connect-your-agent.md#claude-code)\n\nOne command, then check the connection.\n\n  \n\n### [Cursor](https://docs.bridge.new/connect-your-agent.md#cursor)\n\nAdd the server to your project's MCP file.\n\n  \n\n### [GitHub Copilot](https://docs.bridge.new/connect-your-agent.md#github-copilot)\n\nGuided setup in VS Code or portable project configuration.\n\n## Codex\n\n**Before you start:** have Codex CLI or the Codex IDE extension installed. For the CLI, add the server in your terminal:\n\n```bash\ncodex mcp add bridgeDocs --url https://docs.bridge.new/mcp\ncodex mcp list\n```\n\nThe list should include `bridgeDocs` with the Bridge documentation URL. Listing it confirms configuration; use [the connection check below](https://docs.bridge.new/connect-your-agent.md#check-the-connection) to confirm that it can actually read documentation.\n\nIf you prefer editing configuration, add this entry to `~/.codex/config.toml`. Keep your other settings and server entries:\n\n```toml\n[mcp_servers.bridgeDocs]\nurl = \"https://docs.bridge.new/mcp\"\n```\n\nRestart the client or extension after changing its configuration. In the Codex terminal interface, `/mcp` shows active servers. Local Codex clients can share configuration on the same host; remote environments can have separate configuration and policy.\n\n**Example prompt:**\n\n> Use bridgeDocs to read the Bridge REST contract and the first-conversation guide. Prepare a Node.js integration that creates a Contact, starts a Conversation and creates a Message. List the required identifiers and configuration first. Keep every API field and type unchanged, cite your sources, and do not make live API requests.\n\n[Official Codex MCP guidance](https://learn.chatgpt.com/docs/extend/mcp?surface=cli).\n\n## Claude Code\n\n**Before you start:** have Claude Code installed. Add the server to your personal configuration so it is available across projects:\n\n```bash\nclaude mcp add --transport http bridgeDocs --scope user https://docs.bridge.new/mcp\nclaude mcp list\n```\n\nFor a shared project configuration instead, use `--scope project` rather than `--scope user`. Review the resulting project configuration before committing it.\n\nStart Claude Code and use `/mcp` to inspect `bridgeDocs`. Then run [the connection check below](https://docs.bridge.new/connect-your-agent.md#check-the-connection). The Bridge documentation endpoint does not need a Bridge API Key or an OAuth login.\n\n**Example prompt:**\n\n> Use bridgeDocs to read the current Bridge webhook verification and receiver guides. Implement a Python receiver with the documented HMAC representation and durable acceptance before acknowledgement. Add offline tests using the published signature vector. Explain the timestamp limitation and duplicate handling, and cite the pages you used.\n\n[Official Claude Code MCP guidance](https://code.claude.com/docs/en/mcp).\n\n## Cursor\n\n**Before you start:** open your integration project in Cursor. Create or update `.cursor/mcp.json` in that project. Merge the entry with any existing servers:\n\n```json\n{\n  \"mcpServers\": {\n    \"bridgeDocs\": {\n      \"url\": \"https://docs.bridge.new/mcp\"\n    }\n  }\n}\n```\n\nFor a personal configuration across projects, use `~/.cursor/mcp.json` instead. In Cursor's **Customize** sidebar, check that the server is enabled. Open an Agent conversation and run [the connection check below](https://docs.bridge.new/connect-your-agent.md#check-the-connection); relevant MCP tools appear under **Available Tools**. Review tool requests when Cursor asks.\n\n**Example prompt:**\n\n> Use bridgeDocs to find the complete PDF attachment workflow. Implement a server-side upload that hashes the exact file bytes as Base64 SHA-256, requests a presigned upload, sends the required PUT and creates a Message with the returned mediaId. Preserve the documented fields and explain each stage's failure handling. Do not send a Bridge API Key to the upload URL.\n\n[Official Cursor MCP guidance](https://cursor.com/docs/mcp).\n\n## GitHub Copilot\n\n**Before you start:** enable GitHub Copilot in VS Code and open your project folder. Select **Copilot** as the session target.\n\n1. Open the Command Palette with `Command` + `Shift` + `P` on macOS, or `Ctrl` + `Shift` + `P` on Windows/Linux.\n2. Run **MCP: Add Server** and choose **HTTP (HTTP or Server-Sent Events)**.\n3. Enter `https://docs.bridge.new/mcp` and name the server `bridgeDocs`.\n4. Choose **Global → Copilot Global** for your personal configuration, when available. Review the destination shown by your VS Code version and any server trust request.\n5. Open a Copilot conversation, enable the Bridge documentation tools for that session, and run [the connection check below](https://docs.bridge.new/connect-your-agent.md#check-the-connection).\n\nFor a shared project using the current portable Copilot configuration, create or merge this entry into `.mcp.json` at the workspace root:\n\n```json\n{\n  \"mcpServers\": {\n    \"bridgeDocs\": {\n      \"type\": \"http\",\n      \"url\": \"https://docs.bridge.new/mcp\",\n      \"tools\": [\"*\"]\n    }\n  }\n}\n```\n\nThe portable Copilot format above differs from editor-managed `.vscode/mcp.json`, which uses a `servers` object. Use the file for the connection you selected; do not mix the two shapes. If the portable destination is unavailable in your version, use the guided setup and its selected configuration file.\n\n**Example prompt:**\n\n> Use bridgeDocs to read the Custom Connections guide and its separate OpenAPI contract. Prepare a landing-page form that records consent, obtains a new CAPTCHA token for each attempt and shows a 202 as accepted with its requestId. Keep captureKey, request fields and the configured CAPTCHA action unchanged. Do not use a REST API Key in the page.\n\n[Official VS Code setup](https://code.visualstudio.com/docs/agent-customization/mcp-servers) · [Copilot configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration).\n\n## Check the connection\n\nAsk this after configuring any of the four clients:\n\n> Use the Bridge documentation MCP server to search for creating a Message. Read the MessageRequest schema from the published REST OpenAPI. Which three fields are required, and does a successful 201 prove WhatsApp delivery? Cite the exact Bridge source you read. Do not call the business API.\n\nA successful check uses a documentation tool and cites a Bridge page or schema. It should identify `conversationId`, `messageBody` and `enableExternalBroadcast` as required. It should distinguish a created Message from a provider delivery receipt. A plausible answer without a tool call or source is not a connection test.\n\n## Read with any AI tool, without MCP\n\nNo MCP connection is required to read Bridge documentation. Give an assistant that can fetch URLs the [trilingual documentation index](https://docs.bridge.new/llms.txt), then choose the sources for your task and language:\n\n| Language | Page index | Collected documentation |\n| --- | --- | --- |\n| English | [English index](https://docs.bridge.new/llms-en.json) | [English collection](https://docs.bridge.new/llms-full-en.json) |\n| Español | [Índice en español](https://docs.bridge.new/llms-es.json) | [Documentación en español](https://docs.bridge.new/llms-full-es.json) |\n| Português do Brasil | [Índice em português](https://docs.bridge.new/llms-pt-br.json) | [Documentação em português](https://docs.bridge.new/llms-full-pt-br.json) |\n\nThe index points to individual `.md` pages and the exact OpenAPI files. The [combined collection](https://docs.bridge.new/llms-full.txt) includes all three languages; a language-specific collection is smaller. Collected endpoint descriptions do not replace the complete schemas in OpenAPI.\n\nThe language-specific files use JSON: the index lists page URLs, and each collection provides the page text as Markdown in its `contentMarkdown` field. The general `llms.txt` and `llms-full.txt` remain plain Markdown.\n\n**Example prompt:**\n\n> Read https://docs.bridge.new/llms.txt and select the English sources for sending a PDF. Read the full attachment guide and the linked REST OpenAPI before writing code. Explain the upload sequence, required fields and delivery limitations. Preserve the published API signatures and cite the URLs you read. Prepare code only; do not make live requests.\n\nIf you use MCP search, specify `language: \"en\"`, `language: \"es\"` or `language: \"pt-BR\"` to retrieve the requested language. HTTP exports use `.md`; MCP filesystem page reads use `.mdx`. For a concise capability and source guide, read [Bridge integration guidance](https://docs.bridge.new/skill.md).\n\n## Build with the right contract\n\nThe server provides documentation search, complete page/schema reading in a virtual documentation filesystem, and optional documentation feedback. The filesystem tool does not run commands on your computer. Feedback is separate from searching and reading; use it only when you intend to send a documentation comment.\n\nGive your assistant the task and relevant workflow:\n\n- [First conversation](https://docs.bridge.new/guides/first-conversation.md): Contact → Conversation → Message.\n- [Attachments](https://docs.bridge.new/guides/send-attachments.md): file bytes → checksum → presigned PUT → Message.\n- [Templates](https://docs.bridge.new/guides/send-template.md): discover variables before sending.\n- [Agent outbound](https://docs.bridge.new/guides/agent-outbound.md): select an Agent and understand `forceSend`, reuse and recovery.\n- [Webhook receiver](https://docs.bridge.new/webhooks/receive-events.md): verify, persist, acknowledge and process.\n\nThe [documentation index](https://docs.bridge.new/llms.txt) locates pages. [Collected documentation](https://docs.bridge.new/llms-full.txt) gathers guides and endpoint descriptions; it does not include every schema. Before generating code, read the individual endpoint Markdown or the appropriate [REST OpenAPI](https://docs.bridge.new/api-reference/openapi.yaml) or [Custom Connections OpenAPI](https://docs.bridge.new/api-reference/campaign-leads.openapi.yaml).\n\nAsk the assistant to preserve hosts, methods, paths, authentication, identifiers, field names, types, required/nullable fields, enums, defaults, limits and response codes. If published sources conflict, identify the passages and confirm the current behavior before relying on them.\n\n## If the server does not appear\n\n| Symptom | What to check |\n| --- | --- |\n| Server missing | Confirm the file location or configuration scope, the server name and the client/environment you are using. Keep existing configuration when adding the entry. |\n| Configured but no tools | Restart or reload the client, enable the server and check its MCP status or logs. A managed organization may restrict custom servers. |\n| Authentication prompt | Bridge's public documentation server needs no Bridge API Key. Check the URL and your client's or organization's policy. Do not paste workspace credentials into chat. |\n| Answer without citations | Ask explicitly to use `bridgeDocs`, search, and read the endpoint schema. Run the connection check again. |\n| MCP temporarily unavailable | Read the [index](https://docs.bridge.new/llms.txt), endpoint Markdown and OpenAPI directly. The documentation server does not have a separate uptime commitment. |\n\nClient interfaces change over time. Setup options can differ by version and organization policy; use the linked provider documentation if your menus differ. Provider instructions last checked: **November 14, 2026**.\n"
    },
    {
      "title": "REST API authentication",
      "source": "https://docs.bridge.new/credentials",
      "markdownUrl": "https://docs.bridge.new/credentials.md",
      "description": "Generate and use a server-side API key for the REST Integrations API, and keep it separate from webhook credentials and Custom Connections.",
      "kind": "page",
      "contentMarkdown": "## Overview\n\nEvery request to the **REST Integrations API** at `https://api-connect-us.bridge.new` uses an **API Key** in the `x-api-key` header. This key identifies your application and authorizes it to access Bridge resources on behalf of your organization.\n\n| Integration surface | Credential handling |\n| --- | --- |\n| REST Integrations API | Server-side `x-api-key` sent to `https://api-connect-us.bridge.new`. |\n| Webhooks | Your receiver checks the configured webhook API key, HMAC signature and freshness policy. Follow [Verify signatures](https://docs.bridge.new/webhooks/verifying-signatures.md). |\n| Custom Connections | Browser-side public `captureKey` and reCAPTCHA; no authentication header. Follow its [integration guide](https://docs.bridge.new/lead-capture/integration.md). |\n\n**Note**\n\n  You will need access to Bridge's admin console at https://admin.bridge.new.\n\n  If you don't have one, please ask your Bridge Admin to create you a user.\n\n---\n\n## Generating an API Key\n\n  \n\n### Open the Admin Console\n\n    Go to your [Bridge Dashboard](https://admin.bridge.new) and log in with your credentials.\n  \n\n  \n\n### Navigate to API Keys\n\n    In the left panel, go to **Developers → API Keys Management**.\n  \n\n  \n\n### Create a new key\n\n    Click **Generate New Key**. You can give it a descriptive name and set an expiration date.\n\n    \n\n**Warning**\n\n      **Important:**\\\n      Your API Key will only be displayed once upon creation. Copy it immediately and store it in a secure location — you will not be able to retrieve it later.\n    \n\n  \n\n---\n\n## Using your API Key\n\nInclude your REST API Key in the `x-api-key` header on every request to the REST Integrations host:\n\n```bash\ncurl -X GET \"https://api-connect-us.bridge.new/bridge/api/health\" \\\n  -H \"x-api-key: YOUR_API_KEY\"\n```\n\n```javascript\nconst response = await fetch(\"https://api-connect-us.bridge.new/bridge/api/health\", {\n  method: \"GET\",\n  headers: {\n    \"x-api-key\": \"YOUR_API_KEY\",\n  },\n});\n\nconst data = await response.json();\nconsole.log(data);\n```\n\n```python\nimport requests\n\nresponse = requests.get(\n    \"https://api-connect-us.bridge.new/bridge/api/health\",\n    headers={\"x-api-key\": \"YOUR_API_KEY\"}\n)\n\nprint(response.json())\n```\n\n\n**Note**\n\n  Use the `GET /bridge/api/health` endpoint to verify that your API Key is valid and your connection is working correctly.\n\n---\n\n## Security Best Practices\n\n- **Use environment variables** — Store your API Key in an environment variable (e.g. `BRIDGE_API_KEY`) instead of hardcoding it in your source code.\n- **Never expose keys in client-side code** — API Keys should only be used in server-side applications. Never include them in frontend code, mobile apps, or public repositories.\n- **Set an expiration date** — Always configure an expiration when creating a key. Avoid using keys without a defined expiry.\n- **One key per application** — Use a separate API Key for each service or application that connects to Bridge. This limits the impact if a key is compromised.\n- **Rotate keys periodically** — Create a new key and update your applications before deactivating the old one. This ensures zero downtime during rotation.\n\n---\n\n## Troubleshooting\n\n| Error | Possible Cause | Solution |\n| ----- | -------------- | -------- |\n| `401 Unauthorized` | Invalid, expired, or missing API Key | Verify that your key is correct, active, and included in the `x-api-key` header |\n| `403 Forbidden` | The API Key does not have access to the requested resource | Check that your key has the necessary permissions for this endpoint |\n| Cannot retrieve API Key | The key is only shown once at creation time | Generate a new API Key from the Admin Console |\n\nFor a complete list of error codes, see the [Error Codes](https://docs.bridge.new/error-codes.md) reference.\n\n---\n\n## Related Topics\n\n- [Error Codes](https://docs.bridge.new/error-codes.md) — Full reference of Bridge API error codes\n- [REST API Reference](https://docs.bridge.new/api-reference/health/health-check.md) — Explore all available endpoints\n- [Webhooks](https://docs.bridge.new/webhooks.md) — Set up real-time event notifications\n"
    },
    {
      "title": "Glossary and identifiers",
      "source": "https://docs.bridge.new/guides/identifiers",
      "markdownUrl": "https://docs.bridge.new/guides/identifiers.md",
      "description": "Find the correct workspace, Contact, Bridge User, Agent, conversation, template, line, and media identifiers without confusing them.",
      "kind": "page",
      "contentMarkdown": "## Start with the right integration\n\nThe **REST Integrations API** reads or changes workspace resources from your server using `x-api-key`. **Webhooks** send events from Bridge to your receiver. **Custom Connections** registers leads from a landing page using its separate public endpoint, `captureKey`, and reCAPTCHA.\n\nAn API Key is a server-side secret. A Custom Connections `captureKey` is designed to be in the landing page. These values are not interchangeable. See [Authentication](https://docs.bridge.new/credentials.md) and [Custom Connections](https://docs.bridge.new/lead-capture/index.md).\n\n## People, Agents, and lines\n\n- A **Contact** is the external person your business communicates with. Contact endpoints and external conversation participants refer to this person.\n- A **Bridge User** is an internal human user. Internal-user lookup and handoff assignees refer to these users.\n- An **Agent** is an automated Agent exposed by the Agents API. Its type is `LEADSCOUT` or `SERVICE_AGENT`; it has its own identifier, availability, and lines.\n- A **provider** is a channel type, such as `WHATSAPP`. A **line** is a specific channel used for a conversation. A provider type, phone number, and `providerChannelId` are different values.\n- A **workspace** scopes the request. A **brand** is another identifier used where the operation requires or accepts it; it is not a workspace or a line.\n\nA Contact's `ownerId` refers to a Bridge User, not an automated Agent. Resolve it with the [internal-user read endpoint](https://docs.bridge.new/api-reference/internal-users-api/retrieve-an-internal-user-by-id.md).\n\n## Where each identifier comes from\n\nUse identifiers from the intended workspace. Do not reuse an identifier merely because the same phone number, email, or name appears in another workspace.\n\n| Value you need | Where to obtain or verify it | What to pass |\n| --- | --- | --- |\n| `workspaceId` | Use the workspace identifier supplied for your integration. If it is missing, ask your Bridge administrator. The published REST contract does not provide a global workspace-list endpoint. | The workspace ID in the operation's path. Custom Connections also requires it in its body and, for browser requests, query string. |\n| Contact ID | [Look up a Contact](https://docs.bridge.new/api-reference/contacts-api/retrieve-a-contact-by-an-identifier.md) by `PHONE`, `EMAIL`, or `ID`; list, create, and upsert responses also return the Contact's `id`. | The returned `id` for `{contactId}` or an identifier object with `identifierType: \"ID\"`, where that operation accepts it. |\n| Bridge User ID | [Look up an internal user](https://docs.bridge.new/api-reference/internal-users-api/retrieve-an-internal-user-by-an-identifier.md) by `EMAIL` or `ID`, or use the internal-user list. | The returned `id` for `{userId}` or an internal identifier object. Internal-user lookup does not accept `PHONE`. |\n| Agent ID | [List Agents](https://docs.bridge.new/api-reference/agents-api/list-the-agents-of-a-workspace.md). Read `agents[].id`, `type`, and `available`. | `agents[].id` for `{agentId}` in Agent outbound. `available` indicates whether that Agent can start conversations. |\n| Agent's line ID | In the same Agents response, read that Agent's `lines[].providerChannelId`. | A line ID belonging to that Agent when the operation accepts `contact.providerChannelId`. Read the outbound reference for line-selection rules. |\n| General provider channel | [List provider channels](https://docs.bridge.new/api-reference/provider-channels-api/get-provider-channels-by-workspace-id.md). This response publishes `provider` and `identifier`, not a channel UUID. | Use these fields only for the purposes documented by the consuming operation. Do not substitute the phone-like `identifier` for `providerChannelId`. |\n| `brandId` | An Agent response can expose its `brandId`. For another flow, obtain the intended brand ID from your integration configuration or administrator; the published contract does not provide a general brand-list endpoint. | The brand ID accepted by the operation. A brand ID is not a line ID. |\n| `teamId` | Obtain the intended internal team ID from your Bridge administrator or integration configuration. The published REST contract does not provide a team-list endpoint. | The documented team assignment option on conversation creation; do not also supply `participants.internals`. |\n| Conversation ID | Create or retrieve a conversation and retain its returned `id`. Outbound responses contain `conversation.id`. | `{conversationId}` or `conversationId`, exactly where the operation requires it. |\n| Template | [Discover a template](https://docs.bridge.new/api-reference/templates-api/discover-a-templates-agnostic-variable-inventory.md) using its ID or name. The response returns `id`, `name`, and variable keys. | Use the selector expected by the sending operation; see the next section. |\n| `mediaId` | [Create presigned uploads](https://docs.bridge.new/api-reference/files-api/create-presigned-uploads-for-files.md). Each response array item has `mediaId` and `presignUrl`. Complete the required PUT upload. | The returned `mediaId` in `mediaIds`, or the media field supported by that operation. Do not send the presigned URL as a media ID. |\n\n**Note**\n\n  Agents lists expose line IDs in the context of each Agent. This is not a general discovery method for every workspace line. If another flow requires a `providerChannelId` you cannot obtain from the published response, ask your Bridge administrator or [support](mailto:support@sainapsis.com) for the intended line ID. Do not generate one or derive one from a telephone number.\n\n## Similar names can require different fields\n\n**Contact lookup** takes query parameters named `type` and `value`. An external participant in a conversation takes `identifier` and `identifierType`, plus the other fields required by that operation. Do not copy a lookup query directly into a participant body.\n\n**Template discovery** accepts an ID or name in the path parameter `{templateId}`. **Conversation onboarding** uses `onboardingTemplates[].templateId`. **Outbound template selection** uses the `template` object's `id` or `name`; read that endpoint's schema rather than renaming every selector to `templateId`.\n\nUse each returned variable's `key` in template `parameters`. Variable keys are not Meta slot indexes. The [template parameters guide](https://docs.bridge.new/templates-and-parameters.md) explains required values, defaults, and the difference between omitted and blank values.\n\n## Keep result identifiers separate\n\n| Returned value | Identifies | What it does not establish by itself |\n| --- | --- | --- |\n| `messageId` | A message created through the message endpoint | A provider delivery receipt |\n| `trackingId` | An accepted LeadScout handoff request | Completed assignment of the requested users |\n| `requestId` | An accepted Custom Connections submission | Delivery of the opening WhatsApp message |\n| `eventId` | An identifier supplied with a webhook event; some event families use entity-derived values | A globally unique deduplication key or delivery-order guarantee |\n\nFollow [Receive events](https://docs.bridge.new/webhooks/receive-events.md) for event identity and duplicate handling. See [Responses and errors](https://docs.bridge.new/guides/responses.md) for status codes, asynchronous results, and retry decisions. For a complete flow, follow [your first conversation](https://docs.bridge.new/guides/first-conversation.md) or [sending an attachment](https://docs.bridge.new/guides/send-attachments.md).\n"
    },
    {
      "title": "Create a contact and send a message",
      "source": "https://docs.bridge.new/guides/first-conversation",
      "markdownUrl": "https://docs.bridge.new/guides/first-conversation.md",
      "description": "Follow a complete server-side flow from a Contact to a Conversation and a Message, preserving the returned identifiers.",
      "kind": "page",
      "contentMarkdown": "This recipe creates a Contact, starts a Conversation with an internal Bridge User, and creates a Message. Run it on your server.\n\n**Warning**\n\n  Use a test Contact you are authorized to message. Creating a Conversation can run the workspace's onboarding templates before the separate Message request. Review that configuration first; setting the later Message's broadcast flag to false does not disable Conversation onboarding.\n\n## Before you start\n\n- Get a server-side API Key from [Authentication](https://docs.bridge.new/credentials.md).\n- Obtain your workspace ID, an active WhatsApp line's `providerChannelId`, and an internal Bridge User's email. See [Identifiers and concepts](https://docs.bridge.new/guides/identifiers.md).\n- Use a new test phone number in international format. If the Contact already exists, use [contact lookup](https://docs.bridge.new/api-reference/contacts-api/retrieve-a-contact-by-an-identifier.md) instead of creating it again.\n- Check any required custom attributes and the workspace's onboarding sequence. This minimal example does not supply custom attributes or override templates.\n- Choose `enableExternalBroadcast` explicitly: `true` requests external delivery when conversation and recipient rules permit it; `false` still creates the Message but skips external broadcast. Neither value confirms provider delivery. This flag does not override visibility or permissions.\n\n## The sequence\n\n| Step | Request | Value to keep |\n| --- | --- | --- |\n| Check access | `GET /bridge/api/health` | Successful HTTP response |\n| Create Contact | `POST /bridge/api/v1/workspaces/{workspaceId}/contacts` | Contact `id` |\n| Start Conversation | `POST /bridge/api/v1/workspaces/{workspaceId}/conversations` | Conversation `id` |\n| Create Message | `POST /bridge/api/v1/workspaces/{workspaceId}/messages` | `messageId` |\n\nThe Contact's `id` becomes the external participant's `identifier` with `identifierType: \"ID\"`. The Conversation's `id` becomes the Message's `conversationId`.\n\n## Run the example\n\nUse Node.js with built-in `fetch`. Save this as `first-conversation.mjs`. Supply these environment variables through your server's secret and configuration settings:\n\n| Variable | Value |\n| --- | --- |\n| `BRIDGE_API_KEY` | Your secret API Key |\n| `BRIDGE_WORKSPACE_ID` | The intended workspace ID |\n| `BRIDGE_PROVIDER_CHANNEL_ID` | The authorized WhatsApp line ID, not its phone number |\n| `BRIDGE_USER_EMAIL` | An internal Bridge User in the same workspace |\n| `TEST_CONTACT_PHONE` | A new, authorized test Contact's phone number |\n| `ENABLE_EXTERNAL_BROADCAST` | Exactly `true` or `false`; no default is assumed |\n\nThe names and message below are examples. Replace them with your intended test content before running.\n\n```javascript\nconst BASE = \"https://api-connect-us.bridge.new\";\n\nfunction required(name) {\n  const value = process.env[name];\n  if (!value) throw new Error(`Set ${name} before running this example.`);\n  return value;\n}\n\nconst apiKey = required(\"BRIDGE_API_KEY\");\nconst workspaceId = required(\"BRIDGE_WORKSPACE_ID\");\nconst providerChannelId = required(\"BRIDGE_PROVIDER_CHANNEL_ID\");\nconst internalEmail = required(\"BRIDGE_USER_EMAIL\");\nconst cellphone = required(\"TEST_CONTACT_PHONE\");\nconst broadcast = required(\"ENABLE_EXTERNAL_BROADCAST\");\nif (![\"true\", \"false\"].includes(broadcast)) {\n  throw new Error(\"ENABLE_EXTERNAL_BROADCAST must be true or false.\");\n}\nconst workspacePath = `/bridge/api/v1/workspaces/${encodeURIComponent(workspaceId)}`;\n\nasync function request(method, path, body, expectedStatus) {\n  const response = await fetch(`${BASE}${path}`, {\n    method,\n    headers: {\n      \"x-api-key\": apiKey,\n      ...(body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n    },\n    ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n  });\n  const result = await response.json().catch(() => null);\n  if (response.status !== expectedStatus) {\n    const code = result?.code ? ` (${result.code})` : \"\";\n    throw new Error(`${method} ${path}: HTTP ${response.status}${code}`);\n  }\n  return result;\n}\n\nawait request(\"GET\", \"/bridge/api/health\", undefined, 200);\n\nconst contact = await request(\"POST\", `${workspacePath}/contacts`, {\n  firstName: \"Alex\",\n  lastName: \"Example\",\n  languageCode: \"EN\",\n  cellphone,\n}, 201);\nif (!contact?.id || contact.workspaceId !== workspaceId) {\n  throw new Error(\"The Contact response does not identify the intended workspace.\");\n}\n\nconst conversation = await request(\"POST\", `${workspacePath}/conversations`, {\n  participants: {\n    externals: [{\n      identifier: contact.id,\n      identifierType: \"ID\",\n      messageProviderType: \"WHATSAPP\",\n      providerChannelId,\n    }],\n    internals: [{ identifier: internalEmail, identifierType: \"EMAIL\" }],\n  },\n  name: \"Integration test\",\n  status: \"ACTIVE\",\n}, 201);\nif (!conversation?.id || conversation.workspaceId !== workspaceId) {\n  throw new Error(\"The Conversation response does not identify the intended workspace.\");\n}\n\nconst message = await request(\"POST\", `${workspacePath}/messages`, {\n  conversationId: conversation.id,\n  messageBody: \"Hello from our authorized Bridge integration test.\",\n  enableExternalBroadcast: broadcast === \"true\",\n}, 201);\nif (!message?.messageId) throw new Error(\"The Message response has no messageId.\");\n\nconsole.log({\n  contactId: contact.id,\n  conversationId: conversation.id,\n  messageId: message.messageId,\n});\n```\n\n```bash\nnode first-conversation.mjs\n```\n\n## Understand the result\n\n- A `201` Contact response gives you the Contact's identity and workspace. Read `type` to see its actual `NORMAL` or `PARTIAL` classification.\n- A `201` Conversation response gives you the ID to use next. When no line is free, Bridge can recover a released Conversation with exactly the requested participants. That keeps its ID and history; the requested `name` and `status` are not applied in that recovery case.\n- A `201` Message response supplies `messageId`. It does not by itself prove that WhatsApp delivered the message to the Contact.\n\nThis recipe explicitly assigns an internal Bridge User. For a team assignment, use the documented `teamId` option instead of `participants.internals`; they are mutually exclusive.\n\n## If a step fails\n\nThe example stops at the failed request and never retries a write automatically. A network failure can leave the outcome uncertain. Check the relevant resource before repeating a create or send operation.\n\n| Situation | Next action |\n| --- | --- |\n| Contact already exists | Use contact lookup and its returned `id`; do not keep creating the same Contact. |\n| Brand or line error | Confirm the authorized line or brand with your administrator; a line's phone number is not its ID. |\n| Template `0604`–`0608` | Follow [WhatsApp template parameters](https://docs.bridge.new/templates-and-parameters.md), including workspace configuration errors. |\n| Message rejected because the Conversation is released | Inspect the Conversation's state and the operation's error reference before taking another action. |\n| `429` | Follow [Rate limits](https://docs.bridge.new/rate-limits.md), including `Retry-After`. |\n\nSee [Responses and errors](https://docs.bridge.new/guides/responses.md) for response shapes and recovery decisions.\n\n## Next steps\n\n- [Upload and send an attachment](https://docs.bridge.new/guides/send-attachments.md).\n- [Choose and fill a WhatsApp template](https://docs.bridge.new/guides/send-template.md).\n- [Create a Contact](https://docs.bridge.new/api-reference/contacts-api/create-a-new-contact.md), [create a Conversation](https://docs.bridge.new/api-reference/conversations-api/creates-a-new-conversation-with-flexible-user-identification.md), and [create a Message](https://docs.bridge.new/api-reference/messages-api/create-a-new-message.md) for the complete request and response contracts.\n"
    },
    {
      "title": "Send PDFs and other attachments",
      "source": "https://docs.bridge.new/guides/send-attachments",
      "markdownUrl": "https://docs.bridge.new/guides/send-attachments.md",
      "description": "Send a PDF or another supported file in a conversation: calculate its checksum, upload it to the presigned URL, and attach its media ID to a Message.",
      "kind": "page",
      "contentMarkdown": "To send a PDF or another supported attachment, request an upload from Bridge, upload the file bytes to the returned URL, then create a Message containing the returned `mediaId`. The complete server-side example below sends a PDF.\n\n## Before you start\n\n- Have an existing Conversation in the intended workspace. See [First conversation](https://docs.bridge.new/guides/first-conversation.md).\n- Choose a PDF you are authorized to share and confirm it meets the [Files API](https://docs.bridge.new/api-reference/files-api/create-presigned-uploads-for-files.md) limits.\n- Keep your Bridge API Key on your server. Do not add it to the presigned upload request.\n- Choose the Message's `enableExternalBroadcast` flag explicitly.\n\n## Validate the file before requesting an upload\n\nCheck the actual file on your server: its extension, MIME type and byte length must fit the published [Files API](https://docs.bridge.new/api-reference/files-api/create-presigned-uploads-for-files.md) rules. The presign request carries metadata and a checksum, not the file bytes or a size field. A `201` response prepares an upload; it is not proof that the actual bytes were inspected, that the uploaded size meets the published limits, or that a messaging provider will accept the file.\n\nIf you omit `contentType`, still copy the returned `presignUrl.headers` into the PUT request: Bridge can derive `Content-Type` from the file name. If you supply it, use the MIME type of the actual file and keep it consistent with `fileType` and the extension. Renaming a file does not convert its contents.\n\nAfter PUT succeeds, upload processing may still be pending. If a Message request reports a [file upload error](https://docs.bridge.new/error-codes.md#file-upload-errors), preserve the full error and check the stage that failed before deciding whether another write is safe.\n\n## Upload sequence\n\n| Stage | What happens | What it does not confirm |\n| --- | --- | --- |\n| `POST .../files` | Bridge returns `mediaId` and `presignUrl` data. | The bytes have not been uploaded yet. |\n| Presigned `PUT` | Your server sends the exact bytes using the returned upload URL and headers. | No Message has been created by this step. |\n| `POST .../messages` | You attach the uploaded `mediaId` through `mediaIds`. | The API response alone does not prove provider delivery. |\n\nThe checksum is the **Base64-encoded SHA-256 digest of the complete file bytes**. It is not a hexadecimal digest and not a hash of the file name.\n\n## Complete server-side example\n\nUse Node.js with built-in `fetch`. Save this as `send-attachment.mjs`. Set `BRIDGE_API_KEY`, `BRIDGE_WORKSPACE_ID`, `BRIDGE_CONVERSATION_ID`, `ATTACHMENT_PATH`, and `ENABLE_EXTERNAL_BROADCAST` (`true` or `false`).\n\n```javascript\nimport { readFile } from \"node:fs/promises\";\nimport { basename } from \"node:path\";\nimport { createHash } from \"node:crypto\";\n\nconst BASE = \"https://api-connect-us.bridge.new\";\nfunction required(name) {\n  const value = process.env[name];\n  if (!value) throw new Error(`Set ${name} before running this example.`);\n  return value;\n}\nconst apiKey = required(\"BRIDGE_API_KEY\");\nconst workspaceId = required(\"BRIDGE_WORKSPACE_ID\");\nconst conversationId = required(\"BRIDGE_CONVERSATION_ID\");\nconst filePath = required(\"ATTACHMENT_PATH\");\nconst broadcast = required(\"ENABLE_EXTERNAL_BROADCAST\");\nif (![\"true\", \"false\"].includes(broadcast)) {\n  throw new Error(\"ENABLE_EXTERNAL_BROADCAST must be true or false.\");\n}\nif (!filePath.toLowerCase().endsWith(\".pdf\")) {\n  throw new Error(\"This example is configured for a PDF document.\");\n}\nconst workspacePath = `/bridge/api/v1/workspaces/${encodeURIComponent(workspaceId)}`;\nconst bytes = await readFile(filePath);\nconst checksumBase64 = createHash(\"sha256\").update(bytes).digest(\"base64\");\n\nasync function bridgePost(path, body) {\n  const response = await fetch(`${BASE}${workspacePath}${path}`, {\n    method: \"POST\",\n    headers: { \"x-api-key\": apiKey, \"Content-Type\": \"application/json\" },\n    body: JSON.stringify(body),\n  });\n  const result = await response.json().catch(() => null);\n  if (response.status !== 201) {\n    const code = result?.code ? ` (${result.code})` : \"\";\n    throw new Error(`Bridge ${path}: HTTP ${response.status}${code}`);\n  }\n  return result;\n}\n\nconst uploads = await bridgePost(\"/files\", [{\n  name: basename(filePath),\n  fileType: \"DOCUMENT\",\n  contentType: \"application/pdf\",\n  checksumBase64,\n}]);\nconst upload = uploads?.[0];\nif (!upload?.mediaId || !upload.presignUrl?.url) {\n  throw new Error(\"The upload response is missing its mediaId or URL.\");\n}\nconst method = upload.presignUrl.method ?? \"PUT\";\nif (method !== \"PUT\") throw new Error(\"Expected a presigned PUT upload.\");\nconst uploadHeaders = new Headers(upload.presignUrl.headers ?? {});\nconst returnedChecksum = uploadHeaders.get(\"x-amz-checksum-sha256\");\nif (returnedChecksum !== null && returnedChecksum !== checksumBase64) {\n  throw new Error(\"The returned checksum differs from the file checksum.\");\n}\nif (returnedChecksum === null) {\n  uploadHeaders.set(\"x-amz-checksum-sha256\", checksumBase64);\n}\n\nconst uploaded = await fetch(upload.presignUrl.url, {\n  method,\n  headers: uploadHeaders,\n  body: bytes,\n});\nif (!uploaded.ok) {\n  throw new Error(`File upload failed: HTTP ${uploaded.status}; no Message was created.`);\n}\n\nconst message = await bridgePost(\"/messages\", {\n  conversationId,\n  messageBody: \"Here is the document you requested.\",\n  enableExternalBroadcast: broadcast === \"true\",\n  mediaIds: [upload.mediaId],\n});\nif (!message?.messageId) throw new Error(\"The Message response has no messageId.\");\nconsole.log({ mediaId: upload.mediaId, messageId: message.messageId });\n```\n\n```bash\nnode send-attachment.mjs\n```\n\n## Keep the upload request intact\n\nUse `presignUrl.url` without reconstructing its host, path, or query string. Use the returned method and required headers. The contract specifies `PUT` as the method's default if it is omitted, and the Files API requires `x-amz-checksum-sha256` with the same checksum sent to Bridge.\n\nThe upload sends the same `bytes` that produced the checksum. Do not convert the PDF to text, wrap it in JSON, or send a different copy of the file after hashing it.\n\nFor multiple files, preserve the association between each returned `mediaId`, its presigned data, and its corresponding file. Complete every required upload before attaching those IDs to a Message.\n\nSome media need more preparation after the storage upload, such as copying the file, generating a preview, conversion or upload to the messaging provider. These stages can fail independently. A successful PUT or a 201 Message response does not establish that processing or provider delivery has completed. Keep the original file and the returned identifiers for correlation; check the result before repeating a Message request.\n\n## Handle a failed stage\n\n- **Bridge rejects the file request:** check the extension, `fileType`, content type, size, and checksum against the reference.\n- **Upload fails:** stop before creating the Message. Check the exact URL, method, headers, checksum, and bytes. Do not assume an upload lifetime or retry policy that the response does not provide.\n- **Message request fails or times out:** an uploaded file is not proof that a Message exists. Check the outcome before repeating the Message request.\n\n`STICKER` and `WEBSITE` appear in the `FileType` enum, but the published upload table does not specify their extension, MIME, and size rules. Confirm those rules before choosing either for an integration.\n\nSee the [Files API](https://docs.bridge.new/api-reference/files-api/create-presigned-uploads-for-files.md), [Message API](https://docs.bridge.new/api-reference/messages-api/create-a-new-message.md), and [Responses and errors](https://docs.bridge.new/guides/responses.md).\n"
    },
    {
      "title": "Choose and fill a WhatsApp template",
      "source": "https://docs.bridge.new/guides/send-template",
      "markdownUrl": "https://docs.bridge.new/guides/send-template.md",
      "description": "Discover a template by ID or name, supply its agnostic variable keys, and use its returned ID in a Conversation request.",
      "kind": "page",
      "contentMarkdown": "Use this recipe when you want a Conversation to open with a particular WhatsApp template. The detailed [template reference](https://docs.bridge.new/templates-and-parameters.md) explains validation rules and every documented template error.\n\n## Before you start\n\n- Know the template's **ID or name**. Both are accepted by `GET .../templates/{templateId}`.\n- Confirm the template is approved for the WhatsApp Business Account behind the intended line.\n- Have an existing test Contact ID, the authorized WhatsApp `providerChannelId`, and an internal Bridge User's email. See [Identifiers and concepts](https://docs.bridge.new/guides/identifiers.md).\n- Review the workspace's onboarding sequence: your override can replace the whole sequence or only its first template, depending on workspace configuration.\n\n**Warning**\n\n  The final request starts a Conversation and can send onboarding templates. Use an authorized test Contact and intended content.\n\n## 1. Discover the variables\n\n```http\nGET /bridge/api/v1/workspaces/{workspaceId}/templates/{templateId}\nx-api-key: YOUR_API_KEY\n```\n\nURL-encode the supplied ID or name. Read the response's `id`, `name`, `variables`, and `driftWarning`.\n\n| Variable | Action |\n| --- | --- |\n| `callerMustSupply: true` | Supply a non-blank string under that variable's `key`. |\n| `callerMustSupply: false`, not custom | Omit it to keep Bridge's per-Contact value. A supplied value replaces that value for every Contact. |\n| `isCustom: true` | Supply your value, or omit it or send it blank to use its `default`. |\n\nUse the agnostic `key`, never the display `label`, a Meta position, or an invented variable name. `driftWarning` is diagnostic and does not by itself block sending.\n\n## 2. Prepare your values\n\nCreate a server-side JSON file containing only keys returned by discovery. For a template that actually declares `order_number` and `delivery_date`, it could contain:\n\n```json\n{\n  \"order_number\": \"A-10492\",\n  \"delivery_date\": \"October 3\"\n}\n```\n\nThose two names are examples, not universal Bridge fields. Read your own template before using them. Keep values within the [WhatsApp value rules](https://docs.bridge.new/templates-and-parameters.md#whatsapp-value-rules); omit non-custom values you want Bridge to fill instead of sending blank strings.\n\n## 3. Discover, check, and create the Conversation\n\nUse Node.js with built-in `fetch`. Save this as `send-template.mjs` and set `BRIDGE_API_KEY`, `BRIDGE_WORKSPACE_ID`, `BRIDGE_TEMPLATE_ID_OR_NAME`, `TEMPLATE_VALUES_PATH`, `BRIDGE_CONTACT_ID`, `BRIDGE_PROVIDER_CHANNEL_ID`, and `BRIDGE_USER_EMAIL`.\n\n```javascript\nimport { readFile } from \"node:fs/promises\";\n\nconst BASE = \"https://api-connect-us.bridge.new\";\nfunction required(name) {\n  const value = process.env[name];\n  if (!value) throw new Error(`Set ${name} before running this example.`);\n  return value;\n}\nconst apiKey = required(\"BRIDGE_API_KEY\");\nconst workspaceId = required(\"BRIDGE_WORKSPACE_ID\");\nconst templateRef = required(\"BRIDGE_TEMPLATE_ID_OR_NAME\");\nconst contactId = required(\"BRIDGE_CONTACT_ID\");\nconst providerChannelId = required(\"BRIDGE_PROVIDER_CHANNEL_ID\");\nconst internalEmail = required(\"BRIDGE_USER_EMAIL\");\nconst parameters = JSON.parse(await readFile(required(\"TEMPLATE_VALUES_PATH\"), \"utf8\"));\nif (parameters === null || Array.isArray(parameters) || typeof parameters !== \"object\") {\n  throw new Error(\"The template values file must contain a JSON object.\");\n}\nif (Object.entries(parameters).some(([key, value]) =>\n  Array.from(key).length > 128 || typeof value !== \"string\" || Array.from(value).length > 1024\n)) {\n  throw new Error(\"Parameter keys and values must respect the published string limits.\");\n}\nif (Object.keys(parameters).length > 50) throw new Error(\"At most 50 parameters are allowed.\");\nconst workspacePath = `/bridge/api/v1/workspaces/${encodeURIComponent(workspaceId)}`;\n\nasync function request(method, path, body, expectedStatus) {\n  const response = await fetch(`${BASE}${workspacePath}${path}`, {\n    method,\n    headers: {\n      \"x-api-key\": apiKey,\n      ...(body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n    },\n    ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n  });\n  const result = await response.json().catch(() => null);\n  if (response.status !== expectedStatus) {\n    const code = result?.code ? ` (${result.code})` : \"\";\n    throw new Error(`${method} ${path}: HTTP ${response.status}${code}`);\n  }\n  return result;\n}\n\nconst template = await request(\n  \"GET\", `/templates/${encodeURIComponent(templateRef)}`, undefined, 200,\n);\nif (!template?.id) throw new Error(\"Template discovery returned no id.\");\nconst variables = template.variables ?? [];\nconst known = new Set(variables.map((variable) => variable.key));\nconst unknown = Object.keys(parameters).filter((key) => !known.has(key));\nconst missing = variables.filter((variable) =>\n  variable.callerMustSupply &&\n  (typeof parameters[variable.key] !== \"string\" || !parameters[variable.key].trim())\n);\nif (unknown.length || missing.length) {\n  throw new Error(`Check template keys: ${unknown.length} unknown, ${missing.length} missing.`);\n}\n\nconst conversation = await request(\"POST\", \"/conversations\", {\n  participants: {\n    externals: [{\n      identifier: contactId,\n      identifierType: \"ID\",\n      messageProviderType: \"WHATSAPP\",\n      providerChannelId,\n    }],\n    internals: [{ identifier: internalEmail, identifierType: \"EMAIL\" }],\n  },\n  onboardingTemplates: [{ templateId: template.id, parameters }],\n}, 201);\nif (!conversation?.id || conversation.workspaceId !== workspaceId) {\n  throw new Error(\"The Conversation response does not identify the intended workspace.\");\n}\nconsole.log({ templateId: template.id, conversationId: conversation.id });\n```\n\n```bash\nnode send-template.mjs\n```\n\nThe example uses the ID returned by discovery in `onboardingTemplates[].templateId`. The two outbound operations also accept a template ID or name, with the ID taking precedence when both are supplied. Their request shapes differ from `onboardingTemplates`; read the operation's contract before reusing this body elsewhere.\n\nThe local checks catch unknown and missing keys. Bridge still performs its own validation, including approval for the selected line and the final WhatsApp value rules. Do not treat local validation as approval or a delivery guarantee.\n\n## Recover from a rejected request\n\n| Error | What to do |\n| --- | --- |\n| Discovery `BRIDGE_TEMPLATE_0001` | Check the ID/name and workspace. This response does not reveal whether another workspace owns that identifier. |\n| `BRIDGE_CONVERSATION_0604` | Supply the non-blank values listed in `extra.missingVariables`. |\n| `BRIDGE_CONVERSATION_0605` | Remove keys listed in `extra.unknownKeys`; use discovery's agnostic keys. |\n| `BRIDGE_CONVERSATION_0607` | Correct the values listed in `extra.invalidVariables` according to their `reason`. |\n| `BRIDGE_CONVERSATION_0608` | Ask a workspace administrator to change the configured onboarding sequence. Changing your template values does not fix this configuration error. |\n\nRead the [complete template error reference](https://docs.bridge.new/templates-and-parameters.md#errors) for approval, ownership, unsupported parameters, and malformed requests. Do not retry a timed-out Conversation request automatically: determine its outcome first.\n"
    },
    {
      "title": "Start an Agent outbound conversation",
      "source": "https://docs.bridge.new/guides/agent-outbound",
      "markdownUrl": "https://docs.bridge.new/guides/agent-outbound.md",
      "description": "Choose an Agent, send an opening template, understand forceSend and recovery, and request a LeadScout handoff.",
      "kind": "page",
      "contentMarkdown": "Use outbound when an existing Contact should receive an approved WhatsApp template and continue with an AI Agent. Use [create a conversation](https://docs.bridge.new/guides/first-conversation.md) for the general conversation workflow.\n\n## Choose the operation\n\n| Your goal | Operation | How the Agent is selected |\n| --- | --- | --- |\n| Start with a specific LeadScout or Service Agent | `POST /bridge/api/v1/workspaces/{workspaceId}/agents/{agentId}/outbound` | You supply the Agent ID from `GET /agents`. |\n| Start with a LeadScout selected through the workspace's eligible lines | `POST /bridge/api/v1/workspaces/{workspaceId}/leadscout/outbound` | Bridge resolves the prospecting Agent for the line. |\n\nBoth use the REST API host `https://api-connect-us.bridge.new` and a server-side `x-api-key`. They have separate request schemas. Read the [Agents outbound reference](https://docs.bridge.new/api-reference/agents-api/start-an-outbound-conversation-with-a-contact-as-a-given-agent.md) or [LeadScout outbound reference](https://docs.bridge.new/api-reference/leadscout-api/initiate-a-leadscout-outbound-conversation-with-a-known-lead.md) for the operation you use.\n\n## Before you send\n\n1. [Look up the Contact](https://docs.bridge.new/api-reference/contacts-api/retrieve-a-contact-by-an-identifier.md), or [create it](https://docs.bridge.new/api-reference/contacts-api/create-a-new-contact.md) if missing. Outbound requires a Contact that already exists in the workspace; you do not need to create a separate Conversation or Message first.\n2. [List Agents](https://docs.bridge.new/api-reference/agents-api/list-the-agents-of-a-workspace.md). Choose an available Agent and read its `lines[].providerChannelId`. An AI Agent ID differs from a human Bridge User ID.\n3. Choose an approved template. [Discover its variables](https://docs.bridge.new/guides/send-template.md#1-discover-the-variables), supply caller-required values, and check that it is approved for the destination line. Use only the discovery step as this prerequisite; the separate Conversation-creation step in that recipe is not needed for outbound.\n4. Confirm that your business has the required opt-in. If you send `consent.status: \"GRANTED\"`, include `capturedAt`; this records your assertion of consent.\n\nFor Agents outbound, `contact.providerChannelId` must belong to the chosen Agent when supplied. It is a preference: another eligible Agent line can be selected. For LeadScout outbound, an explicit line is respected. [Identifiers and their sources](https://docs.bridge.new/guides/identifiers.md) explains which values to use.\n\n## Send a minimal Agent request\n\nReplace every placeholder. This example assumes the chosen template has no unresolved caller-required variables. Set `forceSend` deliberately; the example keeps the default behavior explicit.\n\n```bash\ncurl --fail-with-body --request POST \\\n  \"https://api-connect-us.bridge.new/bridge/api/v1/workspaces/$WORKSPACE_ID/agents/$AGENT_ID/outbound\" \\\n  --header \"x-api-key: $BRIDGE_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"contact\": {\n      \"identifier\": \"f0000001-0000-4000-8000-000000000001\",\n      \"identifierType\": \"ID\",\n      \"messageProviderType\": \"WHATSAPP\"\n    },\n    \"template\": {\n      \"id\": \"f0000040-0000-4000-8000-000000000040\"\n    },\n    \"forceSend\": false\n  }'\n```\n\nA successful request returns `201`. Store `conversation.id` and interpret these response fields:\n\n| Field | Meaning |\n| --- | --- |\n| `alreadyInitiated: true` | An existing active conversation was reused. The successful request still adds its new template and follow-ups. |\n| `recovered: true` | A released, recoverable conversation was recovered, retaining its ID. Its previous outbound context and `agentContext` are replaced. |\n| `additionalMessagesPersisted` | The number of follow-up messages stored. This is not a delivery receipt. |\n| `templateSent` | A summary of the opening template stored for sending. Do not interpret the field name as proof of recipient delivery. |\n\nNew creation has neither reuse nor recovery set to true. Recovery can happen with `forceSend: false`. Additional messages are persisted in request order; do not rely on a promise that they wait for the Contact to reply. The shared outbound flow persists text follow-ups and does not attach their `mediaIds`. Use the documented `template.media` option for media on the opening template, or the [Message attachment workflow](https://docs.bridge.new/guides/send-attachments.md) for a separate message.\n\n## What forceSend does\n\nAvailability is evaluated for **this Contact's conversation on each line**. An occupied line here does not mean the corporate WhatsApp line is globally reserved for one Contact.\n\nThe following rules remain subject to Agent availability, template validation, consent validation and session checks.\n\nOn a narrow screen, scroll horizontally to compare all four choices. Keyboard users can focus this comparison and use the arrow keys.\n\n| Situation | LeadScout: false | LeadScout: true | Agents: false | Agents: true |\n| --- | --- | --- | --- | --- |\n| Selected line is free | Create | Create | Create | Create |\n| Selected line holds a released conversation marked recoverable | Recover the same ID | Recover the same ID | Recover the same ID | Recover the same ID |\n| Automatic selection finds only occupied, active conversations | `409 …0008` | Reuse the first eligible occupied conversation | `409 …0008` | Prefer one controlled by the requested Agent, then apply the controller check below |\n| Explicit line is occupied | `409 …0007` | Reuse that conversation | Try other free or recoverable Agent lines; otherwise `409 …0008` | Try other free or recoverable Agent lines; otherwise prefer an occupied line the Agent controls |\n| Selected occupied conversation is controlled by a human or another Agent | No active reuse | Legacy route permits reuse, subject to other checks; this does not itself promise a controller transfer | No active reuse | `409 BRIDGE_AI_AGENT_OUTBOUND_0006`; this rejection writes nothing |\n| Only blocked or released, nonrecoverable conversations are available | `409 …0008` | `409 …0008` | `409 …0008` | `409 …0008` |\n\nIn this table, `…0007` is `BRIDGE_AI_LEADSCOUT_0007` and `…0008` is `BRIDGE_AI_LEADSCOUT_0008`. A released conversation is recoverable only when its stored recovery flag permits it; `forceSend` does not make it recoverable.\n\nFor both routes, a LeadScout session that is still open and unexpired blocks a new outbound request with `409 BRIDGE_AI_LEADSCOUT_0017`. Failure to finalize an expired session returns `409 BRIDGE_AI_LEADSCOUT_0018`. A Service Agent can continue its open, unexpired service session; an expired session is closed before a new one opens.\n\n**Warning**\n\n  `forceSend` is not an idempotency key. Repeating a successful request can add another template and more messages. It does not bypass blocked conversations, session checks, approval rules or validation. A timeout leaves the outcome uncertain: inspect the conversation before deciding whether to submit again.\n\n## Handle conflicts and partial work\n\n| Result | Next step |\n| --- | --- |\n| `BRIDGE_AI_LEADSCOUT_0007` / `0008` | Check the Contact's existing conversation and eligible lines. Decide whether reuse is appropriate before enabling `forceSend`. |\n| `BRIDGE_AI_AGENT_OUTBOUND_0006` | Inspect `extra.conversationId` and its controller. Coordinate ownership before deliberately releasing it. Do not automatically interrupt a human conversation. |\n| `BRIDGE_AI_LEADSCOUT_0017` / `0018` | Resolve the existing session or contact support. Switching `forceSend` to true does not fix the session. |\n| `BRIDGE_AI_LEADSCOUT_0019` / `0020` / `0021` | Correct missing, conflicting or unknown template variables using discovery. |\n| `400 BRIDGE_AI_LEADSCOUT_0016` for an unknown template `mediaId` on Agents outbound | The conversation may already have been created or recovered before this error. Inspect it before retrying. |\n\n[Release conversation](https://docs.bridge.new/api-reference/conversations-api/release-conversation.md) is a separate operation with business effects. A `204` has no response body. Release only when ending that conversation is your intended action.\n\n## Request a LeadScout handoff to a human\n\nFor a LeadScout conversation, resolve the human's [Bridge User ID or email](https://docs.bridge.new/guides/identifiers.md), then call the [manual handoff endpoint](https://docs.bridge.new/api-reference/leadscout-api/request-a-manual-handoff-of-a-leadscout-conversation-to-internal-users.md). This operation is specific to LeadScout; do not assume it applies to a Service Agent.\n\n```bash\ncurl --fail-with-body --request POST \\\n  \"https://api-connect-us.bridge.new/bridge/api/v1/workspaces/$WORKSPACE_ID/leadscout/conversations/$CONVERSATION_ID/handoff\" \\\n  --header \"x-api-key: $BRIDGE_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"assignees\": [\n      {\"identifier\": \"f0000002-0000-4000-8000-000000000002\", \"identifierType\": \"ID\"}\n    ]\n  }'\n```\n\nA `202` acknowledges the request. Save its `trackingId` and `conversationId`; acceptance does not prove that assignment has completed or that a human has read or answered the conversation. The reference does not publish a handoff tracking endpoint, so do not construct one from `trackingId`.\n\nContinue with [error handling](https://docs.bridge.new/error-codes.md) and [webhook receiving](https://docs.bridge.new/webhooks/receive-events.md) to connect this workflow to your system.\n"
    },
    {
      "title": "Read every page of results",
      "source": "https://docs.bridge.new/guides/pagination",
      "markdownUrl": "https://docs.bridge.new/guides/pagination.md",
      "description": "Follow opaque cursors across Contacts, Conversations, Bridge Users, and Agents without stopping on a short page.",
      "kind": "page",
      "contentMarkdown": "List endpoints return a collection and an optional `cursor`. Send that cursor unchanged to read the next page. Keep following cursors until it is null or absent.\n\n## Choose the collection\n\nAll four endpoints below accept `limit` from **1 to 100**, with a default of **20**.\n\n| Resource | Path after the workspace | Collection field |\n| --- | --- | --- |\n| Contacts | `/contacts` | `contacts` |\n| Conversations | `/conversations` | `conversations` |\n| Bridge Users | `/users` | `users` |\n| Agents | `/agents` | `agents` |\n\nUse [contact lookup](https://docs.bridge.new/api-reference/contacts-api/retrieve-a-contact-by-an-identifier.md) or [Bridge User lookup](https://docs.bridge.new/api-reference/internal-users-api/retrieve-an-internal-user-by-an-identifier.md) when you need one known person. A full list sweep is a different task.\n\n## Server-side example\n\nThis example reads Contacts. Save it as `list-contacts.mjs` and set `BRIDGE_API_KEY` and `BRIDGE_WORKSPACE_ID` on your server.\n\nThe local `maxPages` guard prevents an accidentally unbounded run; it is not a Bridge API limit. The repeated-cursor guard stops and reports a problem rather than silently returning an incomplete list.\n\n```javascript\nconst BASE = \"https://api-connect-us.bridge.new\";\nconst apiKey = process.env.BRIDGE_API_KEY;\nconst workspaceId = process.env.BRIDGE_WORKSPACE_ID;\nif (!apiKey || !workspaceId) {\n  throw new Error(\"Set BRIDGE_API_KEY and BRIDGE_WORKSPACE_ID.\");\n}\n\nasync function* pages(resource, collection, filters = {}) {\n  let cursor;\n  const seen = new Set();\n  const maxPages = 10000;\n\n  for (let pageNumber = 0; pageNumber < maxPages; pageNumber += 1) {\n    const url = new URL(\n      `/bridge/api/v1/workspaces/${encodeURIComponent(workspaceId)}/${resource}`,\n      BASE,\n    );\n    for (const [key, value] of Object.entries(filters)) {\n      url.searchParams.set(key, String(value));\n    }\n    url.searchParams.set(\"limit\", \"100\");\n    if (cursor !== undefined) url.searchParams.set(\"cursor\", cursor);\n\n    const response = await fetch(url, { headers: { \"x-api-key\": apiKey } });\n    const body = await response.json().catch(() => null);\n    if (response.status !== 200) {\n      const code = body?.code ? ` (${body.code})` : \"\";\n      throw new Error(`List ${resource}: HTTP ${response.status}${code}`);\n    }\n    if (!Array.isArray(body?.[collection])) {\n      throw new Error(`Expected the ${collection} collection.`);\n    }\n\n    yield body[collection];\n    if (body.cursor === null || body.cursor === undefined) return;\n    if (typeof body.cursor !== \"string\" || seen.has(body.cursor)) {\n      throw new Error(\"Invalid or repeated cursor; stopped without claiming completion.\");\n    }\n    seen.add(body.cursor);\n    cursor = body.cursor;\n  }\n  throw new Error(\"Local page guard reached; the sweep is incomplete.\");\n}\n\nlet count = 0;\nfor await (const contacts of pages(\"contacts\", \"contacts\")) {\n  // Process or store this page before continuing. Avoid logging personal data.\n  count += contacts.length;\n}\nconsole.log({ contactsRead: count });\n```\n\n```bash\nnode list-contacts.mjs\n```\n\nUse `pages(\"conversations\", \"conversations\", { status: \"ACTIVE\" })`, `pages(\"users\", \"users\")`, or `pages(\"agents\", \"agents\")` with the corresponding contract. Keep the same filters throughout one traversal.\n\n## Do not stop on a short page\n\nA page containing fewer records than `limit` can still have another page. The Agents endpoint explicitly documents this behavior. The returned cursor is the continuation signal; collection length is not.\n\nTreat the cursor as opaque. Do not decode it, increment it, or build one yourself. `URL.searchParams` in the example preserves its value through URL encoding.\n\n## Filters and rate limits\n\n- The Contact list's `ownerEmail` and `search` filters are deprecated. For a known email or phone, use contact lookup. To filter an inventory by `ownerId`, read that field from the returned rows in your own code.\n- The Bridge User list's `email` and `status` filters are deprecated. Use lookup for a known email, or inspect each returned User's `status`.\n- Each page request consumes the workspace's shared request budget. Follow [Rate limits](https://docs.bridge.new/rate-limits.md), including `Retry-After` on `429`. This minimal example stops on non-200 responses instead of retrying automatically.\n\nThe published contracts do not promise a transactional snapshot of a list while records are changing. Do not describe this traversal as a guaranteed consistent export or infer an undocumented cursor lifetime.\n\n## References\n\n- [List Contacts](https://docs.bridge.new/api-reference/contacts-api/list-contacts.md)\n- [List Conversations](https://docs.bridge.new/api-reference/conversations-api/retrieve-conversations-with-flexible-filtering-options.md)\n- [List Bridge Users](https://docs.bridge.new/api-reference/internal-users-api/list-internal-users.md)\n- [List Agents](https://docs.bridge.new/api-reference/agents-api/list-the-agents-of-a-workspace.md)\n- [Responses and errors](https://docs.bridge.new/guides/responses.md)\n"
    },
    {
      "title": "Responses, errors, and retries",
      "source": "https://docs.bridge.new/guides/responses",
      "markdownUrl": "https://docs.bridge.new/guides/responses.md",
      "description": "Interpret creation, acceptance, empty responses, validation errors, and retry signals without confusing the REST and Custom Connections contracts.",
      "kind": "page",
      "contentMarkdown": "## Read the result for this operation\n\nThe HTTP status and response body describe the operation you called. They do not all mean that a person received a message.\n\n| Operation | Published success | What to do with it |\n| --- | --- | --- |\n| Create a message | `201`, with `messageId` | Retain the message ID. The published response does not include a provider delivery receipt; do not label it delivered from this response alone. |\n| Release a conversation | `204`, with no response body | Treat it as success and do not call a JSON parser on the empty body. |\n| Request LeadScout handoff | `202`, with `trackingId`, `conversationId`, `status`, and `assignees` | Retain the correlation ID. Assignment is asynchronous; assignees echo the request and do not prove that downstream resolution or assignment completed. |\n| Submit a Custom Connections lead | `202`, with `requestId` | Retain the request ID. Show that the lead was accepted; the opening WhatsApp message is sent later by a separate service. |\n\nThe reference for each operation remains authoritative: [messages](https://docs.bridge.new/api-reference/messages-api/create-a-new-message.md), [release](https://docs.bridge.new/api-reference/conversations-api/release-conversation.md), [handoff](https://docs.bridge.new/api-reference/leadscout-api/request-a-manual-handoff-of-a-leadscout-conversation-to-internal-users.md), and [Custom Connections](https://docs.bridge.new/api-reference/campaign-leads/register-a-campaign-lead.md).\n\n**Note**\n\n  A correlation ID is not a status URL. The published contract does not define a polling endpoint for `trackingId` or `requestId`. Keep it for correlation and support; do not construct a new endpoint from it.\n\n## Parse the error shape the endpoint publishes\n\nREST Integrations and Custom Connections have separate error contracts.\n\n### REST Integrations\n\nMany responses use `Problem`: `title` and `status` are required; `code`, `detail`, `errors`, `traceId`, `instance`, and `extra` are optional. When a `code` is present, use it to select a documented error handler rather than matching the human-readable `title`. Keep `traceId` when provided for a support request.\n\nSome published validation responses declare `HTTPValidationError`, whose `detail` is an array of entries with `loc`, `msg`, and `type`. The shared API error handler reviewed for this documentation returns request validation details as `Problem.errors[]` instead. The published response definitions are preserved; confirmation against the deployed API is still pending. See the [validation compatibility note](https://docs.bridge.new/error-codes.md#parse-validation-errors-defensively).\n\nAccept JSON errors with either `application/json` or `application/problem+json`. Inspect a `code` when present, then an `errors` array or a `detail` array for field errors. A string `detail` is an explanation, not an array. A `422` status alone does not identify one universal body shape. Keep a fallback for an error without `code`, and use the endpoint reference together with [Error codes](https://docs.bridge.new/error-codes.md) to choose the action.\n\n### Custom Connections\n\nIts error body has required `type`, `title`, and `status`, and may include `detail`. Here **`type` is the machine-readable error identifier**. It is not the REST `code` field. The [Custom Connections error reference](https://docs.bridge.new/lead-capture/errors.md) lists these responses.\n\nError responses do not carry the submission `requestId`; only a successful `202` does. For an error support request, retain the page URL and approximate time as described in that reference.\n\n## Make retry decisions by operation and cause\n\n| Result | Next action |\n| --- | --- |\n| REST `429` | Follow `Retry-After` in seconds. The rate-limit guide states that the rejected request was not processed. The workspace budget is shared across API Keys. |\n| REST `401` or `403` | Correct the API Key, permissions, or workspace mismatch. A timed retry does not fix those causes. |\n| Validation or configuration error | Correct the documented cause first. Do not repeatedly send the same invalid request. |\n| Custom Connections `422` | The documented cause is CAPTCHA provider unavailability. Its error guide says retrying is safe; mint a fresh CAPTCHA token for the new attempt. |\n| Custom Connections CAPTCHA rejection | Check that the token is present, fresh, and minted for this attempt. The same error also covers a low score, so a new attempt alone does not prove the original cause. |\n| Timeout, connection loss, or an unclassified `5xx` during a mutation | Do not assume nothing happened. Inspect the operation's documented effects and any identifiers already obtained before deciding whether to repeat it. Do not assume a general idempotency guarantee. |\n\nSee [Rate limits](https://docs.bridge.new/rate-limits.md) for pacing and [Custom Connections errors](https://docs.bridge.new/lead-capture/errors.md) for its distinct retry rules.\n\n## Some failures can follow a write\n\nThe [Agent outbound reference](https://docs.bridge.new/api-reference/agents-api/start-an-outbound-conversation-with-a-contact-as-a-given-agent.md) documents that an unknown `template.media.mediaId` can produce `BRIDGE_AI_LEADSCOUT_0016` after the conversation was created or recovered. A retry may therefore produce a different result.\n\nThis is why a failure status alone is not a universal rollback guarantee. Follow the effects and recovery instructions for the specific operation. If those instructions are unclear or conflict, identify the uncertainty and contact [Bridge support](mailto:support@sainapsis.com) before retrying an action with external effects.\n"
    },
    {
      "title": "WhatsApp template parameters",
      "source": "https://docs.bridge.new/templates-and-parameters",
      "markdownUrl": "https://docs.bridge.new/templates-and-parameters.md",
      "description": "Open a conversation with your own WhatsApp templates, fill their variables, and handle every error the request can return.",
      "kind": "page",
      "contentMarkdown": "## Overview\n\nWhen you create a conversation, `onboardingTemplates` lets you choose the WhatsApp templates it opens with, in order. A template can contain variables — an order number, a delivery date, the Contact's first name. Some of them Bridge fills on its own for each Contact; others only you can provide, and you send them in the entry's `parameters`.\n\nThe flow has three steps:\n\n1. Look up the template's variables.\n2. Decide which values to send.\n3. Send them with `POST .../conversations`.\n\n**Note**\n\n  Every check described on this page runs before anything is created. If the request is rejected, **no conversation is created and no message is sent**, so you can correct it and send it again.\n\n---\n\n## 1. Find the template's variables\n\n```http\nGET /bridge/api/v1/workspaces/{workspaceId}/templates/{templateId}\nx-api-key: YOUR_API_KEY\n```\n\n`templateId` accepts either the template's `id` or its `name`. The response lists its variables:\n\n```json\n{\n  \"id\": \"f0000040-0000-4000-8000-000000000040\",\n  \"name\": \"order_shipped_promo\",\n  \"language\": \"es\",\n  \"driftWarning\": { \"hasDrift\": false, \"stale\": [], \"missingLabels\": [] },\n  \"variables\": [\n    { \"key\": \"first_name\", \"label\": \"Contact first name\", \"callerMustSupply\": false, \"isCustom\": false, \"default\": null },\n    { \"key\": \"order_number\", \"label\": \"Order number\", \"callerMustSupply\": true, \"isCustom\": false, \"default\": null },\n    { \"key\": \"delivery_date\", \"label\": \"Delivery date\", \"callerMustSupply\": true, \"isCustom\": false, \"default\": null },\n    { \"key\": \"tracking_code\", \"label\": \"Tracking code\", \"callerMustSupply\": false, \"isCustom\": true, \"default\": \"pending\" }\n  ]\n}\n```\n\n`driftWarning` is diagnostic only: it reports differences between the declared variables and Meta's template snapshot and never blocks a send. The table below describes each item in `variables`.\n\n| Field | Meaning |\n| ----- | ------- |\n| `key` | The name to use in `parameters` |\n| `label` | A human-readable description of the variable |\n| `callerMustSupply` | `true` when you must supply a non-blank value because Bridge has no source for it; `false` otherwise |\n| `isCustom` | Whether this is a custom variable: a value only you know, with a default text |\n| `default` | For a custom variable, the text sent when you send no value for it; `null` otherwise |\n\nA template that is not in your workspace returns `404` with `BRIDGE_TEMPLATE_0001`.\n\n---\n\n## 2. Decide which values to send\n\n| Variable | What it means | What to do |\n| -------- | ------------- | ---------- |\n| `callerMustSupply: true` | Bridge has no source for this value | **Required.** Send a non-blank value for its `key` |\n| `callerMustSupply: false`, `isCustom: false` | Bridge fills it for each Contact, such as the Contact's first name | **Optional.** Omit it to keep Bridge's value |\n| `isCustom: true` | A value only you know, with a `default` | **Optional.** Send your value, or omit it (or send it blank) and its `default` is sent |\n\nA value you send for a `callerMustSupply: false` variable replaces Bridge's value and is used **for every Contact** in the conversation.\n\n**Warning**\n\n  For a variable that is not custom, keep Bridge's value by **omitting the key**. A blank value is not treated as \"use the default\": it is sent as you wrote it. A required variable with a blank or whitespace-only value is rejected as missing. Only custom variables (`isCustom: true`) treat a blank value as not sent and fall back to their `default`.\n\n---\n\n## 3. Send the parameters\n\nEach entry in `onboardingTemplates` takes the template's `id` (as returned in step 1) and its `parameters`, keyed by variable `key`. Position in the list sets the sending order.\n\n```http\nPOST /bridge/api/v1/workspaces/{workspaceId}/conversations\nx-api-key: YOUR_API_KEY\nContent-Type: application/json\n```\n\n```json\n{\n  \"participants\": {\n    \"externals\": [\n      {\n        \"identifier\": \"+13802098869\",\n        \"identifierType\": \"PHONE\",\n        \"messageProviderType\": \"WHATSAPP\",\n        \"brandId\": \"YOUR_BRAND_ID\"\n      }\n    ]\n  },\n  \"onboardingTemplates\": [\n    {\n      \"templateId\": \"f0000040-0000-4000-8000-000000000040\",\n      \"parameters\": {\n        \"order_number\": \"A-10492\",\n        \"delivery_date\": \"3 de octubre\"\n      }\n    }\n  ]\n}\n```\n\nIn this example `first_name` and `tracking_code` are omitted, so each Contact receives their own first name and `tracking_code` is sent as its default, `pending`.\n\nLimits per template: at most **50** parameters, keys up to **128** characters, values up to **1,024** characters. Requests beyond these limits are rejected as malformed (see [Errors](https://docs.bridge.new/templates-and-parameters.md#errors)).\n\nDepending on how the workspace is set up, your templates replace its whole onboarding sequence or only its first template. In the second case, the rest of the configured sequence is still sent after yours.\n\n---\n\n## WhatsApp value rules\n\nWhatsApp rejects some values when the message is sent. Bridge checks them up front so the request fails while you can still fix it:\n\n| Rule | Limit |\n| ---- | ----- |\n| Line breaks and tabs | Not allowed — no `\\n`, `\\r` or `\\t` |\n| Consecutive spaces | At most 4 in a row |\n| Header | The header text with your value in place fits in **60** characters |\n| URL button | The button's link with your value in place fits in **2,000** characters |\n| Body | The body text plus all its values fits in **1,024** characters |\n\n**Warning**\n\n  The body limit includes the values Bridge fills in for each Contact, such as their first name. Bridge can only check the values it can see when you make the request, so leave room for them.\n\nBridge never trims or rewrites your values: what you send is what the Contact receives.\n\n---\n\n## Errors\n\nAll errors use the `application/problem+json` format described in [Error codes](https://docs.bridge.new/error-codes.md). Branch on `code`, never on `title`.\n\n| Code | Status | When | `extra` |\n| ---- | ------ | ---- | ------- |\n| `BRIDGE_CONVERSATION_0601` | 404 | The template does not exist | `templateId`, `workspaceId` |\n| `BRIDGE_CONVERSATION_0602` | 400 | The template belongs to another workspace | `templateId`, `workspaceId` |\n| `BRIDGE_CONVERSATION_0603` | 400 | The template is not approved for the line a participant will be reached on | `templateId`, `workspaceId`, `userIds` (the Contacts whose line it is not approved for, when known) |\n| `BRIDGE_CONVERSATION_0604` | 422 | A required variable is missing or blank | `templateId`, `missingVariables: [{key, label}]` |\n| `BRIDGE_CONVERSATION_0605` | 422 | A key in `parameters` is not one of the template's variables | `templateId`, `unknownKeys: [key]` |\n| `BRIDGE_CONVERSATION_0606` | 422 | `parameters` was sent for a template that is not a WhatsApp template | `templateId` |\n| `BRIDGE_CONVERSATION_0607` | 422 | A value breaks a [WhatsApp rule](https://docs.bridge.new/templates-and-parameters.md#whatsapp-value-rules) | `templateId`, `invalidVariables: [{key, label, reason}]` |\n| `BRIDGE_CONVERSATION_0608` | 422 | A template the workspace sends after yours needs values only the caller can provide | `templateId`, `origin: \"WORKSPACE_CONFIGURATION\"` |\n\n### Missing values — `0604`\n\n```json\n{\n  \"title\": \"A template named in the onboarding override is missing values only the caller can provide\",\n  \"status\": 422,\n  \"detail\": \"A template named in the onboarding override is missing values only the caller can provide\",\n  \"code\": \"BRIDGE_CONVERSATION_0604\",\n  \"extra\": {\n    \"templateId\": \"f0000040-0000-4000-8000-000000000040\",\n    \"missingVariables\": [\n      { \"key\": \"delivery_date\", \"label\": \"Delivery date\" }\n    ]\n  }\n}\n```\n\n### Invalid values — `0607`\n\n```json\n{\n  \"title\": \"Parameter values for an onboarding template break a WhatsApp rule and would be rejected when sent\",\n  \"status\": 422,\n  \"detail\": \"Parameter values for an onboarding template break a WhatsApp rule and would be rejected when sent\",\n  \"code\": \"BRIDGE_CONVERSATION_0607\",\n  \"extra\": {\n    \"templateId\": \"f0000040-0000-4000-8000-000000000040\",\n    \"invalidVariables\": [\n      { \"key\": \"order_number\", \"label\": \"Order number\", \"reason\": \"NEWLINE\" }\n    ]\n  }\n}\n```\n\n`reason` is the first rule the value breaks:\n\n| `reason` | Meaning |\n| -------- | ------- |\n| `NEWLINE` | The value contains `\\n` or `\\r` |\n| `TAB` | The value contains `\\t` |\n| `CONSECUTIVE_SPACES` | The value has more than 4 spaces in a row |\n| `TOO_LONG` | The header or URL button exceeds its limit with the value in place |\n| `BODY_TOO_LONG` | The body exceeds 1,024 characters; every body value you sent is listed |\n\n### `0608` is not fixed by changing the request\n\n`BRIDGE_CONVERSATION_0608` only occurs in workspaces where your templates replace just the first template of the onboarding sequence. It means a template the workspace sends after yours needs values that only the caller can provide, and the API cannot supply them. Ask a workspace admin to change the onboarding sequence.\n\n### Malformed requests\n\nA request that does not match the schema — more than 50 parameters, a key over 128 characters, a value over 1,024, or an unknown field — is rejected with `422` **without a `code`**. It carries an `errors` list pointing at each problem instead:\n\n```json\n{\n  \"title\": \"Unprocessable content\",\n  \"status\": 422,\n  \"errors\": [\n    {\n      \"loc\": [\"body\", \"onboardingTemplates\", 0, \"parameters\", \"order_number\"],\n      \"msg\": \"String should have at most 1024 characters\",\n      \"type\": \"string_too_long\"\n    }\n  ]\n}\n```\n\n### Evaluation order\n\nChecks run in this order, and **the first error wins**:\n\n1. Rate limit — `429`\n2. Request shape — `422` without `code`\n3. Participants, brand and line — `400` / `409`, for example `BRIDGE_BRAND_0101` or `BRIDGE_PROVIDER_CHANNEL_0001`\n4. Each template, in list order: `0601` / `0602` → `0603` → `0606` → `0605` → `0604` → `0607`\n5. `0608`, only in workspaces where your templates replace just the first template\n\nSo fixing one error can reveal the next. Values for a later template are not checked until every earlier template passes.\n\n---\n\n## Related Topics\n\n- [Create a conversation](https://docs.bridge.new/api-reference/conversations-api/creates-a-new-conversation-with-flexible-user-identification.md) — Full request and response reference\n- [Error Codes](https://docs.bridge.new/error-codes.md) — Every code, including brand and line errors\n- [Rate limits](https://docs.bridge.new/rate-limits.md) — Handling `429`\n"
    },
    {
      "title": "Rate limits",
      "source": "https://docs.bridge.new/rate-limits",
      "markdownUrl": "https://docs.bridge.new/rate-limits.md",
      "description": "How many requests the Bridge API accepts, the headers that tell you where you stand, and how to handle a 429 correctly.",
      "kind": "page",
      "contentMarkdown": "## Overview\n\nThe Bridge API accepts **50 requests every 5 seconds**. The limit applies to every endpoint, is enabled by default, and needs no configuration.\n\n**Note**\n\n  The budget belongs to the **workspace**, not to the API Key. If you issue several keys for the same workspace — one per application, as recommended — they all draw from the same 50 requests.\n\n---\n\n## Knowing where you stand\n\nEvery response to an authenticated request carries two headers, so you never have to guess how much budget is left:\n\n```http\nRateLimit-Policy: \"default\";q=50;w=5\nRateLimit: \"default\";r=43;t=3\n```\n\n| Field | Meaning |\n| ----- | ------- |\n| `q` | Requests allowed per window |\n| `w` | Window length, in seconds |\n| `r` | Requests **remaining** in the current window |\n| `t` | Seconds until the window resets and `r` returns to `q` |\n\nA client that watches `r` and pauses as it approaches zero is never rejected at all.\n\n**Note**\n\n  A request rejected **before** authentication — a missing or invalid API Key — carries neither header. See [Authentication](https://docs.bridge.new/credentials.md) for those responses.\n\n---\n\n## When you go over\n\nRequests beyond the budget are answered with `429 Too Many Requests`. Nothing is processed, so the request is safe to repeat.\n\n```http\nHTTP/1.1 429 Too Many Requests\nContent-Type: application/problem+json\nRetry-After: 3\nRateLimit-Policy: \"default\";q=50;w=5\nRateLimit: \"default\";r=0;t=3\n```\n\n```json\n{\n  \"title\": \"Too Many Requests\",\n  \"status\": 429,\n  \"detail\": \"The workspace has exhausted its request budget for the current window\",\n  \"code\": \"BRIDGE_RATE_LIMIT_0001\"\n}\n```\n\n`Retry-After` is expressed in **seconds from now**, not as a date. Wait that long and the next request is served.\n\n**Warning**\n\n  **A `429` is not an authentication problem.** `401` and `403` mean the API Key is missing, invalid, expired or not authorized, and **retrying them on a schedule will never succeed** — it only burns your budget. Only `429` should be retried automatically, and only after the delay it gives you.\n\n---\n\n## What consumes budget\n\nEvery HTTP request counts as one, including requests that end in a validation error or a `404`. What matters is how many calls your client makes, not how much data comes back.\n\nThat distinction shows up when you read:\n\n- **A single resource by id** — `GET .../contacts/{contactId}` — costs **one request**, whatever the size of the response.\n- **A list** — `GET .../contacts` — costs **one request per page**. Reading a list in full costs as many requests as it has pages.\n\nSo page size decides the cost of a full sweep. Walking 500 contacts costs 5 requests at 100 per page, and 25 requests at 20 per page — same data, five times the budget.\n\n**Note**\n\n  Prefer the largest page size an endpoint accepts when you are reading a list in full, and read a single resource by its id when you already know which one you need.\n\n---\n\n## Backing off in practice\n\nA client that paces itself from `RateLimit` and honours `Retry-After` on the rare miss needs no other logic:\n\n```python\nimport time\n\nimport requests\n\nBASE = \"https://api-connect-us.bridge.new\"\nHEADERS = {\"x-api-key\": \"YOUR_API_KEY\"}\n\n\ndef rate_limit_state(response):\n    header = response.headers.get(\"RateLimit\", \"\")\n    fields = dict(part.split(\"=\", 1) for part in header.split(\";\") if \"=\" in part)\n    return int(fields.get(\"r\", 1)), int(fields.get(\"t\", 1))\n\n\ndef get(path):\n    while True:\n        response = requests.get(f\"{BASE}{path}\", headers=HEADERS)\n\n        if response.status_code == 429:\n            time.sleep(int(response.headers[\"Retry-After\"]))\n            continue\n\n        remaining, reset_in = rate_limit_state(response)\n        if remaining == 0:\n            time.sleep(reset_in)\n\n        return response\n```\n\n```javascript\nconst BASE = \"https://api-connect-us.bridge.new\";\nconst HEADERS = { \"x-api-key\": \"YOUR_API_KEY\" };\n\nconst sleep = (seconds) => new Promise((done) => setTimeout(done, seconds * 1000));\n\nfunction rateLimitState(response) {\n  const fields = Object.fromEntries(\n    (response.headers.get(\"RateLimit\") ?? \"\")\n      .split(\";\")\n      .filter((part) => part.includes(\"=\"))\n      .map((part) => part.split(\"=\"))\n  );\n  return { remaining: Number(fields.r ?? 1), resetIn: Number(fields.t ?? 1) };\n}\n\nasync function get(path) {\n  while (true) {\n    const response = await fetch(`${BASE}${path}`, { headers: HEADERS });\n\n    if (response.status === 429) {\n      await sleep(Number(response.headers.get(\"Retry-After\")));\n      continue;\n    }\n\n    const { remaining, resetIn } = rateLimitState(response);\n    if (remaining === 0) await sleep(resetIn);\n\n    return response;\n  }\n}\n```\n\n\n**Warning**\n\n  Do not retry a `429` immediately or on a fixed timer of your own. Retrying before `Retry-After` elapses is rejected again, and a tight retry loop keeps the workspace at zero budget for every other application sharing it.\n\n---\n\n## Standards\n\nThese fields are not Bridge-specific: `RateLimit-Policy` and `RateLimit` follow the IETF [RateLimit header fields](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/) specification, `Retry-After` is [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#field.retry-after), and the `429` body is [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html). If you already handle `X-RateLimit-*` headers from another API, these are the standardised equivalents.\n\nWhen the two disagree, **`Retry-After` wins** — it is the authoritative retry signal, and `RateLimit` is pacing information.\n\n---\n\n## Related Topics\n\n- [Authentication](https://docs.bridge.new/credentials.md) — API Keys, and the `401` / `403` responses that must not be retried\n- [Error Codes](https://docs.bridge.new/error-codes.md) — Full reference, including `BRIDGE_RATE_LIMIT_0001`\n- [REST API Reference](https://docs.bridge.new/api-reference/health/health-check.md) — Every endpoint documents its `429` response\n"
    },
    {
      "title": "Error codes",
      "source": "https://docs.bridge.new/error-codes",
      "markdownUrl": "https://docs.bridge.new/error-codes.md",
      "description": "Interpret REST API error codes, validation responses and uncertain write outcomes, and choose the next action.",
      "kind": "page",
      "contentMarkdown": "## Response format\n\nExample of a `Problem` response. Consult the endpoint reference for its HTTP status, media type and response schema:\n\n```json\n{\n  \"title\": \"A template named in the onboarding override is missing values only the caller can provide\",\n  \"status\": 422,\n  \"detail\": \"A template named in the onboarding override is missing values only the caller can provide\",\n  \"code\": \"BRIDGE_CONVERSATION_0604\",\n  \"extra\": {\n    \"templateId\": \"f0000040-0000-4000-8000-000000000040\",\n    \"missingVariables\": [{ \"key\": \"delivery_date\", \"label\": \"Delivery date\" }]\n  },\n  \"instance\": \"https://api-connect-us.bridge.new/bridge/api/v1/workspaces/{workspaceId}/conversations\",\n  \"traceId\": \"Root=1-6702b8c4-3f1a2b4c5d6e7f8091a2b3c4\"\n}\n```\n\n| Field | Meaning |\n| ----- | ------- |\n| `code` | When present, the stable error code listed on this page. **Branch on this field rather than the title.** Validation and server errors may omit it. |\n| `title` | A human-readable message. It may be longer than the title shown in the tables below, so never match on it |\n| `status` | The HTTP status code |\n| `detail` | More detail about this occurrence, when available |\n| `extra` | Machine-readable context for the error, such as the ids or keys involved, when available |\n| `instance` | The request URL |\n| `traceId` | Include it when you contact support |\n\n**Note**\n\n  Read the response schema for the operation you call. Where it is `HTTPValidationError`, validation details are in a `detail` array; each entry contains `loc` (where), `msg` (what) and `type`. Other documented `422` responses use `Problem` and may include a `code` and `extra`. Do not assume all `422` responses have the same shape.\n\n### Parse validation errors defensively\n\nAccept JSON error bodies with either `application/json` or `application/problem+json`. Check the HTTP status first, then inspect the body:\n\n1. If `code` is present, use the matching entry on this page to choose the action.\n2. If `errors` is an array, read its `loc`, `msg` and `type` entries as validation details.\n3. If `detail` is an array, read it as the `HTTPValidationError` form. A string `detail` is an explanation, not a list of field errors.\n4. Preserve `traceId`, status and the request URL for support. Do not require `code` or `extra` to exist, and do not match human-readable messages as identifiers.\n\nAn invalid field and a rule involving several fields need not share a status. The reviewed outbound body rules raise `400` with `BRIDGE_CORE_0103` when `consent.status` is `GRANTED` without `capturedAt`, or when `template.media` supplies both or neither of `mediaId` and `url`. Invalid field types or UUIDs can instead produce `422` request-validation details. Handle the status and error body together; do not require an `errors` array for every invalid request.\n\n### Legacy code strings\n\nSome legacy error handlers append a symbolic name to `code`, for example `BRIDGE_PRELOAD_FILE_0002 | PRELOAD_FILE_IS_PENDING`. Preserve the complete received value for logs and support. For catalog lookup, accept a known bare code or its explicitly recognized legacy form; do not compare human-readable `title` or `detail`, invent an unknown code, or rewrite the response body. The current deployed handler version has not been independently confirmed by this source review.\n\n**Warning**\n\n  **Validation compatibility:** some published OpenAPI responses declare `HTTPValidationError.detail[]` and `application/json`, while the shared API error handler reviewed for this documentation returns validation details as `Problem.errors[]` with `application/problem+json`. This editorial update preserves those published response definitions. Handle both forms; confirmation against the deployed API remains pending before choosing one as the only accepted form in a strict generated client.\n\n### Decide whether to retry\n\n| Result | Action |\n| --- | --- |\n| `400` or `422` | Correct the request using the code and field errors. Repeating unchanged input does not fix validation. |\n| `401` or `403` | Check the credential, workspace and permissions for the correct integration surface. Keep REST API keys on your server. |\n| `404` | Confirm the identifier and workspace. A lookup that finds no Contact is different from malformed input. |\n| `409` | Read the operation's code: an existing conversation, unavailable Agent or unfinished session needs a different resolution. Do not enable `forceSend` automatically. |\n| `429` | Follow the [rate-limit headers and backoff guidance](https://docs.bridge.new/rate-limits.md). |\n| Timeout, disconnected response or `5xx` after a write | Treat the outcome as uncertain. Inspect the affected resource before resubmitting; an error does not prove that no work was persisted. |\n\nThe general retry guidance does not add idempotency to an endpoint. For outbound reuse, recovery and partial writes, see [Agent outbound and handoff](https://docs.bridge.new/guides/agent-outbound.md). Custom Connections has its own [error reference](https://docs.bridge.new/lead-capture/errors.md) and no REST API key.\n\n## Message reply errors\n\nFor `POST .../messages`, the reviewed reply checks define these additional legacy forms:\n\n- `BRIDGE_MESSAGE_0001 | MESSAGE_NOT_FOUND` (`404`): the Message named by `replyToMessageId` was not found.\n- `BRIDGE_MESSAGE_0002 | MESSAGE_IS_NOT_ON_CONVERSATION` (`403`): the reply target belongs to another Conversation. Use a target from the Conversation you are sending to.\n\nPreserve the full `code` string, including its symbolic suffix. These checks concern the reply target, not the attached file's `mediaId`.\n\n## File upload errors\n\nThe reviewed upload flow defines these legacy error strings. Their suffix is part of the received `code`, not a request field. Consult the operation response contract and keep the compatibility parsing above.\n\nThe operation determines which file errors apply: `POST .../files` checks metadata and can return `0003` or `0004` below. `POST .../messages` can return `0001` or `0002` when an attached `mediaId` is missing or still pending. Outbound template media has its own error handling; do not substitute these codes for `BRIDGE_AI_LEADSCOUT_0016`.\n\n| Legacy `code` | Status | Action |\n| --- | --- | --- |\n| `BRIDGE_PRELOAD_FILE_0001 \\| PRELOAD_FILE_NOT_FOUND` | 404 | Check the returned `mediaId` and the upload being referenced. |\n| `BRIDGE_PRELOAD_FILE_0002 \\| PRELOAD_FILE_IS_PENDING` | 400 | The file is still pending. Confirm the PUT completed and allow upload processing; do not infer that a failed Message request persisted no work. |\n| `BRIDGE_PRELOAD_FILE_0003 \\| PRELOAD_FILE_INVALID_MIME_TYPE` | 400 | Correct the MIME type and logical `fileType` combination. |\n| `BRIDGE_PRELOAD_FILE_0004 \\| PRELOAD_FILE_INVALID_NAME` | 400 | Supply a file name with an extension allowed for that logical `fileType`. |\n\n## Agent Error Codes\n\nError codes for Agent operations.\n\n| Code | Title | Message |\n| ---- | -------------- | ------- |\n| `BRIDGE_AI_AGENT_OUTBOUND_0001` | Agent Not Found | Agent not found in the workspace |\n| `BRIDGE_AI_AGENT_OUTBOUND_0002` | Agent Not Available | Agent is not available for outbound |\n| `BRIDGE_AI_AGENT_OUTBOUND_0003` | Agent Type Not Supported | Agent type is not supported for outbound |\n| `BRIDGE_AI_AGENT_OUTBOUND_0004` | No Channels For Agent | No active messaging line is bound to the agent |\n| `BRIDGE_AI_AGENT_OUTBOUND_0005` | Agent Not Assigned To Channel | Agent is not assigned to the requested channel |\n| `BRIDGE_AI_AGENT_OUTBOUND_0006` | Conversation Controlled By Other Agent | Active conversation is controlled by another agent |\n| `BRIDGE_AI_AGENT_OUTBOUND_0007` | Invalid Cursor | Pagination cursor is malformed or belongs to another workspace |\n\n`POST .../agents/{agentId}/outbound` returns `0001` and `0004` as `404`, `0002` and `0006` as `409`, and `0003` and `0005` as `422`.\n\n- `0002`: the Agent is a draft or has no line. `extra.reason` says which; the same reason appears as `unavailableReason` in `GET .../agents`.\n- `0006`: the Contact's conversation on the line the request resolved is held by another Agent or by a Bridge User. Nothing was sent. Release it with `POST .../conversations/{conversationId}/release`, using `extra.conversationId`, then retry. `extra` also carries `channelId`, and `controllingAgentId` (the Agent holding it) only when an Agent holds it: when a Bridge User does, the key is absent, not `null`.\n\n`GET .../agents` returns `0007` as `400` when the `cursor` is malformed or was issued for another workspace. Start again without a cursor. Keep the same `type` filter while paging: a cursor issued under one `type` filter is rejected if the request carries a different `type`.\n\n### Other codes returned by `POST .../agents/{agentId}/outbound`\n\n| Code | Status | Meaning |\n| ---- | ------ | ------- |\n| `BRIDGE_CORE_0103` | 400 | The request is invalid: `consent.status` is `GRANTED` without `capturedAt`, `template` carries neither `id` nor `name`, or `template.media` does not carry exactly one of `mediaId` or `url` |\n| `BRIDGE_CORE_0301` | 403 | The workspace in the path differs from the one of the API key |\n| `BRIDGE_AI_LEADSCOUT_0001` | 404 | The Contact does not exist in the workspace |\n| `BRIDGE_AI_LEADSCOUT_0002` | 404 | The `handoffOverride` Bridge User does not exist in the workspace |\n| `BRIDGE_AI_LEADSCOUT_0003` | 409 | The `handoffOverride` Bridge User was deleted |\n| `BRIDGE_AI_LEADSCOUT_0004` | 404 | No template of the workspace matches `template.id` (or `template.name` when no id is sent) |\n| `BRIDGE_AI_LEADSCOUT_0005` | 409 | Template not approved |\n| `BRIDGE_AI_LEADSCOUT_0006` | 400 | Template ordered params mismatch |\n| `BRIDGE_AI_LEADSCOUT_0007` | 409 | Active conversation already exists: another request took the released conversation first. Retry |\n| `BRIDGE_AI_LEADSCOUT_0008` | 409 | No line of the Agent is free or recoverable for this Contact (see `forceSend`) |\n| `BRIDGE_AI_LEADSCOUT_0009` | 404 | No line configured for the requested `brandId` |\n| `BRIDGE_AI_LEADSCOUT_0016` | 400 | Outbound validation error |\n| `BRIDGE_AI_LEADSCOUT_0017` | 409 | Latest session is not finalized |\n| `BRIDGE_AI_LEADSCOUT_0018` | 409 | Could not finalize the expired session before starting a new one |\n| `BRIDGE_AI_LEADSCOUT_0019` | 422 | Missing required template variable(s) |\n| `BRIDGE_AI_LEADSCOUT_0020` | 422 | Conflicting template parameter sources for the same slot |\n| `BRIDGE_AI_LEADSCOUT_0021` | 422 | Unknown template parameter key |\n\nFor `POST .../agents/{agentId}/outbound`, `5xx` errors carry no `code` or `extra`.\n\n## LeadScout Error Codes\n\nAdditional codes documented for [starting a LeadScout outbound conversation](https://docs.bridge.new/api-reference/leadscout-api/initiate-a-leadscout-outbound-conversation-with-a-known-lead.md) with `POST .../leadscout/outbound`:\n\n| Code | Status | Title | Meaning |\n| --- | --- | --- | --- |\n| `BRIDGE_AI_LEADSCOUT_0010` | 404 | Channel Not In Workspace | Provider channel not in workspace |\n| `BRIDGE_AI_LEADSCOUT_0011` | 404 | No Provider Channels | Workspace has no provider channels |\n| `BRIDGE_AI_LEADSCOUT_0012` | 404 | No Agent For Channel | No prospecting agent for channel |\n| `BRIDGE_AI_LEADSCOUT_0013` | 409 | Multiple Agents For Brand | Multiple prospecting agents for brand |\n| `BRIDGE_AI_LEADSCOUT_0014` | 409 | Multiple Agents For Channel | Multiple prospecting agents for channel |\n| `BRIDGE_AI_LEADSCOUT_0015` | 500 | Conversation Operation Failed | Conversation operation failed |\n\n## Brand Error Codes\n\nError codes for brand checks when creating a conversation.\n\n| Code | Title | Message |\n| ---- | -------------- | ------- |\n| `BRIDGE_BRAND_0101` | Brand Required | The workspace has brands enabled; each external participant must resolve to a brand or a line |\n\nReturned as `400`. Send a `brandId` or a `providerChannelId` for every external participant; `extra.resources` lists the participants that had neither.\n\n## Conversation Error Codes\n\nError codes for conversation operations.\n\n| Code | Title | Message |\n| ---- | -------------- | ------- |\n| `BRIDGE_CONVERSATION_0001` | Conversation Not Found | The requested conversation was not found |\n| `BRIDGE_CONVERSATION_0002` | Conversation Not In Workspace | The conversation does not belong to the specified workspace |\n| `BRIDGE_CONVERSATION_0003` | Conversation With Same Participants Already Exists | An active conversation with the same participants already exists |\n| `BRIDGE_CONVERSATION_0004` | Conversation Not Active | The conversation is inactive; no operations can be performed on it |\n| `BRIDGE_CONVERSATION_0005` | Conversation Operation Failed | An error occurred while operating on the conversation |\n| `BRIDGE_CONVERSATION_0006` | Conversation With Inconsistent State | The conversation is in an inconsistent state; contact support to resolve |\n| `BRIDGE_CONVERSATION_0007` | Conversation Is Released | Conversation is released (`409` from `POST .../messages`) |\n| `BRIDGE_CONVERSATION_0101` | Internal User Already In Conversation | The internal user is already a participant in this conversation |\n| `BRIDGE_CONVERSATION_0102` | Internal User Not In Conversation | The internal user is not a participant in this conversation |\n| `BRIDGE_CONVERSATION_0103` | External User Already In Conversation | The external user is already a participant in this conversation |\n| `BRIDGE_CONVERSATION_0104` | External User Not In Conversation | The external user is not a participant in this conversation |\n| `BRIDGE_CONVERSATION_0105` | External User Already Use This Channel | The external user is already using this messaging channel in some conversation |\n| `BRIDGE_CONVERSATION_0201` | External Participants Required | At least one external participant is required to operate in a conversation |\n| `BRIDGE_CONVERSATION_0202` | Internal Participants Required | At least one internal participant is required to operate in a conversation |\n| `BRIDGE_CONVERSATION_0203` | User Is Not Admin | The user is not an administrator of this conversation |\n| `BRIDGE_CONVERSATION_0204` | Last Admin Removal Forbidden | The conversation must have at least one administrator |\n| `BRIDGE_CONVERSATION_0205` | Min Internal Users Required | The conversation must have a minimum number of internal participants |\n| `BRIDGE_CONVERSATION_0206` | Max Internal Participants Exceeded | The maximum number of internal participants for this conversation has been exceeded |\n| `BRIDGE_CONVERSATION_0207` | Min External Participants Required | The conversation must have a minimum number of external participants |\n| `BRIDGE_CONVERSATION_0208` | Max External Participants Exceeded | The maximum number of external participants for this conversation has been exceeded |\n| `BRIDGE_CONVERSATION_0210` | Min Participants Required | The conversation must have a minimum number of participants |\n| `BRIDGE_CONVERSATION_0211` | Max Participants Exceeded | The conversation must have a maximum number of participants |\n| `BRIDGE_CONVERSATION_0212` | Max Initial Messages Exceeded | The maximum number of initial messages has been exceeded |\n| `BRIDGE_CONVERSATION_0213` | Messages Not In Conversation | The messages must belong to the same conversation |\n| `BRIDGE_CONVERSATION_0601` | Onboarding Template Not Found | A template named in the onboarding override does not exist |\n| `BRIDGE_CONVERSATION_0602` | Onboarding Template Not In Workspace | A template named in the onboarding override belongs to another workspace |\n| `BRIDGE_CONVERSATION_0603` | Onboarding Template Not Approved | A template named in the onboarding override is not approved for the line a participant will be reached on |\n| `BRIDGE_CONVERSATION_0604` | Onboarding Template Missing Parameters | A template named in the onboarding override is missing values only the caller can provide |\n| `BRIDGE_CONVERSATION_0605` | Onboarding Template Unknown Parameters | The onboarding override names parameters the template does not declare |\n| `BRIDGE_CONVERSATION_0606` | Onboarding Template Parameters Not Supported | Parameters were sent for an onboarding template that is not a WhatsApp provider template |\n| `BRIDGE_CONVERSATION_0607` | Onboarding Template Invalid Parameter Values | Parameter values for an onboarding template break a WhatsApp rule and would be rejected when sent |\n| `BRIDGE_CONVERSATION_0608` | Onboarding Configured Template Requires Caller Input | A template in the workspace's onboarding sequence needs values only the caller can provide; a workspace admin must change the onboarding sequence |\n\nWhen creating a conversation, `BRIDGE_CONVERSATION_0105` returns `409` with `extra.reason` set to `PARTICIPANTS_MISMATCH`: a released conversation holds the Contact's line with other Contacts or lines. Request the same Contacts and lines, or use another line.\n\n`BRIDGE_CONVERSATION_0601` returns `404`, `0602` and `0603` return `400`, and `0604` to `0608` return `422`. See [Onboarding template parameters](https://docs.bridge.new/templates-and-parameters.md) for what each one carries in `extra` and how to fix it.\n\n## External User Error Codes\n\nError codes for external user (contact) operations.\n\n| Code | Title | Message |\n| ---- | -------------- | ------- |\n| `BRIDGE_EXTERNAL_0001` | External User Not Found | The requested external user was not found |\n| `BRIDGE_EXTERNAL_0002` | External User Already Exists | An external user with the same identification data already exists |\n| `BRIDGE_EXTERNAL_0004` | External User Not In Workspace | One or more external users do not belong to the specified workspace |\n| `BRIDGE_EXTERNAL_0005` | External User Without Configured Field | External user does not have configured field; check the workspace or external information settings |\n| `BRIDGE_EXTERNAL_0007` | Cellphone Owned By Another Contact | The phone number is already an active identity of a different external user |\n| `BRIDGE_EXTERNAL_0013` | Email Owned By Another Contact | The email address is already an active identity of a different external user |\n| `BRIDGE_EXTERNAL_0101` | External User Operation Failed | An error occurred while operating on the external user |\n\n## Internal User Error Codes\n\nError codes for internal user operations.\n\n| Code | Readable Title | Message |\n| ---- | -------------- | ------- |\n| `BRIDGE_INTERNAL_0001` | Internal User Not Found | The internal user was not found |\n| `BRIDGE_INTERNAL_0002` | Internal Users Not In Workspace | The provided internal users do not belong to the specified workspace |\n| `BRIDGE_INTERNAL_0003` | Inactive Internal User | One or more internal users are inactive in the workspace; check the user status in the workspace settings |\n| `BRIDGE_INTERNAL_0004` | Internal User Without Configured Field | Internal user does not have configured field; check the workspace or internal information settings |\n| `BRIDGE_INTERNAL_0101` | Internal User Does Not Support Operation | The internal user does not support this operation |\n\n## Provider Channel Error Codes\n\nError codes for provider channel operations.\n\n| Code | Readable Title | Message |\n| ---- | -------------- | ------- |\n| `BRIDGE_PROVIDER_CHANNEL_0001` | Provider Channel Not Found | The provider channel was not found; verify the workspace has channels configured |\n\nWhen creating a conversation, this code is returned as `409` when no WhatsApp line is free for a Contact and there is no released conversation to recover, or when the `providerChannelId` you named is not available for that Contact.\n\n## Rate Limit Error Codes\n\nError codes returned when a workspace exceeds its request budget. See [Rate limits](https://docs.bridge.new/rate-limits.md) for the budget, the headers and a client that paces itself.\n\n| Code | Title | Message |\n| ---- | -------------- | ------- |\n| `BRIDGE_RATE_LIMIT_0001` | Too Many Requests | The workspace has exhausted its request budget for the current window |\n\nThis is the only code that should be retried automatically, and only after the delay given in the `Retry-After` header. Authentication failures are not rate-limit errors and retrying them on a schedule will never succeed.\n\n## Request Error Codes\n\nError codes returned before an operation runs, when the request itself cannot be processed. See [Changelog](https://docs.bridge.new/changelog.md) for parameters an endpoint will stop accepting.\n\n| Code | Title | Message |\n| ---- | -------------- | ------- |\n| `BRIDGE_CORE_0101` | Invalid Field Format | The field format is invalid; provide a valid value for the field |\n| `BRIDGE_CORE_0102` | Missing Required Field | The request is missing required fields; review the operation |\n| `BRIDGE_CORE_0103` | Bad Request | The request is invalid; review the body and parameters |\n| `BRIDGE_CORE_0104` | Invalid Cursor | The provided cursor is invalid; use a cursor obtained from a previous response |\n\nNone of these should be retried unchanged. `detail` describes what is wrong and, when it can, names the field or parameter at fault; the same request will fail the same way until you change it.\n\n## Template Error Codes\n\nError codes for template operations.\n\n| Code | Title | Message |\n| ---- | -------------- | ------- |\n| `BRIDGE_TEMPLATE_0001` | Provider Template Not Found | No provider template found for the given workspace and name |\n\nReturned as `404` by `GET .../templates/{templateId}` when the id or name does not match a WhatsApp template in your workspace.\n\n## Workspace Error Codes\n\nError codes for workspace operations.\n\n| Code |  Title | Message |\n| ---- | -------------- | ------- |\n| `BRIDGE_WORKSPACE_0001` | Workspace Not Found | The requested workspace was not found |\n| `BRIDGE_WORKSPACE_0002` | Workspace Not In Organization | The workspace does not belong to the specified organization |\n| `BRIDGE_WORKSPACE_0003` | Workspace Without Available Provider Channels | The workspace has no provider channels available |\n| `BRIDGE_WORKSPACE_0004` | Workspace Without Configured Field | Workspace without configured field (`409` from `POST .../conversations/{conversationId}/release`) |\n"
    },
    {
      "title": "Changelog",
      "source": "https://docs.bridge.new/changelog",
      "markdownUrl": "https://docs.bridge.new/changelog.md",
      "description": "Changes to the Bridge API that affect your integration, newest first.",
      "kind": "page",
      "contentMarkdown": "## Overview\n\nChanges that affect the calls you already make, newest first. Additive changes — a new endpoint, a new optional field — are not listed here unless they replace something being removed; they appear in the [REST API](https://docs.bridge.new/api-reference/health/health-check.md) reference when they ship.\n\n**Note**\n\n  The older entries below did not record effective dates. Their deployment dates have not been verified; their order does not establish a release date. The retirement date for deprecated list filters is **not yet announced**. Keep using the documented current contract until an effective change is published, and use the available lookup endpoints for new integrations.\n\n## Documentation improvements\n\nThis revision clarifies the existing API contract and introduces task-based integration guides. It renames the landing-page feature to **Custom Connections**, explains outbound conversation reuse and recovery, and improves error and webhook guidance. The existing API signatures and `/lead-capture` documentation URLs are preserved. A documentation revision date is not an API deployment or deprecation date.\n\n---\n\n## Creating a conversation recovers a released one\n\n`POST /conversations` used to return `409 BRIDGE_PROVIDER_CHANNEL_0001` when no line was free. Now, if the requested Contacts have a conversation released with recovery, it is recovered and returned with `201` and its existing `id`, so you may receive an `id` you already stored. If that conversation has other Contacts or lines, the request returns `409 BRIDGE_CONVERSATION_0105`. See the [REST API](https://docs.bridge.new/api-reference/health/health-check.md) reference for details.\n\n---\n\n## Finding a contact or a user by an identifier\n\n**Warning**\n\n  **This will remove query parameters that exist today.** They still work and are marked deprecated in the [REST API](https://docs.bridge.new/api-reference/health/health-check.md) reference. The date they stop being accepted will be published here before that happens; the lookup endpoints that replace them are available now.\n\n### What changes\n\n**Available now:** `GET /contacts/lookup` and `GET /users/lookup` return one contact or one Bridge User (an internal user, in the API reference) by an identifier.\n\n**Deprecated, to be removed:** the list endpoints will stop filtering. `GET /contacts` and `GET /users` will keep `limit` and `cursor` and nothing else.\n\n| Deprecated | Replacement |\n| ------- | ----------- |\n| `GET /contacts?search=` — a cellphone | `GET /contacts/lookup?type=PHONE&value=…` |\n| `GET /contacts?search=` — an email | `GET /contacts/lookup?type=EMAIL&value=…` |\n| `GET /contacts?search=` — a name | **none** |\n| `GET /contacts?ownerEmail=` | **none** |\n| `GET /users?email=` | `GET /users/lookup?type=EMAIL&value=…` |\n| `GET /users?status=` | **none** — every row already carries `status` |\n\nOnce removed, a parameter will be **refused**, not ignored. Sending one will return `400` with `BRIDGE_CORE_0103`, and the response will name the parameter. We chose this deliberately: a call that silently ignored your filter would hand you the first page of your whole workspace and let you treat it as a match.\n\n### What to do\n\n**If you search by cellphone or email**, move to the matching lookup endpoint. It answers with the same object the list returns for each row, and returns `404` when nothing matches.\n\n`type` takes the identifier types the API already uses elsewhere — the same values as `identifierType` on a conversation participant. Contacts accept `ID`, `EMAIL` and `PHONE`; Bridge Users accept `ID` and `EMAIL`, since a Bridge User's cellphone is not an addressable identifier in Bridge.\n\n`ID` is accepted for the same reason: code that already carries an `{identifierType, identifier}` pair should not have to branch on one of the values to build a different URL. It returns exactly what `GET /contacts/{contactId}` returns, and that remains the canonical URL for a contact — prefer it when the id is what you hold.\n\n```bash\n# Before\ncurl \"https://api-connect-us.bridge.new/bridge/api/v1/workspaces/$WORKSPACE/contacts?search=%2B14084029292\" \\\n  -H \"x-api-key: $BRIDGE_API_KEY\"\n\n# After\ncurl \"https://api-connect-us.bridge.new/bridge/api/v1/workspaces/$WORKSPACE/contacts/lookup?type=PHONE&value=%2B14084029292\" \\\n  -H \"x-api-key: $BRIDGE_API_KEY\"\n```\n\nA `PHONE` is read the same way as the cellphone you send when you create a contact: a number without a country code is read in your workspace's default country, and spaces and separators are ignored. An `EMAIL` has its domain lowercased, but its local part is matched exactly as sent.\n\n**Note**\n\n  A cellphone in international form begins with `+`, which must be percent-encoded as `%2B` in a query string. Most HTTP clients do this for you when you pass parameters as a map rather than building the URL by hand.\n\nA `value` that does not match its `type` — not a well-formed email, cellphone or UUID — is rejected with `422` and `BRIDGE_CORE_0101` before any lookup happens. Handle it separately from `404` if you pass user input straight through: one says *you sent something that is not an identifier*, the other says *nobody here holds it*.\n\n**If you filter Bridge Users by status**, read `status` off each row instead. It is derived rather than stored, and it is already in every response. Note that today the list returns only active users unless you pass `status`; once the parameter is removed it will return active and disabled users alike.\n\nThe same applies to `GET /users/lookup`: it returns the Bridge User whether active or disabled, preferring the active one when an email matches both. `GET /users?email=` returns nothing for a disabled user today, so check `status` on the response if you only act on active users.\n\n**If you search contacts by name**, there will be no replacement, and it is worth knowing what you would lose. The parameter only matches the **exact full name** — first name, one space, last name. It never matches a prefix, a partial, or a surname on its own. If it works for you, you are passing the complete name already.\n\n**If you filter contacts by owner**, there will be no replacement. List the page and filter on `ownerId` in your own code. `ownerId` is the id of the owning Bridge User, not an email: get it once with `GET /users/lookup?type=EMAIL&value=…` and compare against its `id`.\n\n### Why\n\nFiltering on those parameters makes the cost of the call depend on the size of your workspace rather than the size of the answer. Looking up one contact by phone number in a large workspace reads tens of thousands of records to return one — and asking for a bigger page makes it slower, not faster. The lookup endpoints read only the records that hold that identifier.\n\nThis is also why the filters are being removed rather than fixed: where a replacement exists it is strictly better, and the name search, which has none, never matched anything but the exact full name.\n\n---\n\n## Related Topics\n\n- [Error Codes](https://docs.bridge.new/error-codes.md) — Full reference, including `BRIDGE_CORE_0103` and `BRIDGE_CORE_0101`\n- [REST API Reference](https://docs.bridge.new/api-reference/health/health-check.md) — The lookup endpoints, their parameters and their responses\n- [Rate limits](https://docs.bridge.new/rate-limits.md) — What the API accepts per workspace, and how to back off correctly\n"
    },
    {
      "title": "Receive Bridge webhooks",
      "source": "https://docs.bridge.new/webhooks",
      "markdownUrl": "https://docs.bridge.new/webhooks.md",
      "description": "Connect an HTTPS receiver to Bridge events and understand authentication, acknowledgement and asynchronous processing.",
      "kind": "page",
      "contentMarkdown": "Webhooks send events **from Bridge to your system**. Configure an HTTPS callback and subscribe to the event families your integration needs. Bridge sends JSON using `POST`; your receiver authenticates and stores the event before returning `2xx`.\n\nUse the [REST Integrations API](https://docs.bridge.new/credentials.md) when your system needs to read or change a Bridge resource. A webhook subscription does not create a complete two-way CRM integration by itself: your receiver must map identifiers, process the event and implement any intended write back through the documented REST operation.\n\n## Build the receiver\n\n  \n\n### Choose your events\n\n    Read the [event reference](https://docs.bridge.new/webhooks/events.md) and select the family and subscription version that matches your workflow.\n  \n\n  \n\n### Verify and store each request\n\n    Follow [Verify signatures](https://docs.bridge.new/webhooks/verifying-signatures.md) for the exact HMAC serialization and [Receive events](https://docs.bridge.new/webhooks/receive-events.md) for a durable inbox example.\n  \n\n  \n\n### Configure and test the callback\n\n    Enter the URL, webhook API key and client secret in the [Bridge Admin Console](https://docs.bridge.new/webhooks/configuration.md). Check the test callback before enabling your business workflow.\n  \n\n```mermaid\nsequenceDiagram\n    participant B as Bridge\n    participant R as Your receiver\n    participant I as Durable inbox\n    participant W as Your worker\n    B->>R: POST JSON event and authentication headers\n    R->>R: Verify API key, HMAC and freshness policy\n    R->>I: Commit verified event or confirm exact duplicate\n    I-->>R: Durable acceptance\n    R-->>B: 2xx acknowledgement\n    W->>I: Read unprocessed events\n    W->>W: Apply business action idempotently\n```\n\n## Keep the credentials separate\n\n| Header | Purpose |\n| --- | --- |\n| `X-Bridge-API-Key` | Webhook API key configured for this callback. Compare it with your expected value. |\n| `X-Bridge-Signature` | `sha256=` plus HMAC-SHA256 of Bridge's canonical event representation, using your webhook client secret. |\n| `X-Bridge-Timestamp` | Unix seconds for a receiver freshness check. The HMAC does **not** include this header. |\n\nThese webhook credentials are separate from the REST API's `x-api-key` and the browser's Custom Connections `captureKey`. Keep the webhook client secret on your server.\n\n## What an acknowledgement means\n\nA `2xx` response means your receiver accepted the event. It does not establish that your CRM update or other business action completed. Persist first and process outside the request, so a slow downstream service does not hold up acknowledgement.\n\nThe current HTTP sender makes three attempts per delivery operation, with one- and two-second waits and a default ten-second socket timeout. This is not a promise of three attempts across the entire event pipeline. Read [retry, duplicate and ordering behavior](https://docs.bridge.new/webhooks/receive-events.md) before choosing your recovery policy.\n\nDo not assume exactly-once delivery, a global event order, or a public replay endpoint. The [event reference](https://docs.bridge.new/webhooks/events.md) describes payloads; the [receiver guide](https://docs.bridge.new/webhooks/receive-events.md) explains how to handle them reliably.\n"
    },
    {
      "title": "Verify webhook requests",
      "source": "https://docs.bridge.new/webhooks/verifying-signatures",
      "markdownUrl": "https://docs.bridge.new/webhooks/verifying-signatures.md",
      "description": "Verify the webhook API key and the exact HMAC representation used by Bridge, then deduplicate accepted events.",
      "kind": "page",
      "contentMarkdown": "Verify each request before accepting it into your system. Use the **webhook** API key and client secret configured for this callback; these are separate from your REST Integrations API key.\n\n## Authentication headers\n\n| Header | Check |\n| --- | --- |\n| `X-Bridge-API-Key` | Constant-time comparison with the webhook API key you configured |\n| `X-Bridge-Signature` | `sha256=` followed by the lowercase HMAC-SHA256 hex digest |\n| `X-Bridge-Timestamp` | Unix seconds within your accepted freshness window; the example allows five minutes in either direction |\n\nHeader names are case-insensitive.\n\n## Exactly what is signed\n\nBridge builds the HMAC input from the JSON event using the Python expression below, then encodes that string as UTF-8:\n\n```python\njson.dumps(event, sort_keys=True).encode(\"utf-8\")\n```\n\nThis sorts object keys recursively, preserves array order, escapes non-ASCII characters, and uses Python's default spaces after commas and colons. It does not append a newline. The key is the client secret encoded as UTF-8. This is a Python JSON serialization convention, not RFC 8785 / JCS.\n\nDo not replace it with a digest of arbitrary raw request bytes, compact JSON, a generic JavaScript `JSON.stringify`, or a timestamp-prefixed string. Whitespace, Unicode escaping and number formatting can change the HMAC input even when the parsed object looks equivalent. Use the Python verifier below or test another implementation against the vector before connecting it.\n\n**Warning**\n\n  The current HMAC input contains the event JSON only. **`X-Bridge-Timestamp` is not included in the signature.** Check freshness, but do not treat the timestamp alone as cryptographic replay protection: a previously valid payload and signature can be presented with a different timestamp. Authenticate the webhook API key and use a durable inbox and idempotent business processing as described in [Receive events](https://docs.bridge.new/webhooks/receive-events.md).\n\n## Python verifier\n\nSave this as `bridge_webhooks.py`. It returns the verified parsed object so downstream code uses the same interpretation that was authenticated. Secrets should come from server-side environment variables or your secret store.\n\n```python\nimport hashlib\nimport hmac\nimport json\nimport re\nimport sqlite3\nimport time\n\n\nclass VerificationError(ValueError):\n    pass\n\n\ndef canonical_payload(event):\n    # Match Bridge's Python json.dumps defaults: sort recursively, ASCII escapes,\n    # a space after commas and colons, then UTF-8. This is not RFC 8785 / JCS.\n    return json.dumps(event, sort_keys=True).encode(\"utf-8\")\n\n\ndef _unique_object(pairs):\n    result = {}\n    for key, value in pairs:\n        if key in result:\n            raise VerificationError(\"Duplicate JSON object key\")\n        result[key] = value\n    return result\n\n\ndef verify_webhook(raw_body, headers, webhook_api_key, client_secret, *, now=None, tolerance=300):\n    headers = {key.lower(): value for key, value in headers.items()}\n    received_key = headers.get(\"x-bridge-api-key\", \"\")\n    if not received_key or not webhook_api_key or not client_secret:\n        raise VerificationError(\"Missing webhook credentials\")\n    if not hmac.compare_digest(received_key.encode(\"utf-8\"), webhook_api_key.encode(\"utf-8\")):\n        raise VerificationError(\"Invalid webhook API key\")\n\n    received_signature = headers.get(\"x-bridge-signature\", \"\")\n    if not re.fullmatch(r\"sha256=[0-9a-f]{64}\", received_signature):\n        raise VerificationError(\"Invalid signature format\")\n    timestamp = headers.get(\"x-bridge-timestamp\", \"\")\n    if not re.fullmatch(r\"[0-9]+\", timestamp):\n        raise VerificationError(\"Invalid timestamp\")\n    current_time = time.time() if now is None else now\n    if abs(current_time - int(timestamp)) > tolerance:\n        raise VerificationError(\"Timestamp outside the permitted window\")\n\n    try:\n        event = json.loads(raw_body.decode(\"utf-8\"), object_pairs_hook=_unique_object)\n    except (UnicodeDecodeError, json.JSONDecodeError) as exc:\n        raise VerificationError(\"Invalid JSON body\") from exc\n    if not isinstance(event, dict):\n        raise VerificationError(\"Expected a JSON object\")\n    expected = \"sha256=\" + hmac.new(\n        client_secret.encode(\"utf-8\"), canonical_payload(event), hashlib.sha256\n    ).hexdigest()\n    if not hmac.compare_digest(expected, received_signature):\n        raise VerificationError(\"Invalid webhook signature\")\n    return event\n\n```\n\nThe five-minute window is receiver policy; keep your server clock synchronized. The verifier also rejects ambiguous duplicate JSON keys. Do not disable authentication when debugging a mismatch: check the configured webhook credentials and compare the canonical input against the fixture below.\n\n## Reproducible signature vector\n\nThese values are synthetic fixtures, not live credentials or an event to send to Bridge. When testing freshness checks, set the verifier's `now` argument to `1800000000`.\n\n| Value | Fixture |\n| --- | --- |\n| Client secret | `bridge-docs-test-secret` |\n| Webhook API key | `bridge-docs-test-key` |\n| Timestamp header | `1800000000` |\n| Expected signature | `sha256=5536c29fd98baab6ffe46dd9517adb185b32202864cbb5a76146172fc0b39423` |\n\nEvent object:\n\n```json\n{\n  \"eventId\": \"evt_docs_001\",\n  \"eventType\": \"contact.updated\",\n  \"entity\": \"contact\",\n  \"workspaceId\": \"ws_docs\",\n  \"timestamp\": \"2026-01-01T00:00:00Z\",\n  \"payload\": {\n    \"name\": \"José\",\n    \"tags\": [\n      \"café\",\n      \"one\"\n    ],\n    \"active\": true,\n    \"count\": 2\n  }\n}\n```\n\nExact signed JSON string (the `\\u` sequences below are literal ASCII backslashes, not decoded characters):\n\n```text\n{\"entity\": \"contact\", \"eventId\": \"evt_docs_001\", \"eventType\": \"contact.updated\", \"payload\": {\"active\": true, \"count\": 2, \"name\": \"Jos\\u00e9\", \"tags\": [\"caf\\u00e9\", \"one\"]}, \"timestamp\": \"2026-01-01T00:00:00Z\", \"workspaceId\": \"ws_docs\"}\n```\n\nChanging a field must fail verification. Changing only input key order or sending literal Unicode instead of JSON Unicode escapes must still verify after parsing and rebuilding the same canonical representation.\n\n## After verification\n\nCommit the event to a durable inbox before acknowledging it. A valid HMAC confirms the authenticated event representation; it does not prove that your business action has run, that the event is new, or that it is the latest state of the resource. Continue with [delivery, duplicates and ordering](https://docs.bridge.new/webhooks/receive-events.md).\n"
    },
    {
      "title": "Receive events reliably",
      "source": "https://docs.bridge.new/webhooks/receive-events",
      "markdownUrl": "https://docs.bridge.new/webhooks/receive-events.md",
      "description": "Verify, persist and acknowledge webhooks, then process duplicates and out-of-order events safely.",
      "kind": "page",
      "contentMarkdown": "A webhook receiver has two jobs: accept an authenticated event quickly, then process it reliably. Keep slow CRM calls, enrichment and other business work outside the HTTP request.\n\n## Receive → verify → persist → acknowledge\n\n1. Read the request body and headers.\n2. Verify the webhook API key, canonical HMAC and timestamp policy with [the verifier](https://docs.bridge.new/webhooks/verifying-signatures.md).\n3. Atomically store the verified event in a durable inbox, or confirm that this exact event is already there.\n4. Return a `2xx` response promptly. Process stored events in a worker and make each business action idempotent.\n\nReturn success only after durable acceptance. If storage is unavailable, return a non-`2xx` response so the delivery attempt is not acknowledged. A duplicate that is already stored should receive `2xx` again.\n\n## Retry behavior\n\nThe current sender makes up to **three HTTP attempts** for one delivery operation. It waits **one second**, then **two seconds**, before the remaining attempts. Any `2xx` response stops those attempts. A non-`2xx` response, a connection failure or a request exception can trigger the next attempt. The default socket timeout is **10 seconds** per attempt; acknowledge in a few seconds rather than relying on a 30-second allowance.\n\nThe payload, signature and timestamp headers are reused within that delivery operation. These three attempts are a bound for one send operation, **not an end-to-end ceiling on all deliveries of an event**. If an upstream event is delivered again, it can invoke the sender again. No total retry count or replay horizon is promised here. Do not rely on a `Retry-After` response header to set Bridge's retry timing.\n\nIf all attempts fail, the delivery is considered failed. Do not assume indefinite automatic retry or a public replay endpoint. For investigation, keep the event's workspace, type, ID and approximate time, and contact Bridge support. Do not include webhook credentials in support messages or application logs.\n\n## Duplicates and event identity\n\nYour service may have committed an event even when Bridge did not receive your `2xx` response. Retrying it must not create a second CRM update, invoice, message or other business effect.\n\nScope an inbox record to `workspaceId`, `eventType` and `eventId`. Also store a digest of the verified event. Do not assume `eventId` is a globally unique ID across every notification family: some notifications use an entity-derived ID. Retain distinct payloads that share an ID, then apply business idempotency for the operation your worker performs.\n\nThe example below deduplicates exact repeated events using all three identifiers plus the SHA-256 fingerprint of the canonical event. This is an inbox key for this example, not a new Bridge request field.\n\nDo not expire a deduplication record merely because the five-minute timestamp window has passed. That header is not signed, so the window does not establish a safe replay expiry. Choose retention around your business reconciliation needs and keep destination-side business actions idempotent. The example does not automatically delete inbox records.\n\n## Minimal durable receiver\n\nAdd these definitions to the `bridge_webhooks.py` module from [Verify webhook requests](https://docs.bridge.new/webhooks/verifying-signatures.md). The verifier module already includes the imports used below.\n\n```python\nclass SQLiteInbox:\n    \"\"\"Single-service example inbox. Use a shared durable store for multiple replicas.\"\"\"\n    def __init__(self, path):\n        self.path = str(path)\n        with sqlite3.connect(self.path) as db:\n            db.execute(\"\"\"CREATE TABLE IF NOT EXISTS webhook_inbox (\n                workspace_id TEXT NOT NULL, event_type TEXT NOT NULL,\n                event_id TEXT NOT NULL, fingerprint TEXT NOT NULL,\n                body TEXT NOT NULL, received_at REAL NOT NULL,\n                processed INTEGER NOT NULL DEFAULT 0,\n                PRIMARY KEY(workspace_id, event_type, event_id, fingerprint)\n            )\"\"\")\n\n    def accept(self, event):\n        identifiers = [event.get(key) for key in (\"workspaceId\", \"eventType\", \"eventId\")]\n        if not all(isinstance(value, str) and value for value in identifiers):\n            raise VerificationError(\"Missing event envelope identifiers\")\n        body = canonical_payload(event)\n        fingerprint = hashlib.sha256(body).hexdigest()\n        with sqlite3.connect(self.path, timeout=3) as db:\n            cursor = db.execute(\n                \"\"\"INSERT OR IGNORE INTO webhook_inbox\n                (workspace_id, event_type, event_id, fingerprint, body, received_at)\n                VALUES (?, ?, ?, ?, ?, ?)\"\"\",\n                (*identifiers, fingerprint, body.decode(\"utf-8\"), time.time()),\n            )\n            inserted = cursor.rowcount == 1\n        # Context manager commits before the request can be acknowledged.\n        return inserted\n\n\ndef make_wsgi_app(inbox, webhook_api_key, client_secret):\n    \"\"\"Verify, commit an inbox record, then acknowledge; workers process it later.\"\"\"\n    def app(environ, start_response):\n        if environ.get(\"REQUEST_METHOD\") != \"POST\":\n            start_response(\"405 Method Not Allowed\", [(\"Allow\", \"POST\")])\n            return [b\"\"]\n        try:\n            length = int(environ.get(\"CONTENT_LENGTH\") or \"0\")\n            if length < 1:\n                raise VerificationError(\"Missing JSON body\")\n            raw_body = environ[\"wsgi.input\"].read(length)\n            headers = {\n                \"x-bridge-api-key\": environ.get(\"HTTP_X_BRIDGE_API_KEY\", \"\"),\n                \"x-bridge-signature\": environ.get(\"HTTP_X_BRIDGE_SIGNATURE\", \"\"),\n                \"x-bridge-timestamp\": environ.get(\"HTTP_X_BRIDGE_TIMESTAMP\", \"\"),\n            }\n            event = verify_webhook(raw_body, headers, webhook_api_key, client_secret)\n            inbox.accept(event)  # A duplicate exact event is also acknowledged.\n        except (VerificationError, ValueError):\n            status, body = \"400 Bad Request\", b'{\"accepted\":false}'\n        except sqlite3.Error:\n            status, body = \"503 Service Unavailable\", b'{\"accepted\":false}'\n        else:\n            status, body = \"200 OK\", b'{\"accepted\":true}'\n        start_response(status, [(\"Content-Type\", \"application/json\"), (\"Content-Length\", str(len(body)))])\n        return [body]\n    return app\n\n```\n\nCreate the WSGI application in your server configuration:\n\n```python\nimport os\n\nfrom bridge_webhooks import SQLiteInbox, make_wsgi_app\n\napplication = make_wsgi_app(\n    SQLiteInbox(os.environ[\"WEBHOOK_INBOX_PATH\"]),\n    os.environ[\"BRIDGE_WEBHOOK_API_KEY\"],\n    os.environ[\"BRIDGE_WEBHOOK_CLIENT_SECRET\"],\n)\n```\n\nUse a durable database path for this single-service example. When you run multiple receiver replicas, use a shared store with an equivalent atomic uniqueness constraint. Enforce your service's request-size and network policies at the HTTP server, and terminate HTTPS there.\n\nThis example stops at durable acceptance. A worker must read rows with `processed=0`, perform the business action idempotently, and mark them processed only after success. A local acknowledgement is not proof that the business action completed.\n\n## Ordering and reconciliation\n\nDo not rely on a global delivery order. Different resource families and independent delivery attempts can interleave. Route on the published `eventType` and use the schema for that subscription.\n\nWhere a payload includes `sequence`, treat it as a resource/stream ordering hint, not a global counter shared by all workspaces or event families. Do not invent it for events whose schema has no sequence. Preserve source timestamps and identifiers, and avoid replacing newer state with an older notification. Where the REST API supports reading the resource, fetch current state when an event leaves the result uncertain; otherwise use the event-family guidance or contact support.\n\nDo not require a universal `version` field in every event envelope. For the supported event families in this webhook service, the configured subscription version selects that family's registered transformer and payload schema. Follow the documented schema for that family and subscription; do not infer a version property that its envelope does not publish.\n\n## Check the receiver before launch\n\nVerify the provided signature vector, a changed payload, a wrong API key, expired and future timestamps, duplicate delivery after a process restart, storage failure, and two distinct payloads sharing an ID. A re-timestamped replay of a previously signed payload demonstrates why the durable inbox is needed in addition to the timestamp check.\n\nUse Bridge's console test webhook to validate the connection and configured credentials after deploying your receiver. A successful test demonstrates the callback path; it does not establish delivery or ordering guarantees for every business event.\n"
    },
    {
      "title": "Configure webhooks",
      "source": "https://docs.bridge.new/webhooks/configuration",
      "markdownUrl": "https://docs.bridge.new/webhooks/configuration.md",
      "description": "Configure a callback URL, webhook credentials and event subscriptions in the Bridge Admin Console.",
      "kind": "page",
      "contentMarkdown": "You can manage all webhook settings directly from the [Bridge Admin Console](https://admin.bridge.new).  \nThis includes defining your webhook endpoint, authentication keys, and selecting which event types to subscribe to.\n\n---\n\n## Setting Up Webhooks\n\nFollow these steps to configure your webhook in the Bridge Admin Console:\n\n1. **Log in** to your Bridge Admin Console.\n2. Navigate to **Developers → Webhooks**.\n3. Provide the following details:\n   - **Webhook URL:** The full HTTPS URL of your endpoint.\n   - **API Key:** Used for identifying your workspace.\n   - **Client Secret:** Used to generate and verify webhook signatures (HMAC-SHA256).\n4. **Select Event Types** you want to subscribe to.\n\n---\n\n## Native CRM connectors and custom webhooks\n\nA native CRM connector and a custom webhook are separate delivery paths. For a connected CRM, check the connector's active configuration and the features shown in the console. This page does not establish that every event is synchronized, that no setup is required, or that updates have a fixed real-time deadline.\n\nConfigure a custom webhook when your service needs to receive an event and apply its own processing. You can subscribe to an event family that is also used by a native connector. If your receiver writes to the same CRM, coordinate the two paths to avoid applying the same business action twice.\n\nA custom webhook acknowledgement confirms your receiver's acceptance only. Use [durable acceptance and idempotent processing](https://docs.bridge.new/webhooks/receive-events.md); do not assume that two delivery paths provide exactly-once CRM updates.\n\n---\n\n## Event Subscription Options\n\nBridge currently supports the following event categories:\n\n### Conversation Summary Notifications\n\nv1\n\nReceive a notification once per day, per conversation, when Bridge AI generates the daily summary.\n\n- `conversation_summary.created`\n\n### Tasks Notifications\n\nv1\n\nReceive notifications when tasks are created or their state changes.\n\n- `task.created`\n- `task.status_changed`\n- `task.assigned`\n- `task.unassigned`\n- `task.remove_reminder`\n- `task.field_updated`\n\n### Contact Updates\n\nv1\n\nReceive notifications when contacts are created, updated, or deleted.\n\n- `contact.created`\n- `contact.updated`\n- `contact.deleted`\n\n### Call Notifications\n\nv1\n\nReceive a single webhook for each lifecycle phase of a call (created, ringing, started, finished) plus the AI transcript once it is ready. Calls are not deleted.\n\n- `call.created`\n- `call.updated`\n- `call.transcription.created`\n\n### Meeting Notifications\n\nv1\n\nReceive a notification when a meeting reaches the CREATED state. Meetings are not deleted, so only the `meeting.updated` event is emitted.\n\n- `meeting.updated`\n\n### Email Notifications\n\nv1\n\nReceive a notification once an email has been processed and the AI summary is available. Emails are not deleted, so only the `email.updated` event is emitted.\n\n- `email.updated`\n\n---\n\n## Verifying Your Configuration\n\nOnce you save your webhook configuration, Bridge automatically sends a **test webhook** to your endpoint to ensure it is reachable and correctly responding.\n\nYour endpoint must respond with:\n\n- **HTTP Status Code:** `200–299`\n- **Content-Type:** `application/json` (optional)\n- **Response Time:** acknowledge promptly after durable acceptance. The sender uses a default 10-second socket timeout per HTTP attempt; do not rely on a 30-second allowance. See [retry behavior](https://docs.bridge.new/webhooks/receive-events.md#retry-behavior).\n\n---\n\n## Managing Webhooks\n\nYou can manage your webhook configuration anytime from the console:\n\n| Action             | Description                                                         |\n| ------------------ | ------------------------------------------------------------------- |\n| **Edit**           | Update the URL, API key, or subscribed events                       |\n| **Disable Events** | Temporarily pause specific event types without deleting the webhook |\n| **Delete**         | Permanently remove a webhook configuration                          |\n\n---\n\n## Related Topics\n\n- [Overview](https://docs.bridge.new/webhooks.md)\n- [Verifying Signatures](https://docs.bridge.new/webhooks/verifying-signatures.md)\n- [Event Types](https://docs.bridge.new/webhooks/events.md)\n"
    },
    {
      "title": "Webhook events",
      "source": "https://docs.bridge.new/webhooks/events",
      "markdownUrl": "https://docs.bridge.new/webhooks/events.md",
      "description": "Event families, payload examples and subscription versions for Bridge webhooks.",
      "kind": "page",
      "contentMarkdown": "Bridge webhooks deliver structured event payloads in JSON format whenever specific actions occur in your workspace.  \nEach event follows a consistent schema to make parsing and validation simple.\n\n---\n\n## Common Event Structure\n\nThe following illustrates the common envelope; `\"string\"` is a type placeholder, not an event value to send. Read the family-specific payload examples below. Do not assume that `eventId` is globally unique across notification families.\n\n```json\n{\n  \"eventId\": \"string\",\n  \"eventType\": \"string\",\n  \"entity\": \"string\",\n  \"workspaceId\": \"string\",\n  \"timestamp\": \"string\",\n  \"payload\": {}\n}\n```\n\n---\n\n## Conversation Summary Notifications\n\nv1\n\nTriggered once per day, per conversation, when Bridge AI generates the daily summary. The summary captures the key points exchanged along with the internal and external participants involved.\n\n  \n\n### Event types\n\n    | Event Type                        | Description                                          |\n    | --------------------------------- | ---------------------------------------------------- |\n    | `conversation_summary.created`    | A daily conversation summary was generated           |\n  \n\n  \n\n### Example payload\n\n    ```json\n    {\n      \"eventId\": \"evt_summary_001\",\n      \"eventType\": \"conversation_summary.created\",\n      \"entity\": \"conversation_summary\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-05-19T05:03:15.910Z\",\n      \"payload\": {\n        \"conversationId\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n        \"summaryDate\": \"2026-05-19\",\n        \"summary\": \"- Advisor asked the customer about their preferred schedule and offered to book a product demo.\\n- Customer replied they are unavailable on Monday morning.\\n- Advisor offered alternative slots later in the week and is waiting for confirmation.\",\n        \"externalUsers\": [\n          { \"id\": \"c1d2e3f4-a5b6-7890-cdef-012345678901\", \"first_name\": \"Ana\", \"last_name\": \"Garcia\", \"cellphone\": \"+57300000000\" }\n        ],\n        \"internalUsers\": [\n          { \"id\": \"d4e5f6a7-b8c9-0123-defa-234567890123\", \"first_name\": \"Bob\", \"last_name\": \"Smith\", \"cellphone\": \"+57301000000\", \"email\": \"bob.smith@example.com\" }\n        ],\n        \"initMessageId\": \"f1e2d3c4-b5a6-7890-fedc-ba0987654321\",\n        \"endMessageId\": \"a9b8c7d6-e5f4-3210-abcd-ef9876543210\",\n        \"createdAt\": \"2026-05-19T05:03:15.910Z\",\n        \"updatedAt\": \"2026-05-19T05:03:15.910Z\"\n      }\n    }\n    ```\n  \n\n---\n\n## Task Updates\n\nv1\n\nTriggered when tasks are created or their state changes (status, assignment, reminders).\n\nPayloads vary by event type. The `details` field in the payload describes what changed for that specific event.\n\n  \n\n### Event types\n\n    | Event Type             | Description                                       |\n    | ---------------------- | ------------------------------------------------- |\n    | `task.created`         | A new task was created                            |\n    | `task.status_changed`  | The task status changed (for example, from OPEN to CLOSED) |\n    | `task.assigned`        | A user was assigned to the task                   |\n    | `task.unassigned`      | A user was unassigned from the task               |\n    | `task.remove_reminder` | A task reminder was removed                       |\n    | `task.field_updated`   | A task field was updated                          |\n  \n\n  \n\n### Example payloads\n\n    \n\n    ```json Created\n    {\n      \"eventId\": \"evt_123456789\",\n      \"eventType\": \"task.created\",\n      \"entity\": \"task\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-01-15T10:30:00.000Z\",\n      \"payload\": {\n        \"taskId\": \"task_123\",\n        \"actorType\": \"USER\",\n        \"actorId\": \"user_123\",\n        \"task\": {\n          \"title\": \"Sample task\",\n          \"description\": \"Example task description\",\n          \"type\": \"TASK\",\n          \"priority\": \"NORMAL\",\n          \"status\": \"OPEN\",\n          \"sourceType\": \"MANUAL\",\n          \"dueDate\": \"2026-01-20T00:00:00.000Z\",\n          \"recurrenceEnded\": false,\n          \"createdAt\": \"2026-01-15T10:30:00.000Z\",\n          \"updatedAt\": \"2026-01-15T10:30:00.000Z\"\n        },\n        \"assignees\": [\n          { \"assigneeId\": \"user_456\", \"assigneeType\": \"USER\" }\n        ],\n        \"conversations\": [\n          {\n            \"id\": \"conv_123\",\n            \"status\": \"ACTIVE\",\n            \"createdAt\": \"2026-01-14T09:10:00.000Z\",\n            \"updatedAt\": \"2026-01-14T09:10:00.000Z\"\n          }\n        ]\n      }\n    }\n    ```\n\n    ```json Assigned\n    {\n      \"eventId\": \"evt_123456790\",\n      \"eventType\": \"task.assigned\",\n      \"entity\": \"task\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-01-15T10:31:00.000Z\",\n      \"payload\": {\n        \"taskId\": \"task_123\",\n        \"actorType\": \"USER\",\n        \"actorId\": \"user_123\",\n        \"details\": { \"assignee\": { \"assigneeId\": \"user_456\" } }\n      }\n    }\n    ```\n\n    ```json Unassigned\n    {\n      \"eventId\": \"evt_123456791\",\n      \"eventType\": \"task.unassigned\",\n      \"entity\": \"task\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-01-15T10:32:00.000Z\",\n      \"payload\": {\n        \"taskId\": \"task_123\",\n        \"actorType\": \"USER\",\n        \"actorId\": \"user_123\",\n        \"details\": { \"assignee\": { \"assigneeId\": \"user_456\" } }\n      }\n    }\n    ```\n\n    ```json Status changed (open → closed)\n    {\n      \"eventId\": \"evt_123456792\",\n      \"eventType\": \"task.status_changed\",\n      \"entity\": \"task\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-01-15T10:33:00.000Z\",\n      \"payload\": {\n        \"taskId\": \"task_123\",\n        \"actorType\": \"USER\",\n        \"actorId\": \"user_123\",\n        \"details\": {\n          \"statusChange\": { \"fromStatus\": \"OPEN\", \"toStatus\": \"CLOSED\" }\n        }\n      }\n    }\n    ```\n\n    ```json Status changed (closed → open)\n    {\n      \"eventId\": \"evt_123456793\",\n      \"eventType\": \"task.status_changed\",\n      \"entity\": \"task\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-01-15T10:34:00.000Z\",\n      \"payload\": {\n        \"taskId\": \"task_123\",\n        \"actorType\": \"USER\",\n        \"actorId\": \"user_123\",\n        \"task\": {\n          \"title\": \"Sample task\",\n          \"description\": \"Example task description\",\n          \"type\": \"TASK\",\n          \"priority\": \"NORMAL\",\n          \"status\": \"OPEN\",\n          \"sourceType\": \"MANUAL\",\n          \"dueDate\": \"2026-01-20T00:00:00.000Z\",\n          \"recurrenceEnded\": false,\n          \"createdAt\": \"2026-01-15T10:30:00.000Z\",\n          \"updatedAt\": \"2026-01-15T10:34:00.000Z\"\n        },\n        \"conversations\": [\n          {\n            \"id\": \"conv_123\",\n            \"status\": \"ACTIVE\",\n            \"createdAt\": \"2026-01-14T09:10:00.000Z\",\n            \"updatedAt\": \"2026-01-14T09:10:00.000Z\"\n          }\n        ],\n        \"details\": {\n          \"statusChange\": { \"fromStatus\": \"CLOSED\", \"toStatus\": \"OPEN\" }\n        }\n      }\n    }\n    ```\n\n    ```json Reminder removed\n    {\n      \"eventId\": \"evt_123456794\",\n      \"eventType\": \"task.remove_reminder\",\n      \"entity\": \"task\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-01-15T10:35:00.000Z\",\n      \"payload\": {\n        \"taskId\": \"task_456\",\n        \"actorType\": \"USER\",\n        \"actorId\": \"user_123\",\n        \"details\": { \"reminder\": { \"reminderId\": \"rem_123\" } }\n      }\n    }\n    ```\n\n    ```json Field updated\n    {\n      \"eventId\": \"evt_123456795\",\n      \"eventType\": \"task.field_updated\",\n      \"entity\": \"task\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-01-15T10:36:00.000Z\",\n      \"payload\": {\n        \"taskId\": \"task_123\",\n        \"actorType\": \"USER\",\n        \"actorId\": \"user_123\",\n        \"task\": {\n          \"title\": \"Sample task\",\n          \"description\": \"Example updated description\",\n          \"type\": \"TASK\",\n          \"priority\": \"NORMAL\",\n          \"status\": \"OPEN\",\n          \"sourceType\": \"MANUAL\",\n          \"dueDate\": \"2026-01-20T00:00:00.000Z\",\n          \"recurrenceEnded\": false,\n          \"createdAt\": \"2026-01-15T10:30:00.000Z\",\n          \"updatedAt\": \"2026-01-15T10:36:00.000Z\"\n        },\n        \"details\": {\n          \"fieldUpdate\": { \"changedFields\": [\"description\"] }\n        }\n      }\n    }\n    ```\n\n    \n\n  \n\n---\n\n## Contact Updates\n\nv1\n\nTriggered when contact records are created, updated, or deleted.\n\n  \n\n### Event types\n\n    | Event Type        | Description                  |\n    | ----------------- | ---------------------------- |\n    | `contact.created` | A new contact was created    |\n    | `contact.updated` | A contact record was updated |\n    | `contact.deleted` | A contact record was deleted |\n  \n\n  \n\n### Example payloads\n\n    Ownership fields depend on whether the contact is linked to an external CRM:\n\n    - **CRM-synced contacts** carry `crmContactId` and `crmLastSyncedAt` (the contact's identity is owned by the CRM, not by an internal Bridge user).\n    - **Bridge-native contacts** (no CRM link) carry `internalUserOwnerId` instead — the Bridge user that owns the record.\n\n    These groups are mutually exclusive: a synced contact will not include `internalUserOwnerId`, and a non-synced contact will not include `crmContactId` or `crmLastSyncedAt`.\n\n    Each entry in `openAttributes` and `closedAttributes` includes a human-readable `name` alongside the `customAttributeId`. For closed attributes, `closedCustomAttributeIds` is an array of `{ id, content }` objects, where `content` is the selected option's display label.\n\n    \n\n    ```json CRM-synced\n    {\n      \"eventId\": \"evt_contact_001\",\n      \"eventType\": \"contact.updated\",\n      \"entity\": \"contact\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-01-15T10:30:00.000Z\",\n      \"payload\": {\n        \"id\": \"contact_123\",\n        \"firstName\": \"John\",\n        \"lastName\": \"Doe\",\n        \"email\": \"john@example.com\",\n        \"cellphone\": \"+1234567890\",\n        \"languageCode\": \"EN\",\n        \"openAttributes\": [\n          {\n            \"customAttributeId\": \"attr_open_1\",\n            \"name\": \"Account Number\",\n            \"content\": \"14\"\n          }\n        ],\n        \"closedAttributes\": [\n          {\n            \"customAttributeId\": \"attr_closed_1\",\n            \"name\": \"Preferred Language\",\n            \"closedCustomAttributeIds\": [\n              { \"id\": \"opt_1\", \"content\": \"ENGLISH\" }\n            ]\n          },\n          {\n            \"customAttributeId\": \"attr_closed_2\",\n            \"name\": \"Software Tools\",\n            \"closedCustomAttributeIds\": [\n              { \"id\": \"opt_2\", \"content\": \"Go\" },\n              { \"id\": \"opt_3\", \"content\": \"Postgres\" }\n            ]\n          }\n        ],\n        \"crmContactId\": \"crm_456\",\n        \"crmLastSyncedAt\": \"2026-01-10T09:00:00.000Z\",\n        \"createdAt\": \"2026-01-01T00:00:00.000Z\",\n        \"updatedAt\": \"2026-01-15T10:30:00.000Z\"\n      }\n    }\n    ```\n\n    ```json Bridge-native (no CRM)\n    {\n      \"eventId\": \"evt_contact_002\",\n      \"eventType\": \"contact.updated\",\n      \"entity\": \"contact\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-01-15T10:32:00.000Z\",\n      \"payload\": {\n        \"id\": \"contact_456\",\n        \"firstName\": \"Jane\",\n        \"lastName\": \"Smith\",\n        \"email\": \"jane@example.com\",\n        \"cellphone\": \"+1987654321\",\n        \"languageCode\": \"EN\",\n        \"openAttributes\": [\n          {\n            \"customAttributeId\": \"attr_open_2\",\n            \"name\": \"Education level\",\n            \"content\": \"Secondary\"\n          }\n        ],\n        \"closedAttributes\": [\n          {\n            \"customAttributeId\": \"attr_closed_1\",\n            \"name\": \"Preferred Language\",\n            \"closedCustomAttributeIds\": [\n              { \"id\": \"opt_1\", \"content\": \"ENGLISH\" }\n            ]\n          },\n          {\n            \"customAttributeId\": \"attr_closed_3\",\n            \"name\": \"Gender\",\n            \"closedCustomAttributeIds\": [\n              { \"id\": \"opt_4\", \"content\": \"FEMALE\" }\n            ]\n          }\n        ],\n        \"internalUserOwnerId\": \"user_789\",\n        \"createdAt\": \"2026-01-05T15:47:49.000Z\",\n        \"updatedAt\": \"2026-01-15T10:32:00.000Z\"\n      }\n    }\n    ```\n\n    \n\n  \n\n---\n\n## Call Notifications\n\nv1\n\nTriggered as a call moves through its lifecycle. Bridge emits at most one webhook per lifecycle phase per call, so your integration receives a clean four-phase timeline (created, ringing, started, finished) rather than every intermediate state transition Bridge writes internally.\n\nThe current phase is derivable from the call state and the participants' statuses:\n\n| Phase    | How to detect                                                            |\n| -------- | ------------------------------------------------------------------------ |\n| created  | state is CREATED and no participant has joined or is ringing             |\n| ringing  | a participant has status RINGING and nobody has joined yet               |\n| started  | state is STARTED and two participants are JOINED                         |\n| finished | state is FINISHED                                                        |\n\nMissed calls (nobody answered) skip the started phase — the integration receives created, ringing, and finished only.\n\nOnce a call finishes, Bridge runs the recording through an asynchronous AI transcription pipeline. When the transcript is ready, the integration receives an additional `call.transcription.created` event with the full transcript broken down into time-coded segments per speaker. This event arrives **after** the `call.updated (finished)` event, typically several seconds to a few minutes later depending on call length. Diarization (multi-speaker labelling) depends on the audio source quality — mono recordings may produce a single speaker label for the entire transcript.\n\n  \n\n### Event types\n\n    | Event Type                    | Description                                                 |\n    | ----------------------------- | ----------------------------------------------------------- |\n    | `call.created`                | A new call was provisioned                                  |\n    | `call.updated`                | A call lifecycle phase changed (ringing, started, finished) |\n    | `call.transcription.created`  | The AI transcript for a finished call is ready              |\n  \n\n  \n\n### Example payloads\n\n    \n\n    ```json Created\n    {\n      \"eventId\": \"evt_call_001\",\n      \"eventType\": \"call.created\",\n      \"entity\": \"call\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-05-04T00:48:05.815Z\",\n      \"payload\": {\n        \"callId\": \"call_abc123\",\n        \"conversationId\": \"conv_xyz\",\n        \"messageId\": \"msg_xyz\",\n        \"callType\": \"ONE_TO_ONE\",\n        \"channelProvider\": \"WHATSAPP\",\n        \"state\": \"CREATED\",\n        \"createdAt\": \"2026-05-04T00:48:05.815Z\",\n        \"updatedAt\": \"2026-05-04T00:48:05.815Z\",\n        \"participants\": {\n          \"internal\": [\n            { \"userId\": \"user_internal_1\", \"name\": \"Jane Doe\", \"email\": \"jane@example.com\", \"status\": \"INITIALIZED\" }\n          ],\n          \"external\": [\n            { \"userId\": \"user_external_1\", \"name\": \"John Smith\", \"email\": \"john@example.com\", \"status\": \"INITIALIZED\" }\n          ]\n        }\n      }\n    }\n    ```\n\n    ```json Ringing\n    {\n      \"eventId\": \"evt_call_002\",\n      \"eventType\": \"call.updated\",\n      \"entity\": \"call\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-05-04T00:48:06.243Z\",\n      \"payload\": {\n        \"callId\": \"call_abc123\",\n        \"conversationId\": \"conv_xyz\",\n        \"messageId\": \"msg_xyz\",\n        \"callType\": \"ONE_TO_ONE\",\n        \"channelProvider\": \"WHATSAPP\",\n        \"state\": \"CREATED\",\n        \"createdAt\": \"2026-05-04T00:48:05.815Z\",\n        \"updatedAt\": \"2026-05-04T00:48:06.243Z\",\n        \"participants\": {\n          \"internal\": [\n            { \"userId\": \"user_internal_1\", \"name\": \"Jane Doe\", \"email\": \"jane@example.com\", \"status\": \"INITIALIZED\" }\n          ],\n          \"external\": [\n            { \"userId\": \"user_external_1\", \"name\": \"John Smith\", \"email\": \"john@example.com\", \"status\": \"RINGING\" }\n          ]\n        }\n      }\n    }\n    ```\n\n    ```json Started\n    {\n      \"eventId\": \"evt_call_003\",\n      \"eventType\": \"call.updated\",\n      \"entity\": \"call\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-05-04T00:48:12.771Z\",\n      \"payload\": {\n        \"callId\": \"call_abc123\",\n        \"conversationId\": \"conv_xyz\",\n        \"messageId\": \"msg_xyz\",\n        \"callType\": \"ONE_TO_ONE\",\n        \"channelProvider\": \"WHATSAPP\",\n        \"state\": \"STARTED\",\n        \"startedAt\": \"2026-05-04T00:48:12.125Z\",\n        \"createdAt\": \"2026-05-04T00:48:05.815Z\",\n        \"updatedAt\": \"2026-05-04T00:48:12.771Z\",\n        \"participants\": {\n          \"internal\": [\n            { \"userId\": \"user_internal_1\", \"name\": \"Jane Doe\", \"email\": \"jane@example.com\", \"status\": \"JOINED\", \"joinedAt\": \"2026-05-04T00:48:12.125Z\" }\n          ],\n          \"external\": [\n            { \"userId\": \"user_external_1\", \"name\": \"John Smith\", \"email\": \"john@example.com\", \"status\": \"JOINED\", \"joinedAt\": \"2026-05-04T00:48:12.124Z\" }\n          ]\n        }\n      }\n    }\n    ```\n\n    ```json Finished\n    {\n      \"eventId\": \"evt_call_004\",\n      \"eventType\": \"call.updated\",\n      \"entity\": \"call\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-05-04T00:48:20.113Z\",\n      \"payload\": {\n        \"callId\": \"call_abc123\",\n        \"conversationId\": \"conv_xyz\",\n        \"messageId\": \"msg_xyz\",\n        \"callType\": \"ONE_TO_ONE\",\n        \"channelProvider\": \"WHATSAPP\",\n        \"state\": \"FINISHED\",\n        \"startedAt\": \"2026-05-04T00:48:12.125Z\",\n        \"finishedAt\": \"2026-05-04T00:48:19.389Z\",\n        \"createdAt\": \"2026-05-04T00:48:05.815Z\",\n        \"updatedAt\": \"2026-05-04T00:48:20.113Z\",\n        \"participants\": {\n          \"internal\": [\n            { \"userId\": \"user_internal_1\", \"name\": \"Jane Doe\", \"email\": \"jane@example.com\", \"status\": \"LEFT\", \"joinedAt\": \"2026-05-04T00:48:12.125Z\", \"leftAt\": \"2026-05-04T00:48:19.388Z\" }\n          ],\n          \"external\": [\n            { \"userId\": \"user_external_1\", \"name\": \"John Smith\", \"email\": \"john@example.com\", \"status\": \"LEFT\", \"joinedAt\": \"2026-05-04T00:48:12.124Z\", \"leftAt\": \"2026-05-04T00:48:19.387Z\" }\n          ]\n        }\n      }\n    }\n    ```\n\n    ```json Transcription created\n    {\n      \"eventId\": \"evt_call_005\",\n      \"eventType\": \"call.transcription.created\",\n      \"entity\": \"call\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-05-04T00:50:14.231Z\",\n      \"payload\": {\n        \"callId\": \"call_abc123\",\n        \"transcriptionId\": \"txn_xyz789\",\n        \"conversationId\": \"conv_xyz\",\n        \"duration\": \"27\",\n        \"startedAt\": \"2026-05-04T00:48:12.125Z\",\n        \"finishedAt\": \"2026-05-04T00:48:19.389Z\",\n        \"transcript\": [\n          { \"speaker\": \"0\", \"text\": \"Hi, how can I help you today?\", \"start\": 1.44, \"end\": 5.2 },\n          { \"speaker\": \"1\", \"text\": \"I have a question about your product.\", \"start\": 5.5, \"end\": 12.3 }\n        ]\n      }\n    }\n    ```\n\n    \n\n    Each transcript segment carries the diarized `speaker` label (typically `\"0\"`, `\"1\"`, ...), the `text`, and `start`/`end` offsets in seconds from the call start.\n  \n\n---\n\n## Meeting Notifications\n\nv1\n\nTriggered when a meeting reaches the CREATED state. Meeting cancellations are surfaced through the corresponding email notification.\n\n  \n\n### Event types\n\n    | Event Type        | Description                                |\n    | ----------------- | ------------------------------------------ |\n    | `meeting.updated` | A meeting reached the `CREATED` state      |\n  \n\n  \n\n### Example payload\n\n    ```json\n    {\n      \"eventId\": \"evt_meeting_001\",\n      \"eventType\": \"meeting.updated\",\n      \"entity\": \"meeting\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-05-04T00:46:40.954Z\",\n      \"payload\": {\n        \"meetingId\": \"meeting_abc123\",\n        \"conversationId\": \"conv_xyz\",\n        \"messageId\": \"msg_xyz\",\n        \"subject\": \"Meeting with John Smith\",\n        \"providerType\": \"TEAMS\",\n        \"status\": \"CREATED\",\n        \"organizerInternalUserId\": \"user_internal_1\",\n        \"internalParticipants\": [\"user_internal_1\"],\n        \"externalParticipants\": [\"user_external_1\"],\n        \"startDateTime\": \"2026-05-04T01:00:00.000Z\",\n        \"endDateTime\": \"2026-05-04T01:15:00.000Z\",\n        \"createdAt\": \"2026-05-04T00:46:29.944Z\",\n        \"updatedAt\": \"2026-05-04T00:46:40.954Z\",\n        \"provider\": {\n          \"joinUrl\": \"https://teams.microsoft.com/l/meetup-join/...\",\n          \"calendarEventId\": \"AAMkADhlMWVlMWJhLWUyZTMtNDllOC...\",\n          \"chatId\": \"MWM1Zjg4MmUtZjk0Mi00YjJkLTgyNTItNmRiODNiZmM1NWEw\",\n          \"providerMeetingId\": \"MSowODJkZmFjYy1iNDZmLTRiNDItOGYzOS0xMzI0NDcyMzUwMWUq...\"\n        }\n      }\n    }\n    ```\n  \n\n---\n\n## Email Notifications\n\nv1\n\nTriggered once an email has been processed and the AI summary is available.\n\nThe `sentFromBridge` field indicates direction: true for outbound emails sent from the workspace, false for inbound emails received.\n\n  \n\n### Event types\n\n    | Event Type      | Description                                          |\n    | --------------- | ---------------------------------------------------- |\n    | `email.updated` | An email reached the summarized state                |\n  \n\n  \n\n### Example payload\n\n    ```json\n    {\n      \"eventId\": \"evt_email_001\",\n      \"eventType\": \"email.updated\",\n      \"entity\": \"email\",\n      \"workspaceId\": \"ws_123\",\n      \"timestamp\": \"2026-05-04T00:45:31.261Z\",\n      \"payload\": {\n        \"emailId\": \"email_abc123\",\n        \"conversationId\": \"conv_xyz\",\n        \"threadId\": \"AAQkADhlMWVlMWJhLWUyZTMtNDllOC05MDg2LWMzNjFkMTI5MjM4MA...\",\n        \"providerType\": \"OUTLOOK\",\n        \"status\": \"PROCESSED\",\n        \"subject\": \"Project update\",\n        \"sender\": \"jane@example.com\",\n        \"recipients\": [\n          { \"recipientEmail\": \"john@example.com\" }\n        ],\n        \"summary\": \"Jane confirms the project is on track and shares next steps.\",\n        \"importance\": \"NORMAL\",\n        \"sentFromBridge\": true,\n        \"hasAttachments\": false,\n        \"sentAt\": \"2026-05-04T00:45:09.000Z\",\n        \"createdAt\": \"2026-05-04T00:45:09.000Z\",\n        \"updatedAt\": \"2026-05-04T00:45:31.261Z\"\n      }\n    }\n    ```\n  \n\n---\n\n## Notes\n\n- Bridge may add new event categories in the future.\n- Always check the event type field to determine which handler to execute.\n- Configure and support the version selected for each event subscription. The common envelope does not declare a universal `version` field; do not require or invent one. See [receiving events](https://docs.bridge.new/webhooks/receive-events.md) for duplicates and ordering.\n\n---\n\n## Related Topics\n\n- [Overview](https://docs.bridge.new/webhooks.md)\n- [Verifying Signatures](https://docs.bridge.new/webhooks/verifying-signatures.md)\n- [Console Configuration](https://docs.bridge.new/webhooks/configuration.md)\n"
    },
    {
      "title": "Custom Connections",
      "source": "https://docs.bridge.new/lead-capture/index",
      "markdownUrl": "https://docs.bridge.new/lead-capture/index.md",
      "description": "Use Custom Connections to register leads from your landing page and queue an opening WhatsApp message.",
      "kind": "page",
      "contentMarkdown": "## What this does\n\nYour landing page posts a lead to Bridge. Bridge then opens the WhatsApp conversation for\nyou, using an approved template. You do not send the message; we do.\n\n**Warning**\n\n  **A `202` response means the lead was accepted, not that a message was sent.** The first\n  WhatsApp message is transmitted later by a separate service. Your thank-you copy must say\n  something like \"we'll message you shortly\" — never \"we've messaged you\".\n\n## What you need, and where it comes from\n\nFour values: the endpoint URL, your `captureKey`, the reCAPTCHA site key, and the reCAPTCHA\naction name.\n\nYou do not assemble them by hand. In the Bridge admin console, under **LeadScout → Capture\nsnippet**, an administrator of your workspace generates an integration example that already\ncontains all four. They hand that block to whoever builds the page — that person never needs\nconsole access.\n\n**Note**\n\n  The example is a starting point, not a drop-in script. You own your form markup and your\n  consent wording; see [Consent requirements](https://docs.bridge.new/lead-capture/consent.md).\n\n## Where to go next\n\n  \n\n### [Consent requirements](https://docs.bridge.new/lead-capture/consent.md)\n\n    What your opt-in must say, and what you are attesting to.\n  \n\n  \n\n### [Template requirements](https://docs.bridge.new/lead-capture/templates.md)\n\n    Choose a supported Utility or Marketing template and understand the category requirements.\n  \n\n  \n\n### [Integration guide](https://docs.bridge.new/lead-capture/integration.md)\n\n    Add the form integration and handle CAPTCHA, consent, responses, and errors.\n  \n\n  \n\n### [Error reference](https://docs.bridge.new/lead-capture/errors.md)\n\n    Every response the endpoint can return, and which ones you can fix.\n"
    },
    {
      "title": "Consent requirements",
      "source": "https://docs.bridge.new/lead-capture/consent",
      "markdownUrl": "https://docs.bridge.new/lead-capture/consent.md",
      "description": "What your opt-in must say, and what you are attesting to when you send consent.status GRANTED.",
      "kind": "page",
      "contentMarkdown": "## What your form must carry\n\nYour landing page must ask for an explicit opt-in before you submit a lead. The wording is\nyours to write, but it has to do two things Meta requires:\n\n- **Name your business.** Not \"us\", not \"our partners\" — the business the person will hear\n  from.\n- **Say the person will receive WhatsApp communication from it.** Not \"we may contact you\";\n  name the channel.\n\nMeta lists a website form as a compliant way to collect this, so a checkbox on your landing\npage is enough — provided it says both of those things.\n\n**Warning**\n\n  A pre-ticked box is not an opt-in. Neither is a line of small print under a submit button\n  that the visitor never interacts with. The visitor has to take the action.\n\n## What you are attesting to\n\n**Note**\n\n  Sending `consent.status: \"GRANTED\"` is an attestation **you** are making. We do not and\n  cannot verify that the visitor ticked a box — we record that you told us they did, and\n  when. If your WhatsApp sending is ever restricted, that record is what is presented on\n  your behalf.\n\nThis is why `capturedAt` matters more than it looks. It must be the moment the visitor\nactually consented — not the moment your server got around to sending the request, and not a\ntimestamp generated during a nightly batch. If the two ever have to be defended, the gap\nbetween them is the first thing anyone will look at.\n\nSend it as an ISO 8601 timestamp:\n\n```json\n{\n  \"consent\": {\n    \"status\": \"GRANTED\",\n    \"capturedAt\": \"2026-08-26T14:32:07.184Z\"\n  }\n}\n```\n\nIf consent was not given, do not submit the lead. A submission with any status other than\n`GRANTED` is rejected with `BRIDGE.CAMPAIGN_LEADS.CONSENT_NOT_GRANTED`.\n\n## An example to adapt\n\nThis is illustrative, not boilerplate to paste. Only you know what you will send and why,\nand the wording has to match that.\n\n> ☐ I agree to receive WhatsApp messages from **Acme Motors** about the quote I just\n> requested.\n\nWhat makes it work: it names the business, it names the channel, and it says what the\nmessages will be about. What would break it: dropping the business name, saying \"messages\"\nwithout saying WhatsApp, or bundling the consent into a general terms-and-conditions\ncheckbox that also covers unrelated things.\n"
    },
    {
      "title": "Template requirements",
      "source": "https://docs.bridge.new/lead-capture/templates",
      "markdownUrl": "https://docs.bridge.new/lead-capture/templates.md",
      "description": "Supported template categories and guidelines for opening conversations through Custom Connections.",
      "kind": "page",
      "contentMarkdown": "## Supported template categories\n\nThe template that opens the conversation can be either a **utility** or a **marketing** template:\n\n- **Utility templates**: Strictly transactional, echoing what the visitor just submitted (such as confirming receipt of an enquiry). These qualify for lower WhatsApp conversation rates.\n- **Marketing templates**: May include promotional language, discounts, special offers, or direct sales pitches. These are billed at standard WhatsApp marketing conversation rates.\n\n**Note**\n\n  **Authentication** templates cannot be used with Custom Connections. Any template configured with the `AUTHENTICATION` category will be rejected.\n\nYou configure and submit your templates directly in Meta WhatsApp Manager, and getting them approved is a prerequisite for going live.\n\n## Meta classification and deliverability\n\n**Warning**\n\n  Meta classifies templates by their content, not strictly by the category you select during submission. If a template submitted as UTILITY contains promotional language, Meta may reclassify it as MARKETING or reject it.\n\nChoosing the right category upfront prevents unintended classification issues and ensures expected deliverability and billing.\n\n## Category examples\n\n**Utility (Transactional)** — acknowledges an enquiry without promotional offers:\n\n```text\nHi {{1}}, thanks for your enquiry about the {{2}}. A member of our team will follow up here shortly.\n```\n\n**Marketing (Promotional)** — includes promotional language, discounts, or sales offers:\n\n```text\nHi {{1}}, thanks for your enquiry about the {{2}}. Book this week and get 15% off — reply YES to claim.\n```\n\n## Practical guidance\n\n- **Choose Utility** when your primary goal is rapid transactional follow-up confirming the visitor's request. Keep it strictly focused on echoing their submission without promotional hooks to maintain the utility category and lower messaging rate.\n- **Choose Marketing** when you want to immediately provide introductory discounts, promotional incentives, or catalog offers upon capture.\n- **Avoid Authentication templates**: Authentication templates are intended exclusively for one-time passwords and verification codes, and are rejected for campaign lead dispatches.\n- Once the person replies, you are in a normal conversation session and template category constraints no longer apply to incoming or agent responses.\n"
    },
    {
      "title": "Integration guide",
      "source": "https://docs.bridge.new/lead-capture/integration",
      "markdownUrl": "https://docs.bridge.new/lead-capture/integration.md",
      "description": "Connect a landing-page form to Bridge: example, CAPTCHA, consent, responses, and error handling.",
      "kind": "page",
      "contentMarkdown": "## The example\n\nThe Bridge admin console supplies a generated integration example with your workspace's configuration. The expanded example below adds validation and submission feedback. Copy the configuration values from your generated example into the placeholders before running it.\n\nReplace the form markup and the consent wording with your own. **Acme Motors is a fictional business used in the example below.** Replace it with the business the visitor will hear from, and describe the WhatsApp messages they are agreeing to receive. See [Consent requirements](https://docs.bridge.new/lead-capture/consent.md). The script shows validation, submission feedback, and error handling. Keep the request fields and reCAPTCHA action from your generated configuration unchanged.\n\n```html\n<!--\n  Campaign leads integration example.\n  This is a starting point, not a drop-in script: replace the form fields, labels and\n  consent copy below with your own. Keep the existing field ids, or update the matching selectors. The API fields and\n  reCAPTCHA action remain the values from your generated configuration.\n  What this does: collects a phone number (and optionally a name), mints a reCAPTCHA v3\n  token, and POSTs the lead to the campaign-leads endpoint. A 202 response means the lead\n  was accepted and queued -- a separate service sends the first WhatsApp message later, so\n  do not tell the visitor \"message sent\"; something like \"we'll message you shortly\" is\n  accurate.\n-->\n<script>\n  window.__CAMPAIGN_LEADS__ = {\"workspaceId\":\"YOUR_WORKSPACE_ID\",\"captureKey\":\"YOUR_CAPTURE_KEY\",\"endpoint\":\"https://integrations-us.app.bridge.new/api/v1/leadscout/campaign-leads\",\"brandId\":null,\"recaptchaSiteKey\":\"YOUR_RECAPTCHA_SITE_KEY\",\"recaptchaAction\":\"submit_campaign_lead\",\"trackedParams\":[]};\n</script>\n<!-- ============================================================\n     EXAMPLE FORM -- replace with your own markup and consent copy.\n     Keep the element ids (or update the selectors in the script below to match yours).\n     ============================================================ -->\n<form id=\"lead-form\" novalidate>\n  <label>\n    <span>Name (optional)</span>\n    <input id=\"lead-name\" type=\"text\" autocomplete=\"name\" />\n  </label>\n  <label>\n    <span>WhatsApp number</span>\n    <input id=\"lead-phone\" type=\"tel\" autocomplete=\"tel\" required />\n  </label>\n  <!-- The wording of this consent statement is yours to write: it is what the visitor is\n       agreeing to, and only you know what you will message them and why. -->\n  <label>\n    <input id=\"lead-consent\" type=\"checkbox\" required />\n    <span>I agree to receive WhatsApp messages from Acme Motors about the quote I just requested.</span>\n  </label>\n  <button type=\"submit\">Submit</button>\n  <p id=\"lead-result\" role=\"status\" aria-live=\"polite\"></p>\n</form>\n<script>\n  (function (config) {\n    \"use strict\";\n    var form = document.getElementById(\"lead-form\");\n    var consentBox = document.getElementById(\"lead-consent\");\n    var submitButton = form.querySelector(\"button[type=submit]\");\n    var result = document.getElementById(\"lead-result\");\n    var consentCapturedAt = null;\n    var inFlight = false;\n    var accepted = false;\n    var submissionStarted = false;\n    var responseReceived = false;\n    consentBox.addEventListener(\"change\", function () {\n      consentCapturedAt = consentBox.checked ? new Date().toISOString() : null;\n    });\n    function showResult(message) {\n      result.textContent = message;\n    }\n    function loadRecaptcha() {\n      return new Promise(function (resolve, reject) {\n        if (window.grecaptcha) {\n          resolve();\n          return;\n        }\n        var tag = document.createElement(\"script\");\n        tag.src = \"https://www.google.com/recaptcha/api.js?render=\" + config.recaptchaSiteKey;\n        tag.onload = function () {\n          resolve();\n        };\n        tag.onerror = function () {\n          reject(new Error(\"Could not load reCAPTCHA. Check the site key and the network.\"));\n        };\n        document.head.appendChild(tag);\n      });\n    }\n    // Mint the token on submit, not on page load: a reCAPTCHA v3 token is only valid for\n    // ~2 minutes, so minting it earlier means it can already be expired by the time the\n    // visitor actually submits the form.\n    function mintToken() {\n      return loadRecaptcha().then(function () {\n        return new Promise(function (resolve, reject) {\n          window.grecaptcha.ready(function () {\n            window.grecaptcha\n              .execute(config.recaptchaSiteKey, { action: config.recaptchaAction })\n              .then(resolve, reject);\n          });\n        });\n      });\n    }\n    // Allowlist, not a denylist: only utm_* params and the names you seeded in\n    // trackedParams are collected. The rest of the query string can carry ad-network\n    // click ids or session tokens that were never meant to be stored -- over-collection\n    // here is irreversible, so when in doubt leave a param out.\n    function readCampaignParams() {\n      var params = new URLSearchParams(window.location.search);\n      var collected = {};\n      params.forEach(function (value, key) {\n        if (key.indexOf(\"utm_\") === 0 || config.trackedParams.indexOf(key) !== -1) {\n          collected[key] = value;\n        }\n      });\n      return collected;\n    }\n    function buildPayload(token) {\n      return {\n        workspaceId: config.workspaceId,\n        captureKey: config.captureKey,\n        cellphone: document.getElementById(\"lead-phone\").value.trim(),\n        name: document.getElementById(\"lead-name\").value.trim() || null,\n        brandId: config.brandId,\n        // origin + pathname only, never the full URL: the query string can carry\n        // click ids or session tokens, and UTMs already travel separately in \"utm\" above.\n        landingUri: window.location.origin + window.location.pathname,\n        utm: readCampaignParams(),\n        consent: { status: \"GRANTED\", capturedAt: consentCapturedAt },\n        recaptchaToken: token,\n      };\n    }\n    form.addEventListener(\"submit\", function (event) {\n      event.preventDefault();\n      if (inFlight || accepted || !form.reportValidity()) return;\n      if (!consentBox.checked || !consentCapturedAt) {\n        // A browser can restore a previously checked control without firing change.\n        // Ask for a new explicit action instead of inventing its consent timestamp.\n        consentBox.checked = false;\n        showResult(\"Please select the WhatsApp consent checkbox before submitting.\");\n        consentBox.focus();\n        return;\n      }\n      inFlight = true;\n      submissionStarted = false;\n      responseReceived = false;\n      submitButton.disabled = true;\n      showResult(\"Submitting your request...\");\n      // A token verifies once. Every manual attempt, including a retry after a\n      // rejected request, obtains a new token. This example never retries a POST.\n      mintToken()\n        .then(function (token) {\n          var payload = buildPayload(token);\n          // The URL must identify the workspace for the browser's CORS preflight.\n          var url = config.endpoint + \"?workspaceId=\" + encodeURIComponent(config.workspaceId);\n          submissionStarted = true;\n          return fetch(url, {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify(payload),\n          });\n        })\n        .then(function (response) {\n          responseReceived = true;\n          return response.json().catch(function () { return null; }).then(function (body) {\n            if (response.status === 202) {\n              accepted = true;\n              var reference = body && typeof body.requestId === \"string\"\n                ? \" Reference: \" + body.requestId\n                : \"\";\n              showResult(\"Request accepted. The business will process your request.\" + reference);\n              return;\n            }\n            var code = body && body.type ? body.type : \"HTTP \" + response.status;\n            if (response.status === 422 && code === \"BRIDGE.CAMPAIGN_LEADS.CAPTCHA_PROVIDER_UNAVAILABLE\") {\n              throw new Error(\"Verification is temporarily unavailable. You can try again; a new CAPTCHA token will be created.\");\n            }\n            if (response.status === 403 && code === \"BRIDGE.CAMPAIGN_LEADS.CAPTCHA_REJECTED\") {\n              throw new Error(\"The verification was rejected. You can try again with a new CAPTCHA token.\");\n            }\n            var detail = body && (body.detail || body.title);\n            throw new Error(code + (detail ? \": \" + detail : \". Check the form configuration.\"));\n          });\n        })\n        .catch(function (error) {\n          if (submissionStarted && !responseReceived) {\n            showResult(\"We could not confirm whether your request was accepted. Check with the business before submitting it again.\");\n            return;\n          }\n          showResult(error.message || \"The request could not be completed.\");\n        })\n        .finally(function () {\n          inFlight = false;\n          submitButton.disabled = accepted;\n        });\n    });\n  })(window.__CAMPAIGN_LEADS__);\n</script>\n```\n\n## What the example does\n\n- Checks the form's required fields before obtaining a CAPTCHA token.\n- Records `consent.capturedAt` when the visitor selects the checkbox. If the browser restores an old checked state, it asks for a fresh consent action.\n- Allows one submission at a time and keeps the button disabled after acceptance to avoid another submission from this form instance.\n- Shows a `202` as accepted and queued, and displays the returned `requestId` as a support reference. It never says a WhatsApp message was delivered.\n- Shows recoverable CAPTCHA failures and creates a fresh token on each manual attempt. It does not retry a POST automatically.\n- Treats a network failure after submission as an uncertain outcome; it does not claim that the lead was rejected or safe to resend.\n\nThis is client-side feedback, not a backend deduplication guarantee. Adapt the visible messages to your page and retain the [error reference](https://docs.bridge.new/lead-capture/errors.md) for diagnosis.\n\n## Your capture key is not an API key\n\n**Note**\n\n  If you have read the [Authentication](https://docs.bridge.new/credentials.md) page, it tells you never to put a key\n  in frontend code. That rule is about the Integrations API key, and it still holds. Your\n  `captureKey` is a different kind of value.\n\nUnlike the `x-api-key` used by the Integrations API, your `captureKey` is designed to live\nin your page's HTML where anyone can read it. It grants no read access to anything: it only\nidentifies which workspace a submission belongs to. Abuse is bounded by two other checks —\nthe reCAPTCHA token, and the fact that submissions are only accepted from the domains\nregistered for your workspace.\n\nNever use your Integrations API key here, and never put one in a page.\n\n## Put the workspace id on the URL\n\nThe workspace id goes in the request body **and** on the URL as `?workspaceId=...`. It has\nto be in both places: a browser sends a CORS preflight before the real request, and that\npreflight carries no body — so without the id on the URL we cannot tell which workspace you\nare, cannot return the header that authorises your domain, and the browser blocks the\nrequest before it is ever sent.\n\nThe failure this causes is confusing, which is why it is worth getting right the first\ntime: you will see a CORS error in the browser console and no request in our logs at all,\nbecause the real request never left the page.\n\n## Mint the captcha token when the form is submitted\n\nA reCAPTCHA v3 token is valid for about two minutes. Minting it when the page loads means\nit is often already expired by the time somebody finishes typing, and it comes back\nrejected — as a `403`, which looks like a permissions problem rather than a timing one.\n\n## Mint a new token for every attempt\n\nA token verifies exactly once. If a submission fails and you retry with the same token, the\nreplay is indistinguishable from an attack and returns the same error a bot would get.\nAlways call `grecaptcha.execute` again before retrying.\n\n## What to send\n\nThe [endpoint reference](https://docs.bridge.new/api-reference/campaign-leads/register-a-campaign-lead.md) carries the full contract.\nThese are the fields whose meaning is not obvious from the schema alone:\n\n### `brandId` (`string`)\n\n  The brand your landing page sells — not a line, channel or phone number id. Required only\n  if your workspace has brands enabled. **A line id is never required.**\n\n### `landingUri` (`string`)\n\n  The page's origin and path, without the query string. Your UTMs travel separately in\n  `utm`, and a raw query string often carries click ids or session tokens that should not be\n  stored. Send `https://example.com/offer`, not `https://example.com/offer?gclid=...`.\n\n### `consent` (`object`)\n\n  Your attestation that the visitor opted in, and when. See\n  [Consent requirements](https://docs.bridge.new/lead-capture/consent.md) — the wording on your form is not optional.\n\n### `utm` (`object`)\n\n  Collected from the query string against an allowlist: `utm_*` parameters plus any extra\n  names you configure in the console. This is an allowlist rather than a denylist on\n  purpose — over-collecting here is not reversible.\n\n## After you submit\n\nA `202` means the lead was accepted and queued. It does **not** mean a message was sent —\nthat happens later, in a separate service. Tell the visitor you will message them shortly.\n\nAnything else is an error. See the [error reference](https://docs.bridge.new/lead-capture/errors.md) for what each one\nmeans and whether it is yours to fix.\n"
    },
    {
      "title": "Error reference",
      "source": "https://docs.bridge.new/lead-capture/errors",
      "markdownUrl": "https://docs.bridge.new/lead-capture/errors.md",
      "description": "Every response the Custom Connections endpoint can return, and whether you can fix it.",
      "kind": "page",
      "contentMarkdown": "Error responses are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details: a\n`type`, a `title`, a `status`, and sometimes a `detail`.\n\n**Note**\n\n  **Error responses do not carry a request id.** Only a successful `202` does. If you need\n  to raise something with support, report the page URL and the approximate time instead.\n\n## Every response\n\n| Status | `type` | What happened | Can you fix it? |\n| --- | --- | --- | --- |\n| `400` | `invalid_payload` | A field is missing or malformed. | Yes |\n| `400` | `BRIDGE.CAMPAIGN_LEADS.INVALID_PHONE` | The phone number could not be read as a real number. | Yes — validate before submitting |\n| `400` | `BRIDGE.CAMPAIGN_LEADS.CONSENT_NOT_GRANTED` | `consent.status` was not `GRANTED`. | Yes |\n| `400` | `BRIDGE.CAMPAIGN_LEADS.BRAND_OR_LINE_REQUIRED` | Your workspace has brands enabled and no `brandId` was sent. | Yes |\n| `403` | `BRIDGE.CAMPAIGN_LEADS.CAPTURE_DISABLED` | The Custom Connections feature is not enabled for this workspace. | No — contact Bridge |\n| `403` | `BRIDGE.CAMPAIGN_LEADS.CAPTURE_KEY_MISMATCH` | The capture key is missing or wrong. | Yes |\n| `403` | `BRIDGE.CAMPAIGN_LEADS.CAPTCHA_REJECTED` | The captcha token was missing, expired, already used, or scored too low. | Sometimes — see below |\n| `403` | `BRIDGE.CAMPAIGN_LEADS.ORIGIN_NOT_ALLOWED` | The page's domain is not registered for this workspace. | No — contact Bridge |\n| `422` | `BRIDGE.CAMPAIGN_LEADS.CAPTCHA_PROVIDER_UNAVAILABLE` | We could not reach the captcha provider. | Retrying is safe |\n| `500` | `unexpected_error` | Something failed on our side. | No |\n\nOnly the first failure is reported. Checks run in this order: workspace and capture key,\nthen the phone number, then consent, then the brand, then the captcha and finally the\ndomain. Fixing one error can reveal the next.\n\n## About the captcha rejection\n\n`CAPTCHA_REJECTED` covers four different situations. Check the token handling in your integration before attributing a rejection to the score:\n\n- **The token expired.** It was minted too early. A reCAPTCHA v3 token lasts about two\n  minutes; mint it when the form is submitted, not when the page loads.\n- **The token was already used.** A token verifies once. If you retried a failed submission\n  without minting a new token, the replay is rejected.\n- **No token was sent.** Make sure your client obtains a token and includes it in `recaptchaToken` on every submission.\n- **The score was too low.** The anti-bot check rejected this attempt. Do not bypass it.\n\nAll four situations return the same code, so the response alone does not identify the cause. First check that the token is present, minted on submit, and fresh for every attempt.\n\n## About the 422\n\nThis is a transient failure on our side: we could not reach the captcha provider, so we\ncould not decide whether the submission was genuine.\n\n**Retrying is safe.** The lead is preserved when this happens, and a retry cannot produce a\nduplicate message. Mint a fresh captcha token for the retry, as you would for any other one.\n\n## About the domain error\n\nThe ordering here is genuinely surprising, so it is worth stating plainly.\n\nYour page's domain is checked against the domains registered for your workspace — but the\ndomain we check is the one the **captcha provider reports**, not the one in the request's\nown headers. That check happens *after* the token verifies.\n\nThe practical consequence: a page served from an unregistered domain fails at the captcha\nstage with `ORIGIN_NOT_ALLOWED`, not with a CORS error. If you are seeing this while your\nintegration otherwise works, the domain is missing from your workspace configuration —\ncontact Bridge to have it added.\n"
    },
    {
      "title": "Health check",
      "source": "https://docs.bridge.new/api-reference/health/health-check",
      "markdownUrl": "https://docs.bridge.new/api-reference/health/health-check.md",
      "description": "Health check endpoint to verify that the API is up and running.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/health`\n\n**operationId:** `health_bridge_api_health_get`\n\nHealth check endpoint to verify that the API is up and running.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Get conversation by id",
      "source": "https://docs.bridge.new/api-reference/conversations-api/get-conversation-by-id",
      "markdownUrl": "https://docs.bridge.new/api-reference/conversations-api/get-conversation-by-id.md",
      "description": "Get a specific conversation by its unique identifier within the workspace.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/conversations/{conversationId}`\n\n**operationId:** `get_conversation_by_id_bridge_api_v1_workspaces__workspaceId__conversations__conversationId__get`\n\nGet a specific conversation by its unique identifier within the workspace.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Update conversation",
      "source": "https://docs.bridge.new/api-reference/conversations-api/update-conversation",
      "markdownUrl": "https://docs.bridge.new/api-reference/conversations-api/update-conversation.md",
      "description": "Update conversation details including participants, status, and name.\n\nSupport partial updates - only include the fields you want to change.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `patch /bridge/api/v1/workspaces/{workspaceId}/conversations/{conversationId}`\n\n**operationId:** `update_conversation_bridge_api_v1_workspaces__workspaceId__conversations__conversationId__patch`\n\nUpdate conversation details including participants, status, and name.\n\nSupport partial updates - only include the fields you want to change.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Retrieve conversations with flexible filtering options",
      "source": "https://docs.bridge.new/api-reference/conversations-api/retrieve-conversations-with-flexible-filtering-options",
      "markdownUrl": "https://docs.bridge.new/api-reference/conversations-api/retrieve-conversations-with-flexible-filtering-options.md",
      "description": "Retrieve conversations with flexible filtering options.\n\nCan search by external user identifiers (ID, email, phone) or internal participants,\nwith support for pagination and status filtering.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/conversations`\n\n**operationId:** `get_conversations_bridge_api_v1_workspaces__workspaceId__conversations_get`\n\nRetrieve conversations with flexible filtering options.\n\nCan search by external user identifiers (ID, email, phone) or internal participants,\nwith support for pagination and status filtering.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Creates a new conversation with flexible user identification",
      "source": "https://docs.bridge.new/api-reference/conversations-api/creates-a-new-conversation-with-flexible-user-identification",
      "markdownUrl": "https://docs.bridge.new/api-reference/conversations-api/creates-a-new-conversation-with-flexible-user-identification.md",
      "description": "Create a new conversation with flexible user identification.\n\nExternal users can be identified by ID, email, or phone.\nInternal users are only assigned when explicitly specified; otherwise, conversations are\nauto-assigned by round-robin through a team or left open for any available internal user.\n\nWhen no line is free and the Contacts have a released conversation with exactly the requested participants,\nthat conversation is recovered and returned instead, keeping its `id` and history. The request's `name` and\n`status` are not applied to it.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `post /bridge/api/v1/workspaces/{workspaceId}/conversations`\n\n**operationId:** `create_conversation_bridge_api_v1_workspaces__workspaceId__conversations_post`\n\nCreate a new conversation with flexible user identification.\n\nExternal users can be identified by ID, email, or phone.\nInternal users are only assigned when explicitly specified; otherwise, conversations are\nauto-assigned by round-robin through a team or left open for any available internal user.\n\nWhen no line is free and the Contacts have a released conversation with exactly the requested participants,\nthat conversation is recovered and returned instead, keeping its `id` and history. The request's `name` and\n`status` are not applied to it.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Release conversation",
      "source": "https://docs.bridge.new/api-reference/conversations-api/release-conversation",
      "markdownUrl": "https://docs.bridge.new/api-reference/conversations-api/release-conversation.md",
      "description": "Release a conversation: mark as released, remove external participants, move to RELEASED section.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `post /bridge/api/v1/workspaces/{workspaceId}/conversations/{conversationId}/release`\n\n**operationId:** `release_conversation_bridge_api_v1_workspaces__workspaceId__conversations__conversationId__release_post`\n\nRelease a conversation: mark as released, remove external participants, move to RELEASED section.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Create a new message",
      "source": "https://docs.bridge.new/api-reference/messages-api/create-a-new-message",
      "markdownUrl": "https://docs.bridge.new/api-reference/messages-api/create-a-new-message.md",
      "description": "Create a new message within a specified conversation.\n\nFor a text message or an uploaded attachment, include all required fields: `conversationId`, `messageBody`, and `enableExternalBroadcast`. Attach uploaded files with their returned identifiers in `mediaIds`; see [Send an attachment](/guides/send-attachments). A successful response returns `messageId`; it does not contain a provider delivery receipt. See [Responses and retries](/guides/responses).",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `post /bridge/api/v1/workspaces/{workspaceId}/messages`\n\n**operationId:** `create_message_bridge_api_v1_workspaces__workspaceId__messages_post`\n\nCreate a new message within a specified conversation.\n\nFor a text message or an uploaded attachment, include all required fields: `conversationId`, `messageBody`, and `enableExternalBroadcast`. Attach uploaded files with their returned identifiers in `mediaIds`; see [Send an attachment](https://docs.bridge.new/guides/send-attachments.md). A successful response returns `messageId`; it does not contain a provider delivery receipt. See [Responses and retries](https://docs.bridge.new/guides/responses.md).\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Retrieve a contact by an identifier",
      "source": "https://docs.bridge.new/api-reference/contacts-api/retrieve-a-contact-by-an-identifier",
      "markdownUrl": "https://docs.bridge.new/api-reference/contacts-api/retrieve-a-contact-by-an-identifier.md",
      "description": "Get a specific contact by any of its identifiers within the workspace.\n\n`type=ID` returns the same contact as GET /contacts/{contactId}, which is the canonical\nURL for a contact and the one to prefer when the id is what you hold.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/contacts/lookup`\n\n**operationId:** `retrieve_contact_by_identifier_bridge_api_v1_workspaces__workspaceId__contacts_lookup_get`\n\nGet a specific contact by any of its identifiers within the workspace.\n\n`type=ID` returns the same contact as GET /contacts/{contactId}, which is the canonical\nURL for a contact and the one to prefer when the id is what you hold.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Retrieve a contact by id",
      "source": "https://docs.bridge.new/api-reference/contacts-api/retrieve-a-contact-by-id",
      "markdownUrl": "https://docs.bridge.new/api-reference/contacts-api/retrieve-a-contact-by-id.md",
      "description": "Get a specific contact by its unique identifier within the workspace.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/contacts/{contactId}`\n\n**operationId:** `retrieve_contact_by_id_bridge_api_v1_workspaces__workspaceId__contacts__contactId__get`\n\nGet a specific contact by its unique identifier within the workspace.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Update contact",
      "source": "https://docs.bridge.new/api-reference/contacts-api/update-contact",
      "markdownUrl": "https://docs.bridge.new/api-reference/contacts-api/update-contact.md",
      "description": "Update a contact in the specified workspace.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `patch /bridge/api/v1/workspaces/{workspaceId}/contacts/{contactId}`\n\n**operationId:** `update_contact_bridge_api_v1_workspaces__workspaceId__contacts__contactId__patch`\n\nUpdate a contact in the specified workspace.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "List contacts",
      "source": "https://docs.bridge.new/api-reference/contacts-api/list-contacts",
      "markdownUrl": "https://docs.bridge.new/api-reference/contacts-api/list-contacts.md",
      "description": "Retrieve a paginated list of contacts within the workspace.\n\nOrdered by creation date. The `ownerEmail` and `search` filters are deprecated and will be removed; to find one contact by an identifier, use /contacts/lookup.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/contacts`\n\n**operationId:** `get_contacts_bridge_api_v1_workspaces__workspaceId__contacts_get`\n\nRetrieve a paginated list of contacts within the workspace.\n\nOrdered by creation date. The `ownerEmail` and `search` filters are deprecated and will be removed; to find one contact by an identifier, use /contacts/lookup.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Create a new contact",
      "source": "https://docs.bridge.new/api-reference/contacts-api/create-a-new-contact",
      "markdownUrl": "https://docs.bridge.new/api-reference/contacts-api/create-a-new-contact.md",
      "description": "Create a new contact in the specified workspace.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `post /bridge/api/v1/workspaces/{workspaceId}/contacts`\n\n**operationId:** `create_contact_bridge_api_v1_workspaces__workspaceId__contacts_post`\n\nCreate a new contact in the specified workspace.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Create or update a contact",
      "source": "https://docs.bridge.new/api-reference/contacts-api/create-or-update-a-contact",
      "markdownUrl": "https://docs.bridge.new/api-reference/contacts-api/create-or-update-a-contact.md",
      "description": "Create or update a contact using only cellphone. Classifies as NORMAL or PARTIAL.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `post /bridge/api/v1/workspaces/{workspaceId}/contacts/partial`\n\n**operationId:** `create_partial_contact_bridge_api_v1_workspaces__workspaceId__contacts_partial_post`\n\nCreate or update a contact using only cellphone. Classifies as NORMAL or PARTIAL.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Create presigned uploads for files",
      "source": "https://docs.bridge.new/api-reference/files-api/create-presigned-uploads-for-files",
      "markdownUrl": "https://docs.bridge.new/api-reference/files-api/create-presigned-uploads-for-files.md",
      "description": "Upload an attachment, such as a PDF, image, or document, before creating a message. This operation returns presigned PUT data; the file bytes are uploaded separately. After the upload succeeds, use the returned `mediaId` in the sending operation. Follow [Send an attachment](/guides/send-attachments) for the checksum, upload, and message steps.\n\nGenerates presigned URLs to upload files (Presigned PUT). Validates extension, contentType, and type-specific limits. Checksum required: send SHA-256 digest in checksumBase64 (Base64). Upload must include header x-amz-checksum-sha256 with the same value.\n\n---\nTo upload your files, you MUST use the returned presigned URL with the HTTP PUT method. Ensure that you upload the file exactly to the URL indicated, without modifications (as required by AWS S3 Presigned URL PUT usage). Refer to the official AWS S3 documentation on Presigned PUT URLs for details: [AWS S3 Presigned PUT Documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html).\nBe sure to include all required headers as specified in the response (e.g., 'x-amz-checksum-sha256' with the correct SHA-256 value). If you fail to use the correct HTTP method or headers, AWS will reject the upload request.\n\n<table><tr><th>Type</th><th>Extension</th><th>Content Type</th><th>Max Size</th></tr><tr><td rowspan=\"4\">Image</td><td>.jpeg</td><td>image/jpeg</td><td rowspan=\"4\">16 MB</td></tr><tr><td>.jpg</td><td>image/jpeg</td></tr><tr><td>.png</td><td>image/png</td></tr><tr><td>.webp</td><td>image/webp</td></tr><tr><td rowspan=\"2\">GIF</td><td>.gif</td><td>image/gif</td><td rowspan=\"2\">3 MB</td></tr><tr><td>.mp4</td><td>video/mp4</td></tr><tr><td rowspan=\"3\">Video</td><td>.avi</td><td>video/x-msvideo</td><td rowspan=\"3\">64 MB</td></tr><tr><td>.mov</td><td>video/quicktime</td></tr><tr><td>.mp4</td><td>video/mp4</td></tr><tr><td rowspan=\"4\">Audio</td><td>.m4a</td><td>audio/mp4</td><td rowspan=\"4\">16 MB</td></tr><tr><td>.mp3</td><td>audio/mpeg</td></tr><tr><td>.ogg</td><td>audio/ogg</td></tr><tr><td>.wav</td><td>audio/wav</td></tr><tr><td rowspan=\"8\">Document</td><td>.doc</td><td>application/msword</td><td rowspan=\"8\">100 MB</td></tr><tr><td>.docx</td><td>application/vnd.openxmlformats-officedocument.wordprocessingml.document</td></tr><tr><td>.pdf</td><td>application/pdf</td></tr><tr><td>.ppt</td><td>application/vnd.ms-powerpoint</td></tr><tr><td>.pptx</td><td>application/vnd.openxmlformats-officedocument.presentationml.presentation</td></tr><tr><td>.txt</td><td>text/plain</td></tr><tr><td>.xls</td><td>application/vnd.ms-excel</td></tr><tr><td>.xlsx</td><td>application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</td></tr></table>",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `post /bridge/api/v1/workspaces/{workspaceId}/files`\n\n**operationId:** `create_preloaded_files_bridge_api_v1_workspaces__workspaceId__files_post`\n\nUpload an attachment, such as a PDF, image, or document, before creating a message. This operation returns presigned PUT data; the file bytes are uploaded separately. After the upload succeeds, use the returned `mediaId` in the sending operation. Follow [Send an attachment](https://docs.bridge.new/guides/send-attachments.md) for the checksum, upload, and message steps.\n\nGenerates presigned URLs to upload files (Presigned PUT). Validates extension, contentType, and type-specific limits. Checksum required: send SHA-256 digest in checksumBase64 (Base64). Upload must include header x-amz-checksum-sha256 with the same value.\n\n---\nTo upload your files, you MUST use the returned presigned URL with the HTTP PUT method. Ensure that you upload the file exactly to the URL indicated, without modifications (as required by AWS S3 Presigned URL PUT usage). Refer to the official AWS S3 documentation on Presigned PUT URLs for details: [AWS S3 Presigned PUT Documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html).\nBe sure to include all required headers as specified in the response (e.g., 'x-amz-checksum-sha256' with the correct SHA-256 value). If you fail to use the correct HTTP method or headers, AWS will reject the upload request.\n\n<table><tr><th>Type</th><th>Extension</th><th>Content Type</th><th>Max Size</th></tr><tr><td rowspan=\"4\">Image</td><td>.jpeg</td><td>image/jpeg</td><td rowspan=\"4\">16 MB</td></tr><tr><td>.jpg</td><td>image/jpeg</td></tr><tr><td>.png</td><td>image/png</td></tr><tr><td>.webp</td><td>image/webp</td></tr><tr><td rowspan=\"2\">GIF</td><td>.gif</td><td>image/gif</td><td rowspan=\"2\">3 MB</td></tr><tr><td>.mp4</td><td>video/mp4</td></tr><tr><td rowspan=\"3\">Video</td><td>.avi</td><td>video/x-msvideo</td><td rowspan=\"3\">64 MB</td></tr><tr><td>.mov</td><td>video/quicktime</td></tr><tr><td>.mp4</td><td>video/mp4</td></tr><tr><td rowspan=\"4\">Audio</td><td>.m4a</td><td>audio/mp4</td><td rowspan=\"4\">16 MB</td></tr><tr><td>.mp3</td><td>audio/mpeg</td></tr><tr><td>.ogg</td><td>audio/ogg</td></tr><tr><td>.wav</td><td>audio/wav</td></tr><tr><td rowspan=\"8\">Document</td><td>.doc</td><td>application/msword</td><td rowspan=\"8\">100 MB</td></tr><tr><td>.docx</td><td>application/vnd.openxmlformats-officedocument.wordprocessingml.document</td></tr><tr><td>.pdf</td><td>application/pdf</td></tr><tr><td>.ppt</td><td>application/vnd.ms-powerpoint</td></tr><tr><td>.pptx</td><td>application/vnd.openxmlformats-officedocument.presentationml.presentation</td></tr><tr><td>.txt</td><td>text/plain</td></tr><tr><td>.xls</td><td>application/vnd.ms-excel</td></tr><tr><td>.xlsx</td><td>application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</td></tr></table>\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Retrieve custom attributes by workspaceid",
      "source": "https://docs.bridge.new/api-reference/custom-attributes-api/retrieve-custom-attributes-by-workspaceid",
      "markdownUrl": "https://docs.bridge.new/api-reference/custom-attributes-api/retrieve-custom-attributes-by-workspaceid.md",
      "description": "Return the custom attributes for the specified workspace.\n\nBy default, only **active** attributes are returned (those without a past `deletedAt`).\n\n**Filters:**\n- `category` — Filter by `OPEN` or `CLOSED`. Omit to return all categories.\n- `includeArchived` — Set to `true` to include archived attributes\n  (those with a past `deletedAt`). Default: `false`.\n- `isRequired` — Set to `true` to return only required attributes,\n  `false` for optional only. Omit to return all.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/settings/attributes`\n\n**operationId:** `get_custom_attributes_by_workspace_bridge_api_v1_workspaces__workspaceId__settings_attributes_get`\n\nReturn the custom attributes for the specified workspace.\n\nBy default, only **active** attributes are returned (those without a past `deletedAt`).\n\n**Filters:**\n- `category` — Filter by `OPEN` or `CLOSED`. Omit to return all categories.\n- `includeArchived` — Set to `true` to include archived attributes\n  (those with a past `deletedAt`). Default: `false`.\n- `isRequired` — Set to `true` to return only required attributes,\n  `false` for optional only. Omit to return all.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Get provider channels by workspace id",
      "source": "https://docs.bridge.new/api-reference/provider-channels-api/get-provider-channels-by-workspace-id",
      "markdownUrl": "https://docs.bridge.new/api-reference/provider-channels-api/get-provider-channels-by-workspace-id.md",
      "description": "Get provider channels by workspace identifier.\n\nThis response exposes each channel's `provider` and phone-like `identifier`; it does not return a `providerChannelId`. For an Agent's lines, read `agents[].lines[].providerChannelId` from the Agents list. Do not substitute a phone number for a channel ID. See [Concepts and IDs](/guides/identifiers).",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/provider-channels`\n\n**operationId:** `get_provider_channels_by_workspace_id_bridge_api_v1_workspaces__workspaceId__provider_channels_get`\n\nGet provider channels by workspace identifier.\n\nThis response exposes each channel's `provider` and phone-like `identifier`; it does not return a `providerChannelId`. For an Agent's lines, read `agents[].lines[].providerChannelId` from the Agents list. Do not substitute a phone number for a channel ID. See [Concepts and IDs](https://docs.bridge.new/guides/identifiers.md).\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Get provider channels by workspace id and provider type",
      "source": "https://docs.bridge.new/api-reference/provider-channels-api/get-provider-channels-by-workspace-id-and-provider-type",
      "markdownUrl": "https://docs.bridge.new/api-reference/provider-channels-api/get-provider-channels-by-workspace-id-and-provider-type.md",
      "description": "Get provider channels by workspace identifier and provider type.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/provider-channels/{messageProviderType}`\n\n**operationId:** `get_provider_channels_by_workspace_id_and_provider_type_bridge_api_v1_workspaces__workspaceId__provider_channels__messageProviderType__get`\n\nGet provider channels by workspace identifier and provider type.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Initiate a leadscout outbound conversation with a known lead",
      "source": "https://docs.bridge.new/api-reference/leadscout-api/initiate-a-leadscout-outbound-conversation-with-a-known-lead",
      "markdownUrl": "https://docs.bridge.new/api-reference/leadscout-api/initiate-a-leadscout-outbound-conversation-with-a-known-lead.md",
      "description": "Start a LeadScout outbound conversation with a known Contact. The Contact must already exist. Bridge resolves a prospecting Agent for an eligible WhatsApp line. The template is referenced by template.id or template.name; the id wins when both are supplied. It must exist and be APPROVED, and any handoffOverride Bridge User must be valid.\n\nAn explicit contact.providerChannelId is respected. Without one, automatic selection prefers a free line, then a released recoverable conversation with this Contact, then an occupied active conversation only when forceSend=true. Availability is per Contact on each line, not global line occupancy.\n\nforceSend=true reuses an eligible active conversation; it does not ignore it or create a fresh one. A successful reuse returns alreadyInitiated=true and persists the new template and follow-ups. A recoverable released conversation can be recovered with either value, retains its ID, and returns recovered=true. Blocked and released nonrecoverable conversations are not made usable by this flag. The legacy route can reuse a conversation held by a human or another Agent; unlike Agents outbound, it does not enforce the requested-Agent controller check.\n\nA nonexpired open LeadScout session still blocks a new outbound request with 409 BRIDGE_AI_LEADSCOUT_0017; inability to finalize an expired session returns 409 BRIDGE_AI_LEADSCOUT_0018. Other validation still applies. Additional messages are persisted; persistence does not prove delivery or a wait for the lead to reply. A failed request can leave partial work, so do not treat forceSend as idempotency or blindly retry an uncertain result.\n\nSee [the complete outbound guide](/guides/agent-outbound) for the false/true matrix, response flags, differences from Agents outbound, errors and handoff.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `post /bridge/api/v1/workspaces/{workspaceId}/leadscout/outbound`\n\n**operationId:** `initiate_leadscout_outbound_bridge_api_v1_workspaces__workspaceId__leadscout_outbound_post`\n\nStart a LeadScout outbound conversation with a known Contact. The Contact must already exist. Bridge resolves a prospecting Agent for an eligible WhatsApp line. The template is referenced by template.id or template.name; the id wins when both are supplied. It must exist and be APPROVED, and any handoffOverride Bridge User must be valid.\n\nAn explicit contact.providerChannelId is respected. Without one, automatic selection prefers a free line, then a released recoverable conversation with this Contact, then an occupied active conversation only when forceSend=true. Availability is per Contact on each line, not global line occupancy.\n\nforceSend=true reuses an eligible active conversation; it does not ignore it or create a fresh one. A successful reuse returns alreadyInitiated=true and persists the new template and follow-ups. A recoverable released conversation can be recovered with either value, retains its ID, and returns recovered=true. Blocked and released nonrecoverable conversations are not made usable by this flag. The legacy route can reuse a conversation held by a human or another Agent; unlike Agents outbound, it does not enforce the requested-Agent controller check.\n\nA nonexpired open LeadScout session still blocks a new outbound request with 409 BRIDGE_AI_LEADSCOUT_0017; inability to finalize an expired session returns 409 BRIDGE_AI_LEADSCOUT_0018. Other validation still applies. Additional messages are persisted; persistence does not prove delivery or a wait for the lead to reply. A failed request can leave partial work, so do not treat forceSend as idempotency or blindly retry an uncertain result.\n\nSee [the complete outbound guide](https://docs.bridge.new/guides/agent-outbound.md) for the false/true matrix, response flags, differences from Agents outbound, errors and handoff.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Request a manual handoff of a leadscout conversation to internal users",
      "source": "https://docs.bridge.new/api-reference/leadscout-api/request-a-manual-handoff-of-a-leadscout-conversation-to-internal-users",
      "markdownUrl": "https://docs.bridge.new/api-reference/leadscout-api/request-a-manual-handoff-of-a-leadscout-conversation-to-internal-users.md",
      "description": "Request a manual handoff of a LeadScout-controlled conversation to one or more internal users.\n\nThe request is acknowledged with `202 Accepted`; assignment is processed asynchronously.\nThe response echoes the requested assignees and a tracking id for correlation. Internal-user\nresolution (by ID or EMAIL) happens downstream and is not reflected in this response.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `post /bridge/api/v1/workspaces/{workspaceId}/leadscout/conversations/{conversationId}/handoff`\n\n**operationId:** `request_leadscout_handoff_bridge_api_v1_workspaces__workspaceId__leadscout_conversations__conversationId__handoff_post`\n\nRequest a manual handoff of a LeadScout-controlled conversation to one or more internal users.\n\nThe request is acknowledged with `202 Accepted`; assignment is processed asynchronously.\nThe response echoes the requested assignees and a tracking id for correlation. Internal-user\nresolution (by ID or EMAIL) happens downstream and is not reflected in this response.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Discover a templates agnostic variable inventory",
      "source": "https://docs.bridge.new/api-reference/templates-api/discover-a-templates-agnostic-variable-inventory",
      "markdownUrl": "https://docs.bridge.new/api-reference/templates-api/discover-a-templates-agnostic-variable-inventory.md",
      "description": "Return a template's variables — agnostic `key`/`label`/`callerMustSupply`/`isCustom`/`default`.\n\nNever a Meta slot address or index.\n\n`templateId` accepts either the template's `id` or its `name`. Outbound operations accept `template.id` or `template.name`, with a nonblank id taking precedence. Conversation onboarding uses `onboardingTemplates[].templateId`. Use the selector required by the sending operation. See [the template recipe](/guides/send-template).\n\nA variable with `callerMustSupply: true` has no declared source — the caller must include\nits `key` in `parameters` when sending, or the send fails with a 422 naming that same `key`.\nA variable with `callerMustSupply: false` is either resolved by Bridge on its own (e.g. the\nrecipient's first name) or already has a value the caller may optionally override.\nA variable with `isCustom: true` is a value only the caller knows, with a `default`: send it in\n`parameters` to use your own value, or leave it out (or blank) and `default` goes out.\n\nError codes:\n- BRIDGE_CORE_0301: workspace in the path does not match the caller's own workspace (403)\n- BRIDGE_TEMPLATE_0001: no PROVIDER_TEMPLATE found for this workspace and identifier — covers\n  an unknown id/name, a template that belongs to another workspace, and a template that is\n  not a PROVIDER_TEMPLATE, identically (404); this endpoint never confirms the existence of\n  a template outside the caller's own workspace",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/templates/{templateId}`\n\n**operationId:** `get_template_discovery_bridge_api_v1_workspaces__workspaceId__templates__templateId__get`\n\nReturn a template's variables — agnostic `key`/`label`/`callerMustSupply`/`isCustom`/`default`.\n\nNever a Meta slot address or index.\n\n`templateId` accepts either the template's `id` or its `name`. Outbound operations accept `template.id` or `template.name`, with a nonblank id taking precedence. Conversation onboarding uses `onboardingTemplates[].templateId`. Use the selector required by the sending operation. See [the template recipe](https://docs.bridge.new/guides/send-template.md).\n\nA variable with `callerMustSupply: true` has no declared source — the caller must include\nits `key` in `parameters` when sending, or the send fails with a 422 naming that same `key`.\nA variable with `callerMustSupply: false` is either resolved by Bridge on its own (e.g. the\nrecipient's first name) or already has a value the caller may optionally override.\nA variable with `isCustom: true` is a value only the caller knows, with a `default`: send it in\n`parameters` to use your own value, or leave it out (or blank) and `default` goes out.\n\nError codes:\n- BRIDGE_CORE_0301: workspace in the path does not match the caller's own workspace (403)\n- BRIDGE_TEMPLATE_0001: no PROVIDER_TEMPLATE found for this workspace and identifier — covers\n  an unknown id/name, a template that belongs to another workspace, and a template that is\n  not a PROVIDER_TEMPLATE, identically (404); this endpoint never confirms the existence of\n  a template outside the caller's own workspace\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Retrieve an internal user by an identifier",
      "source": "https://docs.bridge.new/api-reference/internal-users-api/retrieve-an-internal-user-by-an-identifier",
      "markdownUrl": "https://docs.bridge.new/api-reference/internal-users-api/retrieve-an-internal-user-by-an-identifier.md",
      "description": "Get a specific Bridge User by any of its identifiers within the workspace.\n\n`type=ID` returns the same user as GET /users/{userId}, which is the canonical URL for an\ninternal user and the one to prefer when the id is what you hold.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/users/lookup`\n\n**operationId:** `retrieve_internal_user_by_identifier_bridge_api_v1_workspaces__workspaceId__users_lookup_get`\n\nGet a specific Bridge User by any of its identifiers within the workspace.\n\n`type=ID` returns the same user as GET /users/{userId}, which is the canonical URL for an\ninternal user and the one to prefer when the id is what you hold.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Retrieve an internal user by id",
      "source": "https://docs.bridge.new/api-reference/internal-users-api/retrieve-an-internal-user-by-id",
      "markdownUrl": "https://docs.bridge.new/api-reference/internal-users-api/retrieve-an-internal-user-by-id.md",
      "description": "Get a specific internal user (human agent) by its unique identifier within the workspace.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/users/{userId}`\n\n**operationId:** `retrieve_internal_user_by_id_bridge_api_v1_workspaces__workspaceId__users__userId__get`\n\nGet a specific internal user (human agent) by its unique identifier within the workspace.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "List internal users",
      "source": "https://docs.bridge.new/api-reference/internal-users-api/list-internal-users",
      "markdownUrl": "https://docs.bridge.new/api-reference/internal-users-api/list-internal-users.md",
      "description": "Retrieve a paginated list of internal users within the workspace.\n\nOrdered by creation date. The `email` and `status` filters are deprecated and will be removed; to find one internal user by an identifier, use /users/lookup.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/users`\n\n**operationId:** `get_internal_users_bridge_api_v1_workspaces__workspaceId__users_get`\n\nRetrieve a paginated list of internal users within the workspace.\n\nOrdered by creation date. The `email` and `status` filters are deprecated and will be removed; to find one internal user by an identifier, use /users/lookup.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "List the agents of a workspace",
      "source": "https://docs.bridge.new/api-reference/agents-api/list-the-agents-of-a-workspace",
      "markdownUrl": "https://docs.bridge.new/api-reference/agents-api/list-the-agents-of-a-workspace.md",
      "description": "List the workspace's Agents with their status, availability and lines.\n\nEvery Agent is listed whatever its status; `available` tells whether it can start conversations.\nA page can hold fewer than `limit` items; keep paging while `cursor` is not null.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `get /bridge/api/v1/workspaces/{workspaceId}/agents`\n\n**operationId:** `list_agents_bridge_api_v1_workspaces__workspaceId__agents_get`\n\nList the workspace's Agents with their status, availability and lines.\n\nEvery Agent is listed whatever its status; `available` tells whether it can start conversations.\nA page can hold fewer than `limit` items; keep paging while `cursor` is not null.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Start an outbound conversation with a contact as a given agent",
      "source": "https://docs.bridge.new/api-reference/agents-api/start-an-outbound-conversation-with-a-contact-as-a-given-agent",
      "markdownUrl": "https://docs.bridge.new/api-reference/agents-api/start-an-outbound-conversation-with-a-contact-as-a-given-agent.md",
      "description": "Start an outbound conversation with a known Contact, answered by the given Agent.\n\nWorks for LeadScout and Service Agents. The Contact is resolved first, then the Agent, which must exist in\nthe workspace and be available (see `GET /agents`). The template is referenced by `template.id` or\n`template.name` (the id wins when both are sent), must exist and be APPROVED, and a\n`handoffOverride` Bridge User must be valid. If `template.media.mediaId` is unknown, the request fails\nwith 400 `BRIDGE_AI_LEADSCOUT_0016` only after the conversation was created or recovered, so a retry after\nthat error may answer differently. Body rules (`consent.status=GRANTED` without `capturedAt`, `template.media`\nwithout exactly one of `mediaId` or `url`) fail with 400 `BRIDGE_CORE_0103`.\n\nThe conversation opens on one of the Agent's active WhatsApp lines, controlled by the Agent, with the template\nas its first message. A line is picked in this order:\n- A free line.\n- Otherwise, a line holding a released conversation with this Contact that can be recovered. Recovering\n  returns `recovered=true` and replaces the previous outbound context and `agentContext`. This happens even\n  with `forceSend=false`.\n- Otherwise, when no line is free or recoverable: `forceSend=false` (default) fails with 409\n  `BRIDGE_AI_LEADSCOUT_0008`; `forceSend=true` reuses a line occupied by an active conversation with this\n  Contact, preferring one this Agent controls. If the chosen conversation is held by another Agent or a\n  Bridge User, the call fails with 409 `BRIDGE_AI_AGENT_OUTBOUND_0006` and nothing is written. Release it\n  first, then retry. Lines whose conversation is blocked or was released without recovery are never used, so\n  if no line is occupied by an active conversation the call fails with 409 `BRIDGE_AI_LEADSCOUT_0008` even\n  with `forceSend=true`.\n\n`contact.providerChannelId` is a preference: the pinned line is used when it is free or recoverable;\notherwise the same order applies over all the Agent's lines. Among occupied lines, one this Agent controls\ncomes first; if none does, the pinned line is the one reused (and may fail with 0006).\n\nReusing a conversation depends on the Agent type:\n- LeadScout: if its LeadScout session is still open and not expired, the call fails with 409\n  `BRIDGE_AI_LEADSCOUT_0017` (`extra` carries `conversationId`, `sessionId` and `stage`). If an expired\n  session cannot be finalized, it fails with 409 `BRIDGE_AI_LEADSCOUT_0018`.\n- Service Agent: an open, unexpired service session is continued; an expired one is closed as abandoned and a\n  new one opens.\n\nSee [the complete outbound guide](/guides/agent-outbound) for prerequisites, the forceSend decision table, recovery, partial work and safe retry decisions.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `post /bridge/api/v1/workspaces/{workspaceId}/agents/{agentId}/outbound`\n\n**operationId:** `start_agent_outbound_bridge_api_v1_workspaces__workspaceId__agents__agentId__outbound_post`\n\nStart an outbound conversation with a known Contact, answered by the given Agent.\n\nWorks for LeadScout and Service Agents. The Contact is resolved first, then the Agent, which must exist in\nthe workspace and be available (see `GET /agents`). The template is referenced by `template.id` or\n`template.name` (the id wins when both are sent), must exist and be APPROVED, and a\n`handoffOverride` Bridge User must be valid. If `template.media.mediaId` is unknown, the request fails\nwith 400 `BRIDGE_AI_LEADSCOUT_0016` only after the conversation was created or recovered, so a retry after\nthat error may answer differently. Body rules (`consent.status=GRANTED` without `capturedAt`, `template.media`\nwithout exactly one of `mediaId` or `url`) fail with 400 `BRIDGE_CORE_0103`.\n\nThe conversation opens on one of the Agent's active WhatsApp lines, controlled by the Agent, with the template\nas its first message. A line is picked in this order:\n- A free line.\n- Otherwise, a line holding a released conversation with this Contact that can be recovered. Recovering\n  returns `recovered=true` and replaces the previous outbound context and `agentContext`. This happens even\n  with `forceSend=false`.\n- Otherwise, when no line is free or recoverable: `forceSend=false` (default) fails with 409\n  `BRIDGE_AI_LEADSCOUT_0008`; `forceSend=true` reuses a line occupied by an active conversation with this\n  Contact, preferring one this Agent controls. If the chosen conversation is held by another Agent or a\n  Bridge User, the call fails with 409 `BRIDGE_AI_AGENT_OUTBOUND_0006` and nothing is written. Release it\n  first, then retry. Lines whose conversation is blocked or was released without recovery are never used, so\n  if no line is occupied by an active conversation the call fails with 409 `BRIDGE_AI_LEADSCOUT_0008` even\n  with `forceSend=true`.\n\n`contact.providerChannelId` is a preference: the pinned line is used when it is free or recoverable;\notherwise the same order applies over all the Agent's lines. Among occupied lines, one this Agent controls\ncomes first; if none does, the pinned line is the one reused (and may fail with 0006).\n\nReusing a conversation depends on the Agent type:\n- LeadScout: if its LeadScout session is still open and not expired, the call fails with 409\n  `BRIDGE_AI_LEADSCOUT_0017` (`extra` carries `conversationId`, `sessionId` and `stage`). If an expired\n  session cannot be finalized, it fails with 409 `BRIDGE_AI_LEADSCOUT_0018`.\n- Service Agent: an open, unexpired service session is continued; an expired one is closed as abandoned and a\n  new one opens.\n\nSee [the complete outbound guide](https://docs.bridge.new/guides/agent-outbound.md) for prerequisites, the forceSend decision table, recovery, partial work and safe retry decisions.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    },
    {
      "title": "Register a campaign lead",
      "source": "https://docs.bridge.new/api-reference/campaign-leads/register-a-campaign-lead",
      "markdownUrl": "https://docs.bridge.new/api-reference/campaign-leads/register-a-campaign-lead.md",
      "description": "Registers a lead captured by a tenant's landing page and queues the first outbound WhatsApp message. A 202 means the lead was accepted, not that a message was sent.",
      "kind": "operation",
      "contentMarkdown": "**Operation reference:** `post /leadscout/campaign-leads`\n\n**operationId:** `create_campaign_lead_leadscout_campaign_leads_post`\n\nRegisters a lead captured by a tenant's landing page and queues the first outbound WhatsApp message. A 202 means the lead was accepted, not that a message was sent.\n\n[Exact OpenAPI contract](https://docs.bridge.new/api-reference/campaign-leads.openapi.yaml)\n\nThis corpus contains guides and operation descriptions. It does not embed every request and response schema. Read the linked OpenAPI contract before generating a request.\n"
    }
  ]
}
