Receive SMS

Receive the replies people send to your numbers.

Choose webhook to have each reply posted to your server within seconds, or collect to fetch new replies with a GET request when it suits you. Replies to an alphanumeric sender such as everymessage are not possible over SMS, so send from a number if you want answers.

Webhook

We POST new messages to your URL as JSON. Tell us the URL and we will connect it to your numbers.

POSThttps://your-server.example.com/…
{
  "list": [
    {
      "type": "INBOUND_MESSAGE",
      "messageId": 90321,
      "sender": "+447700900123",
      "inboxName": "support",
      "inboxNumber": "447700900000",
      "content": "Yes, 10:30 is fine",
      "dateTime": "2026-10-11T09:31:12.408Z"
    }
  ]
}
FieldTypeDescription
listarrayOne or more messages. Usually one, but handle several.
typestringINBOUND_MESSAGE.
messageIdintegerOur id for the message. Use it to ignore a repeat delivery.
senderstringThe mobile number that sent it.
inboxNamestringThe inbox the number belongs to, if any.
inboxNumberstringYour number that received it.
inboxKeywordstringThe keyword matched, if your number uses keywords. Left out otherwise.
contentstringThe message text.
dateTimestringWhen it was received, ISO 8601 in UTC.

Your response

Reply with any 2xx status within 10 seconds. If we get an error or no answer, we retry the batch several times over the next few minutes, so make your handler safe to run twice for the same messageId. Older integrations also return {"echo":"ACK"}; that is accepted but not required.

Securing your webhook

  • Use HTTPS.
  • Put a long random token in the URL, for example https://example.com/everymessage/inbound/3f9c…, and reject any request without it. The code examples below do this.
  • Optionally allow only 13.134.161.74, the fixed address our webhooks come from.

Collect by API

GEThttps://api.everymessage.com/v1/inbound_messages

Returns messages you have not collected yet, and marks them collected. Each message is returned once, so store it before your next call. When there is nothing new, responseStatusReason is NO_CURRENT_ENTRIES. Messages wait until you collect them; they are not deleted if you poll less often.

ParameterTypeDescription
inboxstringLimit to one or more inboxes, by id or name. Repeat it, or separate with commas. Omit, or use *, for all.
limitintegerMaximum messages per call. Default 500, maximum 1000. Call again until you get NO_CURRENT_ENTRIES to clear a backlog.

Parameters can also be sent as a JSON body with POST /v1/inbound_messages.

{
  "apiName": "everymessage Web API",
  "apiVersion": "1.0.2",
  "apiTime": "11 October 2026 10:32:00",
  "apiState": "RUNNING",
  "responseStatusCode": 200,
  "processingTime": 12,
  "body": [
    {
      "type": "INBOUND_MESSAGE",
      "messageId": 90321,
      "sender": "447700900123",
      "inboxName": "support",
      "inboxNumber": "447700900000",
      "content": "Yes, 10:30 is fine",
      "dateTime": "2026-10-11 10:31:12"
    }
  ]
}

The fields match the webhook, except that dateTime is UK local time in the form yyyy-MM-dd HH:mm:ss and sender has no +. Use GET /v1/get_inboxes to list your inboxes.

Code examples

Webhook receiver

Set EmWebhookToken (C#) or EM_WEBHOOK_TOKEN (Python) to a long random value and give us the URL including it. The same app also receives delivery receipts. The C# example is an ASP.NET Core minimal API (dotnet new web); the Python example needs pip install fastapi uvicorn.

C#

// Receive inbound SMS and delivery receipts by webhook (ASP.NET Core minimal API, .NET 8 or later)
using System.Security.Cryptography;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// A long random token in the URL stops anyone else posting to your endpoints.
var token = Encoding.UTF8.GetBytes(app.Configuration["EmWebhookToken"]
            ?? throw new InvalidOperationException("Set EmWebhookToken"));
bool Valid(string key) => CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(key), token);

app.MapPost("/everymessage/inbound/{key}", (string key, CallbackBatch<InboundMessage> batch,
    ILogger<Program> log) =>
{
    if (!Valid(key)) return Results.NotFound();

    foreach (var m in batch.List)
        log.LogInformation("SMS {Id} from {Sender} to {Inbox}: {Content}",
            m.MessageId, m.Sender, m.InboxNumber, m.Content);

    // Any 2xx response tells everymessage the batch was received.
    return Results.Ok(new { echo = "ACK" });
});

app.MapPost("/everymessage/receipts/{key}", (string key, CallbackBatch<DeliveryReceipt> batch,
    ILogger<Program> log) =>
{
    if (!Valid(key)) return Results.NotFound();

    foreach (var r in batch.List)
        log.LogInformation("Message {Id} ({Reference}) to {Recipient}: {Status}",
            r.MessageId, r.Reference, r.Recipient, r.Status);

    return Results.Ok(new { echo = "ACK" });
});

