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

# Shortext endpoint integration

> Send the messages of your own users to a Shortext agent and get its replies, synchronously or through a signed webhook.

A [Shortext endpoint](https://docs.shortext.ny-corp.io/channels/shortext-endpoint.md) connects your own software to an agent. Your server posts each message written by one of your users; the agent answers; your server shows the reply.

```mermaid theme={null}
sequenceDiagram
    participant U as Your user
    participant S as Your server
    participant X as Shortext
    U->>S: writes a message
    S->>X: POST /api/v1.0/messages/inbound
    alt reply_mode sync
        X-->>S: status completed + replies
    else reply_mode async (or sync timeout)
        X-->>S: status accepted (or pending)
        X->>S: message.sent webhook (signed)
    end
    S->>U: shows the reply
```

## 1. Create the endpoint

From the dashboard or with `POST /api/v2.0/channels/shortext` (**Manage integrations**):

```json theme={null}
{
  "name": "Mobile app",
  "agent_id": "AGI-XXXXX",
  "reply_mode": "async",
  "sync_timeout": 25,
  "webhook_url": "https://api.example.com/webhooks/shortext-replies"
}
```

| Field | Description |
| - | - |
| `name` | Required on creation. |
| `agent_id` | The agent that answers; `null` detaches the endpoint. |
| `enabled` | `true` by default. A disabled endpoint refuses inbound messages. |
| `reply_mode` | `async` (default): replies are pushed to `webhook_url`. `sync`: replies come back in the response of the inbound call. |
| `sync_timeout` | Seconds the sync mode waits for the replies, 1 to 25 (default 25). |
| `webhook_url` | Where replies are pushed (HTTPS). `null` removes it. |

The response is a channel with its id (`ste_...`) and `settings.inbound_url`. When you set a `webhook_url`, it also holds the **`webhook_secret`, shown once**. `POST /channels/shortext/{id}/rotate-secret` issues a new one (the old one stops working at once).

## 2. Send a user message

`POST https://shortext.ny-corp.io/api/v1.0/messages/inbound`, with your API key:

| Field | Description |
| - | - |
| `channel` | `shortext`. |
| `to` | The endpoint id, `ste_...`. |
| `contact` | Your user, see [Contact format](https://docs.shortext.ny-corp.io/api-reference/contact-format.md): `identifier` is your own user id; the profile is optional. |
| `message.id` | **Your** id for this message, up to 100 characters. Used for idempotency. |
| `message.type` | `text`. |
| `message.content.text` | The text, up to 4,096 characters. |
| `message.metadata` | Optional object, kept with the message. |
| `reply_mode` | Optional: `sync` or `async`, overrides the endpoint setting for this message. |

```bash cURL theme={null}
curl -X POST https://shortext.ny-corp.io/api/v1.0/messages/inbound \
  -H "Authorization: Bearer $SHORTEXT_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "shortext",
    "to": "ste_xxx",
    "reply_mode": "sync",
    "contact": {
      "identifier": "user-42",
      "profile": { "firstname": "Awa", "email": "awa@example.com", "preferred_language": "en" }
    },
    "message": {
      "id": "msg_9f8e7d",
      "type": "text",
      "content": { "text": "Where is my order?" }
    }
  }'
```

```php PHP theme={null}
<?php
$response = (new GuzzleHttp\Client())->post('https://shortext.ny-corp.io/api/v1.0/messages/inbound', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('SHORTEXT_API_KEY'), 'Accept' => 'application/json'],
    'json'    => [
        'channel'    => 'shortext',
        'to'         => 'ste_xxx',
        'reply_mode' => 'sync',
        'contact'    => ['identifier' => 'user-42', 'profile' => ['firstname' => 'Awa']],
        'message'    => ['id' => 'msg_9f8e7d', 'type' => 'text', 'content' => ['text' => 'Where is my order?']],
    ],
]);
$body = json_decode((string) $response->getBody(), true)['body'];

if ($body['status'] === 'completed') {
    foreach ($body['replies'] as $reply) {
        echo $reply['message']['content']['text'], PHP_EOL;
    }
}
```

```javascript Node.js theme={null}
const response = await fetch("https://shortext.ny-corp.io/api/v1.0/messages/inbound", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SHORTEXT_API_KEY}`,
    Accept: "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    channel: "shortext",
    to: "ste_xxx",
    reply_mode: "sync",
    contact: { identifier: "user-42", profile: { firstname: "Awa" } },
    message: { id: "msg_9f8e7d", type: "text", content: { text: "Where is my order?" } },
  }),
});
const { body } = await response.json();

if (body.status === "completed") {
  body.replies.forEach((reply) => console.log(reply.message.content.text));
}
```

```python Python theme={null}
response = requests.post(
    "https://shortext.ny-corp.io/api/v1.0/messages/inbound",
    headers={"Authorization": f"Bearer {os.environ['SHORTEXT_API_KEY']}", "Accept": "application/json"},
    json={
        "channel": "shortext",
        "to": "ste_xxx",
        "reply_mode": "sync",
        "contact": {"identifier": "user-42", "profile": {"firstname": "Awa"}},
        "message": {"id": "msg_9f8e7d", "type": "text", "content": {"text": "Where is my order?"}},
    },
)
body = response.json()["body"]

if body["status"] == "completed":
    for reply in body["replies"]:
        print(reply["message"]["content"]["text"])
