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

# Agent API channel

> Call an agent over plain HTTP — POST a message, get the reply back in the response

The **API channel** is the simplest way to call an agent from your own code. You POST a message to the agent's channel URL with an API key, and the agent's reply comes back in the HTTP response. No token exchange, no JSON-RPC, no MCP client.

It is one of the channels on an agent's **Channels** tab, alongside Slack, Microsoft Teams, Email, and [A2A](/api-reference/platform/a2a). Use it when you want a webhook-style "send text in, get text out" integration.

<Note>
  The API channel and the [Agent Management API](/api-reference/platform/agent-management) both let you invoke an agent over HTTP, but they are different surfaces. The API channel is a single per-agent URL with a dedicated key — ideal for embedding one agent in an external system. The Agent Management API is a full MCP server for *managing* agents (create, update, list, invoke) with your account API key. If you only need to send messages to one agent, use the API channel.
</Note>

## Enable the channel

1. Open the agent and go to the **Channels** tab.
2. Toggle **API** on.
3. Two values appear:
   * **Endpoint URL** — the per-agent URL you POST to, e.g. `https://triggers.app.pinkfish.ai/ext/triggers/{triggerId}`.
   * **API key** — passed in the `X-API-Key` header. Use **Rotate key** to mint a new one; the old key stops working immediately.