app.Run();

record CallbackBatch<T>(IReadOnlyList<T> List);
record InboundMessage(string Type, long MessageId, string Sender, string? InboxName,
    string? InboxNumber, string? InboxKeyword, string Content, DateTimeOffset DateTime);
record DeliveryReceipt(string Type, long MessageId, string? Reference, string Recipient,
    string Status, DateTimeOffset DateTime);

Python

"""Receive inbound SMS and delivery receipts by webhook (pip install fastapi uvicorn).

Run with:  uvicorn webhooks:app --host 0.0.0.0 --port 8000
"""
import os
import secrets
from datetime import datetime

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

app = FastAPI()

# A long random token in the URL stops anyone else posting to your endpoints.
TOKEN = os.environ["EM_WEBHOOK_TOKEN"]


class InboundMessage(BaseModel):
    type: str
    messageId: int
    sender: str
    inboxName: str | None = None
    inboxNumber: str | None = None
    inboxKeyword: str | None = None
    content: str
    dateTime: datetime


class DeliveryReceipt(BaseModel):
    type: str
    messageId: int
    reference: str | None = None
    recipient: str
    status: str
    dateTime: datetime


class InboundBatch(BaseModel):
    items: list[InboundMessage] = Field(alias="list")


class ReceiptBatch(BaseModel):
    items: list[DeliveryReceipt] = Field(alias="list")


def check(key: str) -> None:
    if not secrets.compare_digest(key, TOKEN):
        raise HTTPException(status_code=404)


@app.post("/everymessage/inbound/{key}")
def inbound(key: str, batch: InboundBatch):
    check(key)
    for m in batch.items:
        print(f"SMS {m.messageId} from {m.sender} to {m.inboxNumber}: {m.content}")
    # Any 2xx response tells everymessage the batch was received.
    return {"echo": "ACK"}


@app.post("/everymessage/receipts/{key}")
def receipts(key: str, batch: ReceiptBatch):
    check(key)
    for r in batch.items:
        print(f"Message {r.messageId} ({r.reference}) to {r.recipient}: {r.status}")
    return {"echo": "ACK"}

Collect by API

Polls once a minute and prints each new message.

C#

// Collect inbound SMS replies with the everymessage v1 API (.NET 8 or later)
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;

var apiKey = Environment.GetEnvironmentVariable("EM_API_KEY")!;
var secretKey = Environment.GetEnvironmentVariable("EM_SECRET_KEY")!;

using var http = new HttpClient { BaseAddress = new Uri("https://api.everymessage.com/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiKey}:{secretKey}")));

using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
do
{
    // Each message is returned once: store it before the next call.
    var result = await http.GetFromJsonAsync<ApiEnvelope<List<InboundMessage>>>(
        "v1/inbound_messages?limit=500")
        ?? throw new InvalidOperationException("Empty response from everymessage");

    switch (result.ResponseStatusReason)
    {
        case null:
            foreach (var m in result.Body ?? [])
                Console.WriteLine($"{m.DateTime} from {m.Sender} to {m.InboxNumber}: {m.Content}");
            break;
        case "NO_CURRENT_ENTRIES":
            break; // nothing new
        default:
            throw new InvalidOperationException($"Collect failed: {result.ResponseStatusReason}");
    }
}
while (await timer.WaitForNextTickAsync());

record ApiEnvelope<T>(int ResponseStatusCode, string? ResponseStatusReason, int ProcessingTime, T? Body);
record InboundMessage(string Type, long MessageId, string Sender, string? InboxName,
    string? InboxNumber, string? InboxKeyword, string Content, string DateTime);

Python

"""Collect inbound SMS replies with the everymessage v1 API (Python 3.10+, pip install httpx)."""
import os
import time

import httpx

with httpx.Client(
    base_url="https://api.everymessage.com",
    auth=(os.environ["EM_API_KEY"], os.environ["EM_SECRET_KEY"]),
    timeout=30,
) as client:
    while True:
        # Each message is returned once: store it before the next call.
        response = client.get("/v1/inbound_messages", params={"limit": 500})
        response.raise_for_status()
        result = response.json()

        reason = result.get("responseStatusReason")
        if reason is None:
            for m in result["body"]:
                print(f"{m['dateTime']} from {m['sender']} to {m['inboxNumber']}: {m['content']}")
        elif reason != "NO_CURRENT_ENTRIES":
            raise RuntimeError(f"Collect failed: {reason}")

        time.sleep(60)