> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xpander.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Trigger your agents from any external system with inbound HTTP requests

Webhooks let you trigger an Xpander agent from any system that can make an HTTP request, including automation platforms like Zapier, form submissions, CI/CD pipelines, or your own applications.

Every webhook-enabled agent gets its own **public URL** - a unique, unguessable address that runs
only that agent. No API key, no proxy backend, so you can call it straight from a contact form, a
landing page, or a no-code tool that cannot set custom headers.

Once set up, you can:

* Trigger an agent run from any inbound HTTP POST with a prompt or payload
* Call it directly from a browser or form builder, with no credential to hide
* Choose synchronous (wait for the agent's response) or asynchronous (fire-and-forget) invocation
* Upload files (documents, images, audio) alongside the prompt
* Extract specific fields from the agent's response instead of returning the full payload
* Map nested fields from the incoming payload to the agent's input parameters
* Pass through MCP OAuth tokens for downstream tool authentication

Pre-requisites:

* a [**Personal Agent**](/guides/quickstart)
* or, a [**Custom Agent**](/guides/agents/agent-configuration)

## Enable Webhooks

<Steps>
  <Step title="Open the Channels tab">
    In the Agent Studio, click the **gear** <Icon icon="gear" size={16} /> icon and go to the **Channels** tab. Find the **Webhook** section and toggle it on.

    <Frame>
      <img src="https://mintcdn.com/xpanderai-099931d1/OGlJJ1lp1VY3af7I/images/guide/deployment-channels.png?fit=max&auto=format&n=OGlJJ1lp1VY3af7I&q=85&s=2c6bbaba4ae8d07c8425fca70a2e56d9" alt="Channels tab with the Webhook section showing the Enabled toggle and Configure and test button" width="3396" height="2076" data-path="images/guide/deployment-channels.png" />
    </Frame>
  </Step>

  <Step title="Copy your public URL">
    Enabling the webhook mints the agent's public URL automatically. Click <kbd>Configure and test</kbd> to open the Agent Webhook Tester, where the **Payload URL** is that public URL, alongside a ready-to-use **cURL command**.

    <Frame>
      <img src="https://mintcdn.com/xpanderai-099931d1/OGlJJ1lp1VY3af7I/images/guide/deployment-webhook.png?fit=max&auto=format&n=OGlJJ1lp1VY3af7I&q=85&s=8def14cad11b050bc8cc015fd5342dc4" alt="Agent Webhook Tester showing payload URL, API key, cURL command, JSON payload fields, and Test Webhook button" width="500" data-path="images/guide/deployment-webhook.png" />
    </Frame>
  </Step>

  <Step title="Test it">
    Choose an input format - **JSON**, **Form Data**, or **Multipart**. Set your payload fields and click <kbd>Test Webhook</kbd>. The response appears in the **Response History** panel.
  </Step>
</Steps>

## Make Requests

Every webhook request is a POST to your agent's public URL. The secret in the path both
authenticates the call and binds it to that one agent, so nothing else is required.

```bash theme={"dark"}
curl -X POST 'https://webhook.xpander.ai/wh_YOUR_SECRET' \
  -H 'Content-Type: application/json' \
  -d '{"prompt": "Summarize the latest support tickets"}'
```

Because there is no header to set, the same call works straight from a page:

```js theme={"dark"}
await fetch("https://webhook.xpander.ai/wh_YOUR_SECRET", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name, email, message }),
});
```

<Warning>
  The URL **is** the credential. Anyone holding it can run that agent, so treat it like a password:
  keep it out of public repositories, and use <kbd>Regenerate URL</kbd> in the Channels tab if it
  leaks. Regenerating kills the old URL immediately, so update anything that calls it.

  A public URL can invoke its own agent and nothing else - it grants no access to your account,
  your other agents, or any management API.
</Warning>

<Note>
  Calls to a public URL default to **asynchronous** and return `{"status": "Started"}`, because the
  typical caller is a browser that should not wait. Add `?asynchronous=false` when you want the
  agent's answer in the response.
</Note>

### Invoking with an API key

The original key-based form still works and is unchanged, which suits server-to-server callers
that already hold an organization key:

```bash theme={"dark"}
curl -X POST 'https://webhook.xpander.ai/?agent_id=YOUR_AGENT_ID' \
  -H 'Content-Type: application/json' \
  -H 'x-api-key: YOUR_API_KEY' \
  -d '{"prompt": "Summarize the latest support tickets"}'
```

| Parameter   | Location               | Description               |
| ----------- | ---------------------- | ------------------------- |
| `agent_id`  | Query string or body   | Your agent's ID           |
| `x-api-key` | Header or query string | Your organization API key |

<Warning>
  An organization API key is a broad platform credential - never embed it in browser JavaScript or
  anywhere a visitor can read it. That is what the public URL is for.
</Warning>

### Optional Parameters

| Parameter                      | Type          | Description                                                                                                                                                                                                                                                         |
| ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt`                       | string        | Text input for the agent                                                                                                                                                                                                                                            |
| `asynchronous`                 | boolean       | Return immediately without waiting (default: `true` on a public URL, `false` with an API key)                                                                                                                                                                       |
| `task_id`                      | string        | Continue an existing conversation thread                                                                                                                                                                                                                            |
| `user_id`                      | string        | User identifier for tracking                                                                                                                                                                                                                                        |
| `user_email`                   | string        | User email for personalization                                                                                                                                                                                                                                      |
| `user_first_name`              | string        | User's first name                                                                                                                                                                                                                                                   |
| `user_last_name`               | string        | User's last name                                                                                                                                                                                                                                                    |
| `getter`                       | string        | Dot-notation path to extract from the response                                                                                                                                                                                                                      |
| `json`                         | boolean       | Parse the result as JSON instead of returning a string                                                                                                                                                                                                              |
| `user_tokens`                  | string (JSON) | Pre-authenticated MCP OAuth tokens (see below)                                                                                                                                                                                                                      |
| `disable_attachment_injection` | boolean       | When `true`, uploaded files are not injected into the LLM context window. File URLs are still available to the agent's tools, but the raw content is not prepended to the prompt. Useful when files are large or the agent's tools handle file processing directly. |

You can include any additional fields in your request body - they're all passed to the agent as context.

<Info>
  When the same parameter appears in multiple places, **query parameters** take priority over **body parameters**, which take priority over **defaults**.
</Info>

## Choose Sync vs Async

**Synchronous** (default) - The request blocks until the agent finishes and returns the full result. Best for short tasks and interactive workflows.

```json theme={"dark"}
{
  "id": "task-uuid",
  "agent_id": "your-agent-id",
  "status": "completed",
  "result": "The agent's response...",
  "created_at": "2026-01-29T00:40:46.161472+00:00",
  "finished_at": "2026-01-29T00:40:47.919145+00:00",
  "source": "webhook",
  "execution_attempts": 1
}
```

**Asynchronous** - Set `asynchronous=true` to return immediately. The agent processes in the background. Best for long-running tasks, file processing, or when you don't need the result inline.

```bash theme={"dark"}
curl -X POST 'https://webhook.xpander.ai/wh_YOUR_SECRET?asynchronous=true' \
  -H 'Content-Type: application/json' \
  -d '{"prompt": "Generate the monthly report"}'
```

```json theme={"dark"}
{"status": "Started"}
```

## Upload Files

Upload files using `multipart/form-data`. The agent automatically processes them - OCR for images, text extraction for PDFs, transcription for audio.

```bash theme={"dark"}
curl -X POST 'https://webhook.xpander.ai/wh_YOUR_SECRET' \
  --form 'files=@"/path/to/document.pdf"' \
  --form 'files=@"/path/to/image.png"'
```

Supported types: PDF, DOC, DOCX, TXT, XLS, XLSX, CSV, PNG, JPEG, GIF, WEBP, SVG, ZIP.

<Warning>
  Files must be uploaded directly - URLs to external files are not supported. The webhook needs the actual file content.
</Warning>

<Tip>
  For large batches (10+ files), use async mode and break uploads into batches of 5-10 files.
</Tip>

<Info>
  Uploaded files are stored on the Xpander platform and exposed to your agent through secure presigned URLs. Those URLs are scoped to your organization's agents and remain valid for 30 days after upload. Your agent never handles download or storage logic directly.
</Info>

## Extract Data from Responses

Use the `getter` parameter to extract a specific field from the agent's response using dot notation.

```bash theme={"dark"}
curl -X POST 'https://webhook.xpander.ai/wh_YOUR_SECRET?asynchronous=false&getter=result.summary' \
  -H 'Content-Type: application/json' \
  -d '{"prompt": "Analyze quarterly report"}'
```

Instead of the full response object, this returns just the extracted value:

```json theme={"dark"}
"Q4 revenue increased 15%"
```

## Map Dynamic Parameters

Use `params_mapping` as a query parameter to extract values from nested payload fields and map them to webhook parameters. This is especially useful for integrations with Telegram bots, WhatsApp, and other messaging platforms where identifiers are nested.

```bash theme={"dark"}
curl -X POST 'https://webhook.xpander.ai/wh_YOUR_SECRET?params_mapping={"task_id":"message.chat.id","prompt":"message.text","user_email":"message.from.email"}' \
  -H 'Content-Type: application/json' \
  -d '{
    "message": {
      "chat": {"id": 123456789},
      "from": {"email": "user@example.com"},
      "text": "Hello agent!"
    }
  }'
