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

# Inbound Messages (Relays)

> Receive inbound SMS messages relayed to your endpoint via HTTP POST.

<Info>
  To receive inbound SMS messages, you need a **dedicated longcode (mobile number)** purchased from Phonovation. Contact [sales@phonovation.com](mailto:sales@phonovation.com) to get one set up.
</Info>

When a recipient replies to your dedicated longcode, the Phonovation gateway relays the inbound message to a URL you provide via HTTP POST.

## Receiving opt-outs

Even without a dedicated longcode, **all Phonovation customers can receive opt-out messages** at their inbound relay URL. Opt-outs are always sent to the shared shortcode `50123` — when a recipient texts `STOP` or `OPTOUT` to `50123`, the relay fires to your endpoint with `SMS-To=50123`.

You do not need a longcode to handle opt-outs. See [Opt-out payloads](#opt-out-payloads) below.

```mermaid title="Inbound routing flow" theme={"dark"}
flowchart TD
  Reply["Recipient sends SMS reply"] --> Gateway["Phonovation gateway"]
  Gateway --> Relay["POST to your relay URL"]
  Relay --> Destination{"SMS-To is 50123?"}
  Destination -->|Yes| Suppress["Add SMS-From to suppression list"]
  Destination -->|No| Keyword{"Keyword matched?"}
  Keyword -->|Yes| Handler["Route to keyword handler"]
  Keyword -->|No| General["Route to general inbox"]
```

## Setup

To receive inbound messages, contact [sales@phonovation.com](mailto:sales@phonovation.com) to purchase a dedicated longcode. Once provisioned, provide your relay URL to [support@phonovation.com](mailto:support@phonovation.com). The endpoint must:

* Accept `HTTP POST` requests
* Return a `200 OK` response

## Relay parameters

| Parameter       | Example                            | Description                                                                                    |
| --------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------- |
| `SMS-Type`      | `AuthCode`                         | Message type — see [SMS-Type values](#sms-type-values) below                                   |
| `SMS-Content`   | `Test reply`                       | Full message body                                                                              |
| `SMS-Keyphrase` | `CONFIRM` or `*`                   | First word of the message, used for keyword routing. `*` means no specific keyword was matched |
| `SMS-Network`   | `272/1`                            | Network code in MCC/MNC format — may be empty                                                  |
| `SMS-From`      | `353871111111`                     | Sender's mobile number (GSM format)                                                            |
| `SMS-To`        | `353877777777`                     | The number the message was sent to (GSM format)                                                |
| `SMS-NotifyId`  | `order-001`                        | The `notifyId` from the original outbound message, if one was set — otherwise empty            |
| `SMS-Verify`    | `9db038748b3088971d913616c13e769b` | MD5 hash for payload verification — see below                                                  |
| `SMS-TimeStamp` | `2026-04-09 16:03:11`              | Gateway receipt time (`yyyy-MM-dd HH:mm:ss`)                                                   |
| `SMS-AuthCode`  | `AUTH:reply/1450707`               | **Legacy.** Routing code used by older integrations — ignore in new builds                     |
| `SMS-GroupCode` | `AUTH:G353871234568/*`             | **Legacy.** Group routing code used by older integrations — ignore in new builds               |

### SMS-Type values

| Value      | Meaning                                                 |
| ---------- | ------------------------------------------------------- |
| `AuthCode` | Standard inbound relay message                          |
| `Text`     | Plain text message — seen in some legacy configurations |

## Example relay payload

```text title="Example relay payload" icon="inbox" highlight={2,5-8} wrap theme={"dark"}
SMS-Type=AuthCode
SMS-Content=Test reply
SMS-Keyphrase=*
SMS-Network=
SMS-From=353861234567
SMS-To=353871234568
SMS-NotifyId=
SMS-Verify=5b49b61a65520226f3da14764f7c182b
SMS-TimeStamp=2026-04-09 16:03:11
SMS-AuthCode=AUTH:reply/1450707
SMS-GroupCode=AUTH:G353871234568/*
```

<Warning>
  URL-encode the POST body before sending. Some field values may contain characters that break unencoded requests.
</Warning>

## Verifying the payload

The `SMS-Verify` field is an MD5 hash to confirm the relay genuinely came from Phonovation. The formula follows the same pattern as [DLR verification](/guides/delivery-receipts#verifying-the-payload).

Contact [support@phonovation.com](mailto:support@phonovation.com) for your specific verification key.

<CodeGroup>
  ```bash title="cURL" icon="terminal" wrap theme={"dark"}
  curl -X POST https://example.com/sms/inbound \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "SMS-From=353861234567&SMS-Content=Test%20reply&SMS-Verify=EXPECTED_MD5_HASH"
  ```

  ```js title="JavaScript" icon="js" lines focus={4-8} theme={"dark"}
  const crypto = require('crypto');

  function verifyRelay({ from, content, clientSecret, verify }) {
    const expected = crypto
      .createHash('md5')
      .update(from + content + clientSecret)
      .digest('hex');
    return expected === verify;
  }
  ```

  ```python title="Python" icon="python" lines focus={4-6} theme={"dark"}
  import hashlib

  def verify_relay(sms_from, content, client_secret, verify):
      expected = hashlib.md5(
          f"{sms_from}{content}{client_secret}".encode()
      ).hexdigest()
      return expected == verify
  ```

  ```csharp title="C#" icon="code" lines focus={4-9} theme={"dark"}
  using System.Security.Cryptography;
  using System.Text;

  bool VerifyRelay(string smsFrom, string content, string clientSecret, string verify)
  {
      var input = Encoding.UTF8.GetBytes(smsFrom + content + clientSecret);
      var hash = MD5.HashData(input);
      var expected = Convert.ToHexString(hash).ToLowerInvariant();
      return expected == verify;
  }
  ```

  ```java title="Java" icon="java" lines focus={5-11} theme={"dark"}
  import java.security.MessageDigest;
  import java.util.HexFormat;

  boolean verifyRelay(String smsFrom, String content, String clientSecret, String verify) throws Exception {
      MessageDigest md5 = MessageDigest.getInstance("MD5");
      byte[] hash = md5.digest((smsFrom + content + clientSecret).getBytes());
      String expected = HexFormat.of().formatHex(hash);
      return expected.equals(verify);
  }
  ```

  ```php title="PHP" icon="php" lines focus={2-3} theme={"dark"}
  function verifyRelay($smsFrom, $content, $clientSecret, $verify) {
      $expected = md5($smsFrom . $content . $clientSecret);
      return $expected === $verify;
  }
  ```
</CodeGroup>

## Correlating replies with notifyId

Inbound relay payloads include an `SMS-NotifyId` field. When the gateway can link the inbound reply to an outbound message that had a `notifyId` set, this field will be populated with that value. If no link can be made, it will be empty.

For cases where `SMS-NotifyId` is empty, use `SMS-From` (the sender's mobile number) to look up the record in your system. If you stored the `notifyId` alongside the phone number when you sent the original message, a single lookup gives you the full context.

See [notifyId](/guides/notify-id) for end-to-end flow examples showing outbound, webhook, DLR, and inbound reply all tied together.

## Opt-out payloads

Opt-out messages arrive at your relay URL like any other inbound message. You can identify them by `SMS-To=50123` — this is always the destination for opt-outs regardless of your longcode or sender ID.

```text title="Opt-out relay payload" icon="ban" highlight={2,6} wrap theme={"dark"}
SMS-Type=AuthCode
SMS-Content=Optout
SMS-Keyphrase=*
SMS-Network=
SMS-From=353861234567
SMS-To=50123
SMS-NotifyId=
SMS-Verify=5b49b61a65520226f3da14764f7c182b
SMS-TimeStamp=2026-04-09 16:31:52
SMS-AuthCode=AUTH:reply/177157
SMS-GroupCode=AUTH:G50123/*
```

When you receive a payload with `SMS-To=50123`, add `SMS-From` to your suppression list immediately and do not send further marketing messages to that number.

## Keyword routing

The `SMS-Keyphrase` contains the first word of the inbound message. A value of `*` means the message did not match a specific keyword. Use the keyphrase to route messages to different handlers in your application:

<CodeGroup>
  ```bash title="cURL" icon="terminal" wrap theme={"dark"}
  curl -X POST https://example.com/sms/inbound \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "SMS-Keyphrase=CONFIRM&SMS-From=353861234567&SMS-Content=CONFIRM"
  ```

  ```python title="Python" icon="python" lines expandable focus={5-16} theme={"dark"}
  from flask import Flask, request

  app = Flask(__name__)

  @app.post("/sms/inbound")
  def inbound():
      keyphrase = request.form.get("SMS-Keyphrase", "").upper()
      sender = request.form.get("SMS-From")
      content = request.form.get("SMS-Content")

      if keyphrase == "STOP":
          handle_opt_out(sender)
      elif keyphrase == "CONFIRM":
          handle_confirmation(sender)
      elif keyphrase == "CANCEL":
          handle_cancellation(sender)
      else:
          handle_general(sender, content)

      return "", 200
  ```

  ```csharp title="C#" icon="code" lines expandable focus={5-18} theme={"dark"}
  app.MapPost("/sms/inbound", async (HttpRequest request) =>
  {
      var form = await request.ReadFormAsync();
      var keyphrase = form["SMS-Keyphrase"].ToString().ToUpperInvariant();
      var from = form["SMS-From"].ToString();
      var content = form["SMS-Content"].ToString();

      switch (keyphrase)
      {
          case "STOP":
              HandleOptOut(from);
              break;
          case "CONFIRM":
              HandleConfirmation(from);
              break;
          case "CANCEL":
              HandleCancellation(from);
              break;
          default:
              HandleGeneral(from, content);
              break;
      }

      return Results.Ok();
  });
  ```

  ```java title="Java" icon="java" lines expandable focus={4-17} theme={"dark"}
  post("/sms/inbound", (req, res) -> {
      String keyphrase = req.queryParams("SMS-Keyphrase").toUpperCase();
      String from = req.queryParams("SMS-From");
      String content = req.queryParams("SMS-Content");

      switch (keyphrase) {
          case "STOP" -> handleOptOut(from);
          case "CONFIRM" -> handleConfirmation(from);
          case "CANCEL" -> handleCancellation(from);
          default -> handleGeneral(from, content);
      }

      res.status(200);
      return "";
  });
  ```

  ```php title="PHP" icon="php" lines expandable focus={5-16} theme={"dark"}
  $keyphrase = strtoupper($_POST['SMS-Keyphrase'] ?? '');
  $from = $_POST['SMS-From'] ?? '';
  $content = $_POST['SMS-Content'] ?? '';

  switch ($keyphrase) {
      case 'STOP':
          handleOptOut($from);
          break;
      case 'CONFIRM':
          handleConfirmation($from);
          break;
      case 'CANCEL':
          handleCancellation($from);
          break;
      default:
          handleGeneral($from, $content);
  }

  http_response_code(200);
  ```

  ```js title="JavaScript" icon="js" lines expandable focus={4-15} theme={"dark"}
  app.post('/sms/inbound', (req, res) => {
    const { 'SMS-Keyphrase': keyphrase, 'SMS-From': from, 'SMS-Content': content } = req.body;

    switch (keyphrase.toUpperCase()) {
      case 'STOP':
        handleOptOut(from);
        break;
      case 'CONFIRM':
        handleConfirmation(from);
        break;
      case 'CANCEL':
        handleCancellation(from);
        break;
      default:
        handleGeneral(from, content);
    }

    res.sendStatus(200);
  });
  ```
</CodeGroup>

## Network codes (SMS-Network)

When present, `SMS-Network` uses MCC/MNC format. For Irish numbers, MCC is always `272`:

| MNC | Carrier  |
| --- | -------- |
| `1` | Vodafone |
| `2` | O2       |
| `3` | Meteor   |

This field may be empty depending on the network and message type.