4. Optionally set the **Output format** (Text, Markdown, or JSON — see [below](#output-format)).

<Note>
  Copy the **Endpoint URL** directly from the Channels tab rather than constructing it by hand — the host is environment-specific and the path segment after `/ext/triggers/` is the agent's channel trigger ID, not the agent ID.
</Note>

## Send a message

`POST` the endpoint URL with your API key and a JSON body:

```bash theme={null}
curl -s -X POST "https://triggers.app.pinkfish.ai/ext/triggers/{triggerId}" \
  -H "X-API-Key: YOUR_CHANNEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Summarize today'\''s support tickets"
  }'
```

### Request body

The body must be `application/json`. Unknown fields are rejected.

| Field      | Type   | Required | Description                                                                                                                                                          |
| ---------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message`  | string | Yes      | The user message to send to the agent.                                                                                                                               |
| `chatId`   | string | No       | The chat to continue. Omit on the first call; pass it on every follow-up to keep context. See [Maintaining conversation context](#maintaining-conversation-context). |
| `metadata` | object | No       | Arbitrary caller-supplied key/values, forwarded to the agent as `sourceMetadata`.                                                                                    |

### Response

The agent runs synchronously and the reply comes back in the response body. The shape depends on the configured **Output format**:

* **Text** (default) — `Content-Type: text/plain`, body is the agent's reply text.
* **Markdown** — `Content-Type: text/markdown`, body is the agent's reply as Markdown.
* **JSON** — `Content-Type: application/json`. If the agent has a structured **output schema**, the body is that structured output verbatim. Otherwise the body is a default envelope:

```json theme={null}
{
  "chatId": "auto_xyz789",
  "output": "Here is the summary of today's support tickets..."
}
```

Every response that produced or continued a chat also carries the chat ID in a response header:

```
X-Pf-Chat-Id: auto_xyz789
```

## Maintaining conversation context

<Warning>
  **This is the most common point of confusion.** Each call that does **not** include a `chatId` starts a brand-new conversation with no memory of previous calls. If you see "the chat ID changes on every call" and the agent never remembers earlier messages, it is because you are not passing the previous `chatId` back.
</Warning>

The API channel is stateful, but *you* hold the conversation handle. To keep context across turns:

<Steps>
  <Step title="First call — omit chatId">
    Send just `{ "message": "..." }`. The agent creates a fresh chat and returns its ID in the `X-Pf-Chat-Id` response header (and in the `chatId` field of the JSON envelope, if you use JSON output).
  </Step>

  <Step title="Capture the chat ID">
    Read `X-Pf-Chat-Id` from the response headers (or `chatId` from the JSON body) and store it for this conversation.
  </Step>

  <Step title="Follow-up calls — send the same chatId">
    Include `"chatId": "auto_xyz789"` in the body of every subsequent request. The agent reuses that chat, so it sees the full prior history.
  </Step>
</Steps>

```bash theme={null}
# Turn 1 — no chatId. Capture the returned chat ID from the X-Pf-Chat-Id header.
CHAT_ID=$(curl -s -D - -o /dev/null -X POST "$ENDPOINT_URL" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"message":"My name is Vinod."}' \
  | grep -i '^X-Pf-Chat-Id:' | awk '{print $2}' | tr -d '\r')

# Turn 2 — pass the same chatId back. The agent remembers turn 1.
curl -s -X POST "$ENDPOINT_URL" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d "{\"message\":\"What is my name?\",\"chatId\":\"$CHAT_ID\"}"
```

A given `chatId` belongs to the channel that created it. If you pass a `chatId` that this agent's API channel does not own, the call is rejected with `403` and reason `unowned_chat_id` — and your `chatId` is deliberately not echoed back. Always reuse a `chatId` that came from a previous response of *this same* channel.

## Forwarding headers to your MCP server

If your agent uses a **remote MCP server** — one Pinkfish proxies to at a URL you operate — you can pass per-request headers from the API channel straight through to that server. This is how you do per-end-user authentication and authorization at your own MCP server. The channel's `X-API-Key` authenticates the *channel*, not the person on whose behalf you are calling, so if your MCP server needs to know which user this request is for, send that yourself.

Any request header you send with the reserved prefix `X-Pf-Forward-` is forwarded to your remote MCP server with the prefix stripped:

```bash theme={null}
curl -s -X POST "https://triggers.app.pinkfish.ai/ext/triggers/{triggerId}" \
  -H "X-API-Key: YOUR_CHANNEL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Pf-Forward-User-Id: u123" \
  -H "X-Pf-Forward-Tenant: acme-corp" \
  -d '{"message": "What are my open tickets?"}'
```

Your MCP server receives:

```
user-id: u123
tenant: acme-corp
```

There is nothing to configure — no allowlist to register, no setting in the agent builder. The prefix is the whole mechanism, and it is also the security boundary: headers *without* it are never forwarded.

<Warning>
  **Look the header up case-insensitively.** The name reaches your server lowercased — `X-Pf-Forward-User-Id` arrives as `user-id`, not `User-Id`. This is valid HTTP (header names are case-insensitive) and most frameworks handle it for you, but if yours exposes a raw map — Go's `r.Header` indexed directly, some Python and Node handlers — an exact-case lookup returns empty with no error and no warning. Read it case-insensitively.
</Warning>

### What gets forwarded

| Header you send                 | Your MCP server sees | Why                                       |
| ------------------------------- | -------------------- | ----------------------------------------- |
| `X-Pf-Forward-User-Id: u123`    | `user-id: u123`      | Carries the prefix                        |
| `X-Pf-Forward-Tenant: acme`     | `tenant: acme`       | Carries the prefix                        |
| `X-Custom-Thing: v`             | *(nothing)*          | No prefix — not forwarded                 |
| `Authorization: Bearer …`       | *(nothing)*          | Pinkfish-internal; never forwarded        |
| `X-Pf-Forward-Authorization: v` | *(nothing)*          | Reserved name, dropped even when prefixed |

`content-type`, `accept`, `authorization`, and `mcp-session-id` are dropped even if you deliberately prefix them, so a forwarded header can never override content negotiation, the session, or the credential Pinkfish sends upstream.

<Note>
  The `authorization` header your MCP server receives is the credential from the **connection** backing that server, not the `X-API-Key` you sent to the channel. Use a forwarded header for end-user identity; use the connection for the server's own credential.
</Note>

### Requirements

Two conditions have to hold, and when either fails the tool simply does not appear — there is no error message pointing at the cause:

* **The server must be a remote MCP server** — one registered against a URL you operate. Servers generated from an OpenAPI spec, and Pinkfish's built-in embedded services, do not receive forwarded headers. See [Custom MCP Servers](/api-reference/platform/custom-mcp-servers).
* **A connection must resolve for that server.** Remote servers fetch their credential from a connection matched on service key. With no matching connection the server's tools never load at all.

### Isolation between users

Forwarded headers are part of the key for the cached session to your MCP server, so two requests carrying different forwarded values never share a session. Sending `user-id: alice` and then `user-id: bob` produces two independent upstream sessions — `bob` cannot land on a connection opened for `alice`.

<Note>
  The same prefix works when invoking an agent through the [Agent Management API](/api-reference/platform/agent-management) — both entrypoints converge on the same behavior.
</Note>

## Output format

Set the format on the Channels tab; it is stored per channel.

| Format     | Content-Type       | Body                                                                                            |
| ---------- | ------------------ | ----------------------------------------------------------------------------------------------- |
| `text`     | `text/plain`       | Raw assistant text (default).                                                                   |
| `markdown` | `text/markdown`    | Raw assistant text as Markdown.                                                                 |
| `json`     | `application/json` | The agent's structured output (if it has an output schema), otherwise `{ "chatId", "output" }`. |

Choose **JSON** when your caller wants the `chatId` in the body instead of reading it from a header, or when the agent produces structured output.

## Rotating the key

Use **Rotate key** on the Channels tab to mint a fresh `X-API-Key`. Rotation is atomic: the new key takes effect immediately and the previous key stops working, so update your callers before (or right after) rotating.

## Errors

| HTTP status   | Meaning                                                                                                    |
| ------------- | ---------------------------------------------------------------------------------------------------------- |
| `401` / `403` | Missing or invalid `X-API-Key`, the channel was disabled, or a `chatId` you don't own (`unowned_chat_id`). |
| `415`         | Body was not `application/json` (or used multipart/file upload, which the API channel does not accept).    |
| `400`         | Malformed body or unknown fields.                                                                          |
| `200`         | Success — the agent's reply is in the body.                                                                |

## Related

* [A2A protocol](/api-reference/platform/a2a) — expose the agent to external agent-to-agent clients.
* [Agent Management API](/api-reference/platform/agent-management) — create, update, list, and invoke agents with your account API key.
* [Channels tab](/agents/channels) — enabling channels from the agent builder UI.