```

The mapping uses dot notation to navigate nested fields. It supports all standard parameters: `task_id`, `user_id`, `user_email`, `user_first_name`, `user_last_name`, `prompt`.

## Pass MCP OAuth Tokens

If your agent uses MCP servers with OAuth authentication, pass pre-authenticated user tokens via `user_tokens` to bypass the interactive OAuth flow during webhook execution.

```bash theme={"dark"}
curl -X POST 'https://webhook.xpander.ai/?agent_id=YOUR_AGENT_ID' \
  -H 'x-api-key: YOUR_API_KEY' \
  -d '{
    "prompt": "Check my calendar",
    "user_email": "user@example.com",
    "user_tokens": {
      "mcp-graph-item-uuid-1": "user-access-token-abc123"
    }
  }'
```

To find the graph item IDs for your MCP servers, call the [Get Agent API](/api-reference/v1/agents/get-agent) and inspect `graph.items` for entries with `type: "mcp"`.

## Monitor Webhook Runs

Every webhook invocation creates a task visible in the **Monitor** tab, exactly like any other agent run. You get the full execution trace: prompts, tool calls with request/response payloads, files processed, timing, and token usage. For details on reading execution traces, see [Threads](/guides/observability/threads).

## Handle Errors

| Status | Response                                                      | Cause                                                                                                  |
| ------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| 400    | `{"detail": "agent_id is required"}`                          | Missing `agent_id` (API-key form only)                                                                 |
| 401    | `{"detail": "API key not specified"}`                         | Unknown, regenerated or revoked public URL - or, on the API-key form, a missing `x-api-key`            |
| 403    | `{"detail": "Invalid API key"}`                               | Wrong API key, or **Developer access** is off for this agent - that switch disables the public URL too |
| 503    | `{"detail": "Public webhook lookup temporarily unavailable"}` | Transient platform issue resolving the URL - retry                                                     |

## Next Steps

<CardGroup cols={2}>
  <Card title="Scheduled Tasks" icon="clock" href="/guides/deploy/scheduled-tasks">
    Run agents on a recurring schedule
  </Card>

  <Card title="MCP Protocol" icon="bridge" href="/guides/deploy/mcp">
    Expose agents to Claude Desktop and code editors
  </Card>

  <Card title="Monitor Runs" icon="chart-line" href="/guides/observability/threads">
    Trace execution, debug failures, and review AI performance
  </Card>
</CardGroup>
