> ## 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.

# Webhook response

> Reply in the body of the request we send you — the fastest path, with a 10-second limit.

When a visitor sends a message, we POST a signed `message.created` event to your webhook. **Whatever JSON you return becomes the bot's reply.** No keys, no second request.

```json theme={null}
{ "text": "Thanks! Looking that up now…" }
```

That is the whole integration. In n8n it is a **Respond to Webhook** node. In Make it is **Webhook response**.

## You get 10 seconds

One timer covers the whole exchange: connecting, sending, and reading your reply. Miss it, and we treat the try as a timeout and retry.

<Warning>
  Ten seconds is the *total* limit, not a connection limit. If your work calls an LLM, a slow API, or anything you cannot control, do not reply in the same request. Return an empty body, then reply through the [response URL](/sending-messages/response-url).
</Warning>

We read at most 256 KB of your reply.

## How we read your reply

| You return                       | What happens                                                                                                    |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `2xx` + Block Kit JSON           | Messages are created and show up right away.                                                                    |
| `2xx` + empty body               | We mark it received. No message. The typing dots **stay on**, because we expect a late reply.                   |
| `2xx` + `{}`                     | Same as empty — received, no message.                                                                           |
| `2xx` + other plain text         | The body becomes a markdown message, exactly as sent.                                                           |
| `2xx` + a framework default body | Blocked. See below.                                                                                             |
| `2xx` + broken Block Kit         | No message. The visitor sees an error note. Your delivery log shows `invalid_response` with the exact problems. |
| `3xx`                            | Fails right away. **We never follow redirects.**                                                                |
| `408`, `425`, `429`, `5xx`       | We retry.                                                                                                       |
| Other `4xx`                      | Fails right away, no retry. A client error will not fix itself.                                                 |

## The default-body trap

Most tools return something like `Accepted` when you have not set a reply. Left alone, that would post the word "Accepted" into your customer's chat.

We know the common defaults and **block** them instead:

```text theme={null}
Accepted        OK        ok
success         Success   Workflow was started
```

<Warning>
  The list is an exact match, and short on purpose. A body like `Accepted.` or `Done!` is **not** on it and *will* show up as a bot message. Return JSON, or an empty body.
</Warning>

## Retries

If a try fails in a way we can retry, we try up to **4 times in total**:

| Try | When                                                               |
| --- | ------------------------------------------------------------------ |
| 1   | Right away, in the same request                                    |
| 2   | About 15 seconds later                                             |
| 3–4 | Spaced out by our queue — about a minute, then a couple of minutes |

Every try carries the **same** `X-MyAIChatbot-Event-Id` and the **same** `response_url`, with a fresh signature.

<Warning>
  Retries mean your endpoint may see the same event twice. Use `X-MyAIChatbot-Event-Id` to spot repeats — it never changes between tries. The body field `delivery_attempt` does **not** count tries; read the `X-MyAIChatbot-Attempt` header instead.
</Warning>

After the last try fails, the visitor sees a short system note, and the delivery shows as failed in your log. You can retry it by hand from there.

## A complete handler

<CodeGroup>
  ```javascript Node.js theme={null}
  import express from 'express'

  const app = express()

  app.post('/webhook', express.json(), (req, res) => {
    const e = req.body

    if (e.event === 'webhook.verify') {
      return res.json({ challenge: e.challenge })
    }

    if (e.event === 'message.created') {
      return res.json({
        text: `You said: **${e.message.text}**`,
        buttons: [{ label: 'Talk to a human', value: 'escalate' }]
      })
    }

    res.status(200).end()
  })
  ```

  ```python Python theme={null}
  from flask import Flask, request, jsonify

  app = Flask(__name__)

  @app.post("/webhook")
  def webhook():
      e = request.get_json()

      if e.get("event") == "webhook.verify":
          return jsonify(challenge=e["challenge"])

      if e.get("event") == "message.created":
          return jsonify(
              text=f"You said: **{e['message']['text']}**",
              buttons=[{"label": "Talk to a human", "value": "escalate"}],
          )

      return "", 200
  ```

  ```php PHP theme={null}
  <?php
  $e = json_decode(file_get_contents('php://input'), true);
  header('Content-Type: application/json');

  if ($e['event'] === 'webhook.verify') {
      echo json_encode(['challenge' => $e['challenge']]);
      exit;
  }

  if ($e['event'] === 'message.created') {
      echo json_encode([
          'text' => "You said: **{$e['message']['text']}**",
          'buttons' => [['label' => 'Talk to a human', 'value' => 'escalate']],
      ]);
      exit;
  }

  http_response_code(200);
  ```

  ```go Go theme={null}
  type event struct {
      Event     string `json:"event"`
      Challenge string `json:"challenge"`
      Message   struct {
          Text string `json:"text"`
      } `json:"message"`
  }

  func webhook(w http.ResponseWriter, r *http.Request) {
      var e event
      json.NewDecoder(r.Body).Decode(&e)
      w.Header().Set("Content-Type", "application/json")

      switch e.Event {
      case "webhook.verify":
          json.NewEncoder(w).Encode(map[string]string{"challenge": e.Challenge})
      case "message.created":
          json.NewEncoder(w).Encode(map[string]any{
              "text": "You said: **" + e.Message.Text + "**",
              "buttons": []map[string]string{{"label": "Talk to a human", "value": "escalate"}},
          })
      default:
          w.WriteHeader(http.StatusOK)
      }
  }
  ```

  ```bash curl theme={null}
  # Your webhook returns the body. To see what we would accept,
  # post the same JSON to a response_url from a real event:
  curl -X POST "$RESPONSE_URL" \
    -H "Content-Type: application/json" \
    -d '{ "text": "You said: **hello**" }'
  ```
</CodeGroup>

<Note>
  This handler does not check the signature yet. Do not ship it like this — see [Verifying signatures](/receiving-events/verifying-signatures).
</Note>

## Next

<CardGroup cols={2}>
  <Card title="Work that takes longer" icon="clock" href="/sending-messages/response-url">
    Confirm fast, answer later.
  </Card>

  <Card title="Every block type" icon="layout-grid" href="/block-kit/blocks/text">
    Go past text and buttons.
  </Card>
</CardGroup>