```

> [!NOTE]
>
> This endpoint is served under `/api/v1.0` and works with the same API keys as v2.0. It does not check key permissions.

## 3. Read the outcome

The response `body` holds `status`, the stored `message` and, in sync mode, `replies`:

```json theme={null}
{
  "success": true,
  "message": "Request success.",
  "code": 1000,
  "body": {
    "status": "completed",
    "message": { "id": "k5Rz8pQw", "direction": "inbound", "channel": "shortext", "channel_id": "ste_xxx", "contact": { "identifier": "user-42", "identifier_type": "external_id", "profile": {} }, "message": { "id": "msg_9f8e7d", "type": "text", "content": { "text": "Where is my order?" } }, "status": "received" },
    "replies": [
      { "id": "m2Tq7vXa", "direction": "outbound", "channel": "shortext", "channel_id": "ste_xxx", "contact": { "identifier": "user-42" }, "message": { "id": "shortext-generated-id", "type": "text", "content": { "text": "Could you give me your order reference?" } }, "status": "sent" }
    ]
  }
}
```

| `status` | Meaning | Replies |
| - | - | - |
| `completed` | Sync: the agent answered in time. | In `replies`. |
| `pending` | Sync: the agent did not answer within `sync_timeout`. | Pushed to the webhook. |
| `accepted` | Async: the message is queued. | Pushed to the webhook. |
| `no_auto_reply` | Stored, but no automatic reply: the conversation is in handover, the endpoint has no agent or the agent is inactive, or your balance is insufficient. | A human may answer: pushed to the webhook. |
| `duplicate` | This `message.id` was already received. | The original message is returned, no new reply. |

## Idempotency

`message.id` is your own message id. If you send the same id again (a retry after a timeout, for example), Shortext does not store it twice nor answer twice: you get `duplicate` with the original message. Always retry with the **same** `message.id`.

## 4. Receive replies on your webhook

Replies (async mode, sync timeouts, answers written by your team in the Inbox) are posted to `webhook_url` as `message.sent` events, with the same body, headers and signature as every [Shortext webhook](https://docs.shortext.ny-corp.io/api-reference/webhooks.md):

```json theme={null}
{
  "id": "evt_01JB8Z...",
  "type": "message.sent",
  "api_version": "v2.0",
  "created_at": "2026-10-24T09:12:51+00:00",
  "data": {
    "id": "m2Tq7vXa",
    "conversation_id": "c9Lw3nYb",
    "direction": "outbound",
    "channel": "shortext",
    "channel_id": "ste_xxx",
    "contact": { "identifier": "user-42", "identifier_type": "external_id", "profile": {} },
    "message": { "id": "shortext-generated-id", "type": "text", "content": { "text": "Could you give me your order reference?" } },
    "status": "sent",
    "created_at": "2026-10-24T09:12:51+00:00"
  }
}
```

Use `data.contact.identifier` (your user id) to route the reply to the right user. Verify the signature with the endpoint's `webhook_secret`:

```php PHP theme={null}
<?php
// $secret: the signing secret returned when the webhook endpoint was created
function shortextSignatureIsValid(string $secret, string $timestamp, string $rawBody, string $signature): bool
{
    // Reject old deliveries: protects against replays
    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
        return false;
    }

    $expected = 'sha256=' . hash_hmac('sha256', "{$timestamp}.{$rawBody}", $secret);

    return hash_equals($expected, $signature);
}

$rawBody = file_get_contents('php://input'); // the raw body, before any JSON decoding
$valid   = shortextSignatureIsValid(
    getenv('SHORTEXT_WEBHOOK_SECRET'),
    $_SERVER['HTTP_X_SHORTEXT_TIMESTAMP'] ?? '',
    $rawBody,
    $_SERVER['HTTP_X_SHORTEXT_SIGNATURE'] ?? ''
);

if (!$valid) {
    http_response_code(401);
    exit;
}

$event = json_decode($rawBody, true);
// Acknowledge fast with a 2xx, then process $event (deduplicate on $event['id'])
http_response_code(200);
```

```javascript Node.js theme={null}
import crypto from "node:crypto";
import express from "express";

const app = express();

function signatureIsValid(secret, timestamp, rawBody, signature) {
  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return false;
  }
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw keeps the body exactly as it was sent
app.post("/webhooks/shortext", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  const valid = signatureIsValid(
    process.env.SHORTEXT_WEBHOOK_SECRET,
    req.get("X-Shortext-Timestamp") ?? "",
    rawBody,
    req.get("X-Shortext-Signature")
  );
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(rawBody);
  res.sendStatus(200); // acknowledge fast, then process (deduplicate on event.id)
});

app.listen(3000);
```

```python Python theme={null}
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)


def signature_is_valid(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    signed = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


@app.post("/webhooks/shortext")
def shortext_webhook():
    raw_body = request.get_data()  # the raw body, before any JSON decoding
    if not signature_is_valid(
        os.environ["SHORTEXT_WEBHOOK_SECRET"],
        request.headers.get("X-Shortext-Timestamp", ""),
        raw_body,
        request.headers.get("X-Shortext-Signature", ""),
    ):
        abort(401)

    event = request.get_json()
    # Acknowledge fast, then process (deduplicate on event["id"])
    return "", 200
```

Deliveries are retried for about 21 hours on failure; deduplicate on the event `id`.

## Errors

| Situation | `code` |
| - | - |
| `channel` is not `shortext`, or `to` is not one of your endpoints | `1007` |
| The endpoint is disabled, or is not a server endpoint | `1005` |
| Invalid body | `1002` |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.