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

# HTTP API

> Consume your Lua agent directly via HTTP requests

## Overview

The HTTP API is the external consumption interface for deployed Lua AI agents. Use it when a custom integration, mobile app, backend service, or other client needs to send a user turn to an agent and receive the response.

<Warning>
  **Calling from Lua code? Do not call these endpoints directly.** Inside a tool, job, webhook, preprocessor, or postprocessor, use [`Agents.invoke`](/api/agents) instead. It runs the same full agent pipeline without a manually managed API URL or bearer token.
</Warning>

| Where the call originates                     | Use                                                  |
| --------------------------------------------- | ---------------------------------------------------- |
| External app, service, device, or integration | `/chat/generate/:agentId` or `/chat/stream/:agentId` |
| Lua runtime code inside an agent primitive    | [`Agents.invoke(targetAgentId, ...)`](/api/agents)   |

<CardGroup cols={2}>
  <Card title="Stream Endpoint" icon="wave-pulse">
    Real-time streaming responses via SSE
  </Card>

  <Card title="Generate Endpoint" icon="bolt">
    Single response generation
  </Card>
</CardGroup>

## Base URL

```
https://api.heylua.ai
```

## Authentication

All requests require authentication via Bearer token in the Authorization header. You can use your API key as the token:

```bash theme={null}
Authorization: Bearer YOUR_API_KEY
```

<Tip>
  You can find your API key in the [Admin Dashboard](https://admin.heylua.ai) under **Settings → API Keys**.
</Tip>

<Note>
  A key scoped to a specific organization or agent can only reach the resources its role covers — see [API Keys](/concepts/api-keys) for legacy vs. scoped keys and how roles are granted.
</Note>

***

## Endpoints

### Stream Chat Response

Stream a chat response using Server-Sent Events (SSE).

```
POST /chat/stream/:agentId
```

### Generate Chat Response

Generate a complete chat response (non-streaming).

```
POST /chat/generate/:agentId
```

<Note>
  **PostProcessors:** Only the `/generate` endpoint supports PostProcessors. Streaming responses (`/stream`) bypass post-processing because text is sent incrementally before the full response is available.
</Note>

***

## Path Parameters

| Parameter | Type   | Required | Description                    |
| --------- | ------ | -------- | ------------------------------ |
| `agentId` | string | **Yes**  | The ID of your deployed agent. |

## Query Parameters

| Parameter    | Type   | Required | Description                                                                                                                               |
| ------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `channel`    | string | No       | The channel context for the conversation. Options: `web`, `whatsapp`, `email`, `slack`, `facebook`, `instagram`. Defaults to `undefined`. |
| `identifier` | string | No       | A unique identifier for the message. Can be used for tracking purposes.                                                                   |

***

## Request Body

The request body follows the AI SDK 5 `UserContent` format for messages.

### Required Fields

| Field      | Type          | Description                                                                                                   |
| ---------- | ------------- | ------------------------------------------------------------------------------------------------------------- |
| `messages` | `UserContent` | Array of content parts (text, image, or file). This is a single user message that can contain multiple parts. |

### Optional Fields

| Field            | Type   | Description                                                                                                                                                                                                                                                                                                                                                      |
| ---------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `threadId`       | string | Conversation thread identifier. Turns sharing a `threadId` share history; omit it and the platform uses one default thread per user, agent, and channel. Use a fresh value to start a clean conversation.                                                                                                                                                        |
| `systemPrompt`   | string | Override the agent's default persona/prompt                                                                                                                                                                                                                                                                                                                      |
| `runtimeContext` | string | Additional context injected into the agent's prompt                                                                                                                                                                                                                                                                                                              |
| `clientContext`  | object | Client-side context for the request. Currently supports `timezone` — an IANA timezone string (e.g. `"Africa/Nairobi"`) used as the user's local timezone for date/time-aware responses. When omitted, the agent falls back to the user's stored profile, country, or UTC.                                                                                        |
| `options`        | object | Normalized per-request model options. `options.reasoning` — `{ effort?, show? }` — overrides the agent's `modelSettings.reasoning` for this request (the request always wins, field by field). `options.verbosity` — `'low' \| 'medium' \| 'high'` — requests output verbosity (provider support varies). See [With Reasoning Options](#with-reasoning-options). |

<Accordion title="Advanced Options (rarely needed)">
  These options are for advanced use cases and typically not needed for standard integrations:

  | Field                   | Type    | Description                                                   |
  | ----------------------- | ------- | ------------------------------------------------------------- |
  | `navigate`              | boolean | Enable navigation responses for web widget (default: `false`) |
  | `skillOverride`         | array   | Override the agent's skills with specific sandbox versions    |
  | `personaOverride`       | string  | Override the agent's persona                                  |
  | `preprocessorOverride`  | array   | Override preprocessors                                        |
  | `postprocessorOverride` | array   | Override postprocessors                                       |
</Accordion>

***

## Message Content Types

Messages follow the AI SDK 5 `UserContent` format:

### Text Message

```json theme={null}
{
  "type": "text",
  "text": "Hello, how can you help me today?"
}
```

### Image Message

```json theme={null}
{
  "type": "image",
  "image": "https://example.com/image.jpg",
  "mediaType": "image/jpeg"
}
```

### File Message

```json theme={null}
{
  "type": "file",
  "data": "https://example.com/document.pdf",
  "mediaType": "application/pdf"
}
```

***

## Examples

### Basic Text Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/chat/generate/my-agent" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "messages": [
        {
          "type": "text",
          "text": "What products do you have available?"
        }
      ]
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/chat/generate/my-agent', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      messages: [
        {
          type: 'text',
          text: 'What products do you have available?'
        }
      ]
    })
  });

  const result = await response.json();
  console.log(result);
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.heylua.ai/chat/generate/my-agent',
      headers={
          'Authorization': 'Bearer YOUR_API_KEY',
          'Content-Type': 'application/json'
      },
      json={
          'messages': [
              {
                  'type': 'text',
                  'text': 'What products do you have available?'
              }
          ]
      }
  )

  print(response.json())
  ```
</CodeGroup>

### Streaming Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/chat/stream/my-agent" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "messages": [
        {
          "type": "text",
          "text": "Tell me about your services"
        }
      ]
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/chat/stream/my-agent', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      messages: [
        {
          type: 'text',
          text: 'Tell me about your services'
        }
      ]
    })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    const chunk = decoder.decode(value);
    const lines = chunk.split('\n\n').filter(line => line.trim());
    
    for (const line of lines) {
      const data = JSON.parse(line);
      console.log(data);
    }
  }
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.heylua.ai/chat/stream/my-agent',
      headers={
          'Authorization': 'Bearer YOUR_API_KEY',
          'Content-Type': 'application/json'
      },
      json={
          'messages': [
              {
                  'type': 'text',
                  'text': 'Tell me about your services'
              }
          ]
      },
      stream=True
  )

  for line in response.iter_lines():
      if line:
          print(line.decode('utf-8'))
  ```
