# Make a Text Hook

> A Text Hook is an address any app can post to (Calendly, Typeform, Shopify, Stripe, Jotform, your own form) that turns the post into a text from your number.

`POST /v1/hooks`

A Text Hook is an address any app can post to (Calendly, Typeform, Shopify, Stripe, Jotform, your own form) that turns the post into a text from your number. `mapping` says which posted field is the person’s number and what to say, with `{{body.…}}` for posted fields.

Anyone with the address can send texts from your number, so keep it private. Start with `test_mode` on to check a real post without sending.

Needs a key with the `messages:send` scope. [Keys and scopes](https://usebluebot.com/docs/concepts/api-keys)

Send an `Idempotency-Key` so a retry never acts twice. [Idempotency](https://usebluebot.com/docs/concepts/idempotency)

## Headers

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | No | Any string of up to 255 characters, new for each request. A retry with the same key gets the first answer back for 24 hours, with `Idempotent-Replayed: true`. Up to 255 characters |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Up to 80 characters |
| `preset` | string | No | Start from the usual fields of Calendly, Typeform, Shopify, Stripe or Jotform. One of `calendly`, `typeform`, `shopify`, `stripe`, `jotform` |
| `mapping` | object | No |   |
| `mapping.to` | string | No | Up to 200 characters |
| `mapping.text` | string | No | Up to 2,000 characters |
| `mapping.first_name` | string | No | Up to 200 characters |
| `mapping.last_name` | string | No | Up to 200 characters |
| `mapping.tags` | string | No | Up to 200 characters |
| `template_id` | string | No | A saved template to use instead of `text`. Starts with `tpl_` |
| `sequence_id` | string or null | No | A seq_ id. Starts with `seq_` |
| `test_mode` | boolean | No | Default `false` |

## Example

```bash
curl -X POST "https://usebluebot.com/api/v1/hooks" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0f6d8c2e-5b1a-4c7e-9d3f-2a8b6e1c4d70" \
  -d '{
    "name": "Calendly bookings",
    "preset": "calendly",
    "test_mode": true
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const hook = await client.hooks.create({
  name: 'Calendly bookings',
  preset: 'calendly',
  testMode: true,
}, { idempotencyKey: '0f6d8c2e-5b1a-4c7e-9d3f-2a8b6e1c4d70' });
console.log(hook);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

hook = client.hooks.create(
    name="Calendly bookings",
    preset="calendly",
    test_mode=True,
    idempotency_key="0f6d8c2e-5b1a-4c7e-9d3f-2a8b6e1c4d70",
)
print(hook)
```

## Response

`201`: The Text Hook, with its address.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | A hook_ id. Starts with `hook_` |
| `object` | "text_hook" | Yes |   |
| `name` | string | Yes |   |
| `url` | string | Yes | The address to post to. Anyone with it can send texts from your number, so keep it private. |
| `enabled` | boolean | Yes |   |
| `test_mode` | boolean | Yes | Posts are recorded and checked but no text is sent. |
| `mapping` | object | Yes | How a post becomes a text. Each value is a template over the posted fields: `body` for the JSON or form, `query` for the address’s query string. |
| `mapping.to` | string or null | Yes | Who to text, as a `{{body.…}}` path into the posted fields. Up to 200 characters |
| `mapping.text` | string or null | Yes | The words, with `{{body.…}}` fields. Leave it empty to use `template_id`. Up to 2,000 characters |
| `mapping.first_name` | string or null | Yes | Saved on the contact when it has no first name yet. Up to 200 characters |
| `mapping.last_name` | string or null | Yes | Up to 200 characters |
| `mapping.tags` | string or null | Yes | Tags to add, separated by commas. May use `{{body.…}}` fields. Up to 200 characters |
| `template_id` | string or null | Yes |   |
| `sequence_id` | string or null | Yes | A sequence each person is added to. Starts with `seq_` |
| `last_used_at` | timestamp or null | Yes | When it last received a post. |
| `created_at` | timestamp | Yes | When it was made. |

The answer, `201`:

```json
{
  "id": "hook_1Hq2LxW9cVbN3mKpRtYs0a",
  "object": "text_hook",
  "name": "Calendly bookings",
  "url": "https://usebluebot.com/h/th_8Rk2mQ4zN7wX1cVbL5pT9yHsQ2aVc0Ld",
  "enabled": true,
  "test_mode": true,
  "mapping": {
    "to": "{{body.payload.questions_and_answers.0.answer}}",
    "text": "Hi {{body.payload.name}}, you’re booked for {{body.payload.scheduled_event.start_time}}. Text back if anything changes.",
    "first_name": "{{body.payload.first_name}}",
    "last_name": "{{body.payload.last_name}}",
    "tags": "calendly"
  },
  "template_id": null,
  "sequence_id": null,
  "last_used_at": null,
  "created_at": "2026-11-03T16:00:00.000Z"
}
```

## Errors

Every refusal has the same envelope and a page of its own. These are the ones this operation can give.

| Code                                                        | Status | What it means                                                                                                                                    |
| ----------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`unauthorized`](https://usebluebot.com/docs/errors/unauthorized)                 | 401    | The request carried no key, or a key BlueBot doesn’t know: mistyped, revoked, or a test key sent to the live API.                                |
| [`insufficient_scope`](https://usebluebot.com/docs/errors/insufficient_scope)     | 403    | The key doesn’t carry the scope the endpoint needs. Each endpoint’s reference names its scope.                                                   |
| [`rate_limited`](https://usebluebot.com/docs/errors/rate_limited)                 | 429    | Too many requests. A key may make 120 requests a minute. The `RateLimit` header says how many are left, and `Retry-After` how long to wait.      |
| [`internal`](https://usebluebot.com/docs/errors/internal)                         | 500    | Something went wrong on BlueBot’s side. It was reported. Send the request again; with an Idempotency-Key that is always safe.                    |
| [`invalid_request`](https://usebluebot.com/docs/errors/invalid_request)           | 400    | Something in the request doesn’t fit: a missing or malformed field, a bad id, or a combination the endpoint refuses. `details` names each field. |
| [`not_found`](https://usebluebot.com/docs/errors/not_found)                       | 404    | There is no such thing on this account. Ids are prefixed by kind, so a message id starts with `msg_`.                                            |
| [`limit_reached`](https://usebluebot.com/docs/errors/limit_reached)               | 409    | The account has as many of something as it may have.                                                                                             |
| [`plan_lapsed`](https://usebluebot.com/docs/errors/plan_lapsed)                   | 402    | The account’s plan isn’t active, so BlueBot isn’t taking new texts. Texts already queued wait and go once the plan is active again.              |
| [`idempotency_conflict`](https://usebluebot.com/docs/errors/idempotency_conflict) | 409    | The Idempotency-Key was used in the last 24 hours with a different request. A key stands for one request.                                        |

## Try it
