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

# Response URL

> Reply up to 45 minutes later by POSTing to a special URL — no API key needed.

Every event we send includes a `response_url`. POST Block Kit to it, and the message lands in that conversation. The key is already inside the URL, so there is nothing to set up. That is why this is the main path for n8n, Make, and Zapier.

```text theme={null}
POST https://app.my-aichatbot.com/api/v1/responses/{token}
```

## The pattern

<Steps>
  <Step title="Confirm the webhook">
    Return an empty body with `2xx`. The typing dots stay on, because we know a reply is coming.
  </Step>

  <Step title="Do the slow work">
    Call your LLM, query your database, build the PDF. Take up to 45 minutes.
  </Step>

  <Step title="POST the result">
    Send Block Kit to the `response_url`. It shows up in the chat and turns off the typing dots.
  </Step>
</Steps>

<Note>
  The typing dots give up after about 3 minutes, but the token keeps working. A reply at minute 10 still lands — it just shows up without the dots first.
</Note>

## Send a reply

No `Authorization` header. Having the URL is your permission.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "$RESPONSE_URL" \
    -H "Content-Type: application/json" \
    -d '{
      "text": "Your report is ready 📊",
      "buttons": [{ "label": "Download", "url": "https://example.com/r.pdf" }]
    }'
  ```

  ```javascript Node.js theme={null}
  await fetch(responseUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: 'Your report is ready 📊',
      buttons: [{ label: 'Download', url: 'https://example.com/r.pdf' }]
    })
  })
  ```

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

  requests.post(response_url, json={
      "text": "Your report is ready 📊",
      "buttons": [{"label": "Download", "url": "https://example.com/r.pdf"}],
  })
  ```

  ```php PHP theme={null}
  $ch = curl_init($responseUrl);
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
      CURLOPT_POSTFIELDS => json_encode([
          'text' => 'Your report is ready 📊',
          'buttons' => [['label' => 'Download', 'url' => 'https://example.com/r.pdf']],
      ]),
      CURLOPT_RETURNTRANSFER => true,
  ]);
  $response = curl_exec($ch);
  ```

  ```go Go theme={null}
  body, _ := json.Marshal(map[string]any{
      "text": "Your report is ready 📊",
      "buttons": []map[string]string{
          {"label": "Download", "url": "https://example.com/r.pdf"},
      },
  })

  resp, err := http.Post(responseURL, "application/json", bytes.NewReader(body))
  ```
</CodeGroup>

A success returns a handle for every message you created:

```json theme={null}
{
  "ok": true,
  "messages": [
    { "message_id": "0f8c…", "mid": "job-42", "updated": false }
  ]
}
```

`updated` tells you if the call **created** a new message or **replaced** one — see [Updating messages](/sending-messages/updating-messages).

## Token rules

<ParamField body="Lifetime" type="45 minutes">
  Counted from when the event was created, not from your first use.
</ParamField>

<ParamField body="Reuse" type="multi-use">
  Post several messages, or update one many times, until it expires.
</ParamField>

<ParamField body="Rate limit" type="30 / minute">
  Per token.
</ParamField>

<ParamField body="Retries" type="same token">
  Every retry of one event carries the exact same `response_url`.
</ParamField>

## Errors

| Status | Code                      | Meaning                                                                  |
| ------ | ------------------------- | ------------------------------------------------------------------------ |
| `401`  | `token_invalid`           | The token is broken or has a bad signature.                              |
| `410`  | `RESPONSE_URL_EXPIRED`    | Past 45 minutes. Use an [API key](/sending-messages/api-key) instead.    |
| `404`  | `conversation_not_found`  | The conversation no longer exists.                                       |
| `409`  | `conversation_taken_over` | A human is handling it now. The bot is locked out.                       |
| `413`  | `PAYLOAD_TOO_LARGE`       | Body over 256 KB.                                                        |
| `422`  | `EMPTY_BODY`              | Nothing to post. An empty body is an error here, unlike a webhook reply. |
| `422`  | `FRAMEWORK_DEFAULT_BODY`  | You posted `Accepted`, `OK`, or similar.                                 |
| `422`  | `BLOCK_VALIDATION_FAILED` | Bad Block Kit. The `issues` list shows each problem.                     |
| `429`  | `rate_limited`            | Over 30 per minute. Wait for `retry_after` seconds.                      |

<Warning>
  An empty body is **fine** as a webhook reply (it means "got it"). Here it is an **error** (`422 EMPTY_BODY`). This endpoint only exists to create messages.
</Warning>

A `422` is also written to your delivery log. So even if your scenario ignores HTTP responses, you can still see why nothing showed up.

## Attaching files

Upload through the token — no API key needed mid-flow:

```text theme={null}
POST https://app.my-aichatbot.com/api/v1/responses/{token}/files
```

See [Sending files](/sending-messages/files).

## Next

<CardGroup cols={2}>
  <Card title="Progress updates" icon="loader" href="/sending-messages/updating-messages">
    Turn one bubble from "queued" into "done".
  </Card>

  <Card title="Past 45 minutes?" icon="key" href="/sending-messages/api-key">
    Switch to a key. It never expires.
  </Card>
</CardGroup>