</CodeGroup>

### With Image Attachment

```bash theme={null}
curl -X POST "https://api.heylua.ai/chat/generate/my-agent" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "type": "text",
        "text": "What can you tell me about this product?"
      },
      {
        "type": "image",
        "image": "https://example.com/product-photo.jpg",
        "mediaType": "image/jpeg"
      }
    ]
  }'
```

### With System Prompt Override

Use `systemPrompt` to temporarily override the agent's persona for a specific request:

```bash theme={null}
curl -X POST "https://api.heylua.ai/chat/generate/my-agent" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "type": "text",
        "text": "Help me with my order"
      }
    ],
    "systemPrompt": "You are a friendly customer support agent. Be concise and helpful."
  }'
```

### With Runtime Context

Use `runtimeContext` to inject additional context into the agent's prompt:

```bash theme={null}
curl -X POST "https://api.heylua.ai/chat/generate/my-agent" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "type": "text",
        "text": "What are my recent orders?"
      }
    ],
    "runtimeContext": "Current user: John Doe (ID: 12345). VIP customer since 2020."
  }'
```

### With Timezone

Use `clientContext.timezone` to tell the agent the user's local IANA timezone for date/time-aware responses. When omitted, the agent falls back to the user's stored profile, country, or UTC:

```bash theme={null}
curl -X POST "https://api.heylua.ai/chat/generate/my-agent" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "type": "text",
        "text": "What time should I schedule my call for tomorrow morning?"
      }
    ],
    "clientContext": {
      "timezone": "Africa/Nairobi"
    }
  }'
```

### With Reasoning Options

Use `options.reasoning` to control how much the model reasons for this specific request. It works on both `/chat/generate` and `/chat/stream`, and overrides the agent's `modelSettings.reasoning` default — the request always wins, field by field (setting only `effort` doesn't clear an agent-level `show: false`):

