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

# Intégration d'un endpoint Shortext

> Envoyez les messages de vos propres utilisateurs à un agent Shortext et recevez ses réponses, en synchrone ou par un webhook signé.

Un [endpoint Shortext](https://docs.shortext.ny-corp.io/fr/channels/shortext-endpoint.md) relie votre propre logiciel à un agent. Votre serveur envoie chaque message écrit par l'un de vos utilisateurs ; l'agent répond ; votre serveur affiche la réponse.

```mermaid theme={null}
sequenceDiagram
    participant U as Votre utilisateur
    participant S as Votre serveur
    participant X as Shortext
    U->>S: écrit un message
    S->>X: POST /api/v1.0/messages/inbound
    alt reply_mode sync
        X-->>S: status completed + replies
    else reply_mode async (ou délai sync dépassé)
        X-->>S: status accepted (ou pending)
        X->>S: webhook message.sent (signé)
    end
    S->>U: affiche la réponse
```

## 1. Créer l'endpoint

Depuis le tableau de bord ou avec `POST /api/v2.0/channels/shortext` (**Gérer les intégrations**) :

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

| Champ | Description |
| - | - |
| `name` | Obligatoire à la création. |
| `agent_id` | L'agent qui répond ; `null` détache l'endpoint. |
| `enabled` | `true` par défaut. Un endpoint désactivé refuse les messages entrants. |
| `reply_mode` | `async` (par défaut) : les réponses sont envoyées à `webhook_url`. `sync` : les réponses reviennent dans la réponse de l'appel entrant. |
| `sync_timeout` | Secondes pendant lesquelles le mode sync attend les réponses, de 1 à 25 (25 par défaut). |
| `webhook_url` | Là où les réponses sont envoyées (HTTPS). `null` la supprime. |

La réponse est un canal avec son identifiant (`ste_...`) et `settings.inbound_url`. Quand vous définissez une `webhook_url`, elle contient aussi le **`webhook_secret`, affiché une seule fois**. `POST /channels/shortext/{id}/rotate-secret` en génère un nouveau (l'ancien cesse aussitôt de fonctionner).

## 2. Envoyer un message utilisateur

`POST https://shortext.ny-corp.io/api/v1.0/messages/inbound`, avec votre clé API :

| Champ | Description |
| - | - |
| `channel` | `shortext`. |
| `to` | L'identifiant de l'endpoint, `ste_...`. |
| `contact` | Votre utilisateur, voir [Format des contacts](https://docs.shortext.ny-corp.io/fr/api-reference/contact-format.md) : `identifier` est votre propre identifiant utilisateur ; le profil est facultatif. |
| `message.id` | **Votre** identifiant pour ce message, jusqu'à 100 caractères. Sert à l'idempotence. |
| `message.type` | `text`. |
| `message.content.text` | Le texte, jusqu'à 4 096 caractères. |
| `message.metadata` | Objet facultatif, conservé avec le message. |
| `reply_mode` | Facultatif : `sync` ou `async`, remplace le réglage de l'endpoint pour ce 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]
>
> Cet endpoint est servi sous `/api/v1.0` et fonctionne avec les mêmes clés API que la v2.0. Il ne vérifie pas les permissions des clés.

## 3. Lire le résultat

Le `body` de la réponse contient `status`, le `message` enregistré et, en mode sync, `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` | Signification | Réponses |
| - | - | - |
| `completed` | Sync : l'agent a répondu à temps. | Dans `replies`. |
| `pending` | Sync : l'agent n'a pas répondu dans le délai `sync_timeout`. | Envoyées au webhook. |
| `accepted` | Async : le message est en file. | Envoyées au webhook. |
| `no_auto_reply` | Enregistré, mais sans réponse automatique : la conversation est prise en main, l'endpoint n'a pas d'agent ou l'agent est inactif, ou votre solde est insuffisant. | Une personne peut répondre : envoyée au webhook. |
| `duplicate` | Ce `message.id` a déjà été reçu. | Le message d'origine est renvoyé, sans nouvelle réponse. |

## Idempotence

`message.id` est votre propre identifiant de message. Si vous renvoyez le même identifiant (une relance après un délai dépassé, par exemple), Shortext ne l'enregistre pas deux fois et ne répond pas deux fois : vous obtenez `duplicate` avec le message d'origine. Relancez toujours avec le **même** `message.id`.

## 4. Recevoir les réponses sur votre webhook

Les réponses (mode async, délais sync dépassés, réponses rédigées par votre équipe dans la boîte de réception) sont envoyées à `webhook_url` sous forme d'événements `message.sent`, avec le même corps, les mêmes en-têtes et la même signature que tous les [webhooks Shortext](https://docs.shortext.ny-corp.io/fr/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"
  }
}
```

Utilisez `data.contact.identifier` (votre identifiant utilisateur) pour acheminer la réponse vers le bon utilisateur. Vérifiez la signature avec le `webhook_secret` de l'endpoint :

```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
```

En cas d'échec, les livraisons sont relancées pendant environ 21 heures ; dédoublonnez sur l'`id` de l'événement.

## Erreurs

| Situation | `code` |
| - | - |
| `channel` n'est pas `shortext`, ou `to` n'est pas l'un de vos endpoints | `1007` |
| L'endpoint est désactivé, ou n'est pas un endpoint serveur | `1005` |
| Corps invalide | `1002` |


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