```bash theme={null}
curl -X POST "https://api.heylua.ai/chat/generate/my-agent" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "type": "text",
        "text": "Walk me through the tradeoffs of these two contract options."
      }
    ],
    "options": {
      "reasoning": { "effort": "high", "show": false }
    }
  }'
```

| Field              | Type                                                         | Description                                                                                                                                                                                                                    |
| ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `reasoning.effort` | `'off' \| 'minimal' \| 'low' \| 'medium' \| 'high' \| 'max'` | How much the model reasons before responding. Any value is safe to send for any model — Lua clamps it to the nearest behavior the resolved model supports, never erroring. Unrecognized values are ignored and defaults apply. |
| `reasoning.show`   | boolean                                                      | Whether the reasoning trace is surfaced in the response. Default `true`. `false` suppresses reasoning from both the stream and the generate response.                                                                          |
| `verbosity`        | `'low' \| 'medium' \| 'high'`                                | Requested output verbosity (provider support varies).                                                                                                                                                                          |

<Note>
  Leaving `options.reasoning` unset falls back to the agent's `modelSettings.reasoning`, then to the platform default — adaptive reasoning where the model supports it, low effort otherwise. See [Model Selection → Reasoning Effort](/overview/model-selection#reasoning-effort).
</Note>

### With Channel Context

Specify the channel for channel-specific behavior:

```bash theme={null}
curl -X POST "https://api.heylua.ai/chat/generate/my-agent?channel=whatsapp" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "type": "text",
        "text": "Send me the order confirmation"
      }
    ]
  }'
```

***

## Response Format

### Generate Response

The generate endpoint returns a complete response object:

```json theme={null}
{
  "text": "Here are the products we have available...",
  "toolCalls": [],
  "usage": {
    "promptTokens": 150,
    "completionTokens": 200,
    "totalTokens": 350
  }
}
```

### Stream Response

The stream endpoint returns Server-Sent Events (SSE) with JSON chunks:

```
{"type":"text-delta","textDelta":"Here "}

{"type":"text-delta","textDelta":"are "}

{"type":"text-delta","textDelta":"the products..."}

{"type":"finish","finishReason":"stop"}
```

<Note>
  **Reasoning visibility.** The default stream format above never carries the model's reasoning trace. Reasoning is only streamed on the AI SDK UI message stream — opt in with `?protocol=ui` on `/chat/stream`, where it arrives as `reasoning` parts. Setting `reasoning.show: false` (per request via `options.reasoning`, or per agent via `modelSettings.reasoning`) suppresses reasoning everywhere: from the UI message stream and from the generate response.
</Note>

***

## Error Responses

| Status Code | Description                                                                                                                |
| ----------- | -------------------------------------------------------------------------------------------------------------------------- |
| `400`       | Invalid request payload                                                                                                    |
| `401`       | Unauthorized - Invalid, expired, suspended, or revoked token                                                               |
| `403`       | Forbidden - Token is valid, but its role doesn't allow this action on this resource                                        |
| `423`       | Agent is disabled for this user                                                                                            |
| `503`       | Lua is restarting before your turn starts. Retry once only when the body matches the typed `CORE_DRAINING` contract below. |
| `500`       | Internal server error                                                                                                      |

**Standard error response format:**

```json theme={null}
{
  "type": "error",
  "message": "Error description",
  "statusCode": 400
}
```

**Typed pre-admission rollout response:**

```json theme={null}
{
  "error": {
    "code": "CORE_DRAINING",
    "message": "Lua is restarting. Please retry your request.",
    "retryable": true
  }
}
```

When you receive this exact `503` response, the turn did not start. Retry once after the `Retry-After` header. If the header is missing, wait about one second.

***

## Long-Running Turns

Turns that run research-grade tools or Space delegations can legitimately take 60–120 seconds. The HTTP edge closes a connection that has produced no output for roughly 90 seconds, so a long `/chat/generate` call — or a `/chat/stream` call during a single long tool execution — can return a `502`/`504` or a terminated stream **even though the turn completes on the platform**. The agent's reply is still generated, persisted to the conversation, and visible to every later turn on the same `threadId`.

Handle it like this:

1. **Prefer `/chat/stream`** for any agent that runs tools. Streamed events extend the window and give you partial progress to show.
2. **Treat an edge cut on a long turn as "likely completed", not failed.** Do not automatically resend the same message. The original turn usually finished, and a resend runs the whole tool chain a second time.
3. **Continue on the same `threadId`.** The completed reply is already in the conversation history; a follow-up message (for example, "show me the result again") returns it without redoing the work.

Do not confuse these edge cuts with the typed `503 CORE_DRAINING` response above. The `CORE_DRAINING` response is the safe retry signal. Raw `502`, `504`, and terminated transport failures are not.

### Safe retry during rollouts

Retry only this case:

1. The status is `503`.
2. The body matches the typed `CORE_DRAINING` response.
3. The response arrives before any stream body or generated output.

Do not retry these cases automatically:

1. Raw `502` or `504`.
2. A terminated or reset stream.
3. Any response that already streamed body data.

```typescript theme={null}
async function sendTurnOnce(run: () => Promise<Response>): Promise<Response> {
  const first = await run();
  if (!(await isCoreDraining(first))) {
    return first;
  }

  const delayMs = retryAfterMs(first.headers.get("retry-after"));
  await first.body?.cancel();
  await new Promise((resolve) => setTimeout(resolve, delayMs));
  return run();
}

async function isCoreDraining(response: Response): Promise<boolean> {
  if (response.status !== 503) return false;
  try {
    const payload = await response.clone().json();
    return (
      payload?.error?.code === "CORE_DRAINING" &&
      payload?.error?.message === "Lua is restarting. Please retry your request." &&
      payload?.error?.retryable === true
    );
  } catch {
    return false;
  }
}

function retryAfterMs(value: string | null): number {
  if (value === null) return 1000;
  const seconds = /^\d+$/.test(value.trim()) ? Number(value) : Number.NaN;
  const delay = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(value) - Date.now();
  return Number.isFinite(delay) ? Math.min(Math.max(0, delay), 5000) : 1000;
}
```

Pass a function that creates a fresh request body for each attempt. Do not reuse a consumed `Request` or `ReadableStream` body.

***

## Navigate Option

<Note>
  The `navigate` option is specifically for web widget integrations. When enabled, it allows the agent to send navigation commands that direct users to specific pages on your website.
</Note>

```json theme={null}
{
  "messages": [{ "type": "text", "text": "Show me pricing" }],
  "navigate": true
}
```

When `navigate` is `true`, the agent can include navigation components in its response that trigger the `onNavigate` callback in the LuaPop widget.

<Card title="Learn More About Navigation" icon="compass" href="/formatting/navigate">
  See the Navigate Component documentation for details on how navigation works with the web widget.
</Card>

***

## Best Practices

<AccordionGroup>
  <Accordion title="Use Streaming for Better UX">
    For user-facing applications, use the `/chat/stream` endpoint to provide real-time feedback as the response is generated.
  </Accordion>

  <Accordion title="Include Channel Context">
    When building integrations, specify the `channel` parameter to help the agent format responses appropriately for the platform.
  </Accordion>

  <Accordion title="Use Runtime Context Wisely">
    The `runtimeContext` field is great for injecting user-specific information or session context without modifying the agent's core persona.
  </Accordion>

  <Accordion title="Handle Streaming Properly">
    When using the stream endpoint, ensure you properly handle the SSE format and parse each JSON chunk separately.
  </Accordion>

  <Accordion title="Plan for Long Turns">
    Research- or delegation-heavy turns can run past the edge's \~90-second no-output window and surface as a `502`/`504` while still completing on the platform. See [Long-Running Turns](#long-running-turns) before adding retry logic.
  </Accordion>
</AccordionGroup>

***

## Use Cases

<CardGroup cols={2}>
  <Card title="Mobile Apps" icon="mobile">
    Build native mobile experiences with your Lua agent
  </Card>

  <Card title="Custom Integrations" icon="plug">
    Integrate with internal tools and systems
  </Card>

  <Card title="Voice Assistants" icon="microphone">
    Power voice interfaces with AI responses
  </Card>

  <Card title="Automation" icon="robot">
    Trigger agent responses from workflows
  </Card>
</CardGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Channels" icon="comments" href="/channels/introduction">
    Pre-built channel integrations
  </Card>

  <Card title="Chat Widget" icon="message" href="/chat-widget/introduction">
    Embeddable web widget
  </Card>

  <Card title="Navigate Component" icon="compass" href="/formatting/navigate">
    Web navigation feature
  </Card>

  <Card title="LuaAgent" icon="robot" href="/api/luaagent">
    Agent configuration
  </Card>
</CardGroup>
