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.
https://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"
}
]
}
| Field | Type | Description |
|---|---|---|
list | array | One or more messages. Usually one, but handle several. |
type | string | INBOUND_MESSAGE. |
messageId | integer | Our id for the message. Use it to ignore a repeat delivery. |
sender | string | The mobile number that sent it. |
inboxName | string | The inbox the number belongs to, if any. |
inboxNumber | string | Your number that received it. |
inboxKeyword | string | The keyword matched, if your number uses keywords. Left out otherwise. |
content | string | The message text. |
dateTime | string | When 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
- Verify the signature. Check
X-EM-Signature-V2on every request: see Webhook signatures. - Use HTTPS.
- If you can’t verify signatures, put a long random token in the URL, for example
https://example.com/everymessage/inbound/3f9c…, and reject any request without it. The webhook receiver examples below do this. - Optionally allow only
13.134.161.74, the fixed address our webhooks come from.
Webhook signatures
When a webhook has a signing secret, every inbound message and delivery receipt we POST to it is signed, so you can confirm it came from everymessage and wasn’t altered or replayed. New webhooks get a secret automatically. It’s shown once in the portal under Admin → Webhooks, where you can also generate a new one.
| Header | Value |
|---|---|
X-EM-Timestamp | Unix time in seconds when the request was sent. |
X-EM-Signature-V2 | v2= followed by the lowercase hex HMAC-SHA256 of {X-EM-Timestamp}.{raw body}, keyed with your secret. |
X-EM-Signature | Lowercase hex HMAC-SHA256 of the raw body alone. Kept for existing integrations; it doesn’t protect against replays. |
To verify (use V2)
- Read the raw body. Take the request body as bytes, before any JSON parsing. Re-serialising the JSON would change the bytes and the signature.
- Compute the signature. Join
X-EM-Timestamp, a full stop and the body, compute HMAC-SHA256 with your secret, and hex-encode it in lowercase. - Compare. Compare it with the value after
v2=using a constant-time comparison. - Check the time. Reject the request if
X-EM-Timestampis more than 5 minutes away from your clock.
Each retry is signed again with a new timestamp. Reply with any 2xx status to acknowledge. Tested verifiers in C#, Python and Node.js are under Code examples.
Notes
- If a phone number or WhatsApp account has its own secret, that secret signs its inbound messages instead of the webhook’s.
- PBX-format webhooks (3CX and Yeastar) are signed with
X-Signature-256: sha256=<hex>instead. See Phone systems (PBX). - A callback URL entered directly on a user, number or brand, rather than choosing a webhook, is signed only if that number or account has its own secret.
Collect by API
https://api.everymessage.com/v1/inbound_messagesReturns 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.
| Parameter | Type | Description |
|---|---|---|
inbox | string | Limit to one or more inboxes, by id or name. Repeat it, or separate with commas. Omit, or use *, for all. |
limit | integer | Maximum 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"}
Verify webhook signatures
Set EM_WEBHOOK_SECRET to your webhook’s signing secret. Each example checks X-EM-Signature-V2 and the timestamp on the raw body before parsing the JSON, and returns 401 if either fails. C#: ASP.NET Core minimal API (dotnet new web). Python: pip install fastapi uvicorn. Node.js: npm install express.
C#
// Verify everymessage webhook signatures (X-EM-Signature-V2) in an 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();
// The signing secret shown once when the webhook was created (Admin > Webhooks).
var secret = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("EM_WEBHOOK_SECRET")!);
app.MapPost("/everymessage/webhook", async (HttpRequest request) =>
{
// Verify the exact bytes received: parsing and re-serialising the JSON would change them.
using var buffer = new MemoryStream();
await request.Body.CopyToAsync(buffer);
var body = buffer.ToArray();
if (!Verify(body, request.Headers["X-EM-Timestamp"], request.Headers["X-EM-Signature-V2"], secret))
return Results.Unauthorized();
// Signature checked: now parse and process the JSON.
Console.WriteLine($"Verified webhook: {Encoding.UTF8.GetString(body)}");
return Results.Ok();
});
app.Run();
static bool Verify(byte[] body, string? timestamp, string? signatureHeader, byte[] secret)
{
// Reject anything sent more than 5 minutes ago (or in the future) to stop replays.
if (!long.TryParse(timestamp, out var ts) ||
Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300)
return false;
// HMAC-SHA256 of "{timestamp}.{body}", lowercase hex.
var signed = Encoding.ASCII.GetBytes(timestamp + ".").Concat(body).ToArray();
var expected = Convert.ToHexString(HMACSHA256.HashData(secret, signed)).ToLowerInvariant();
var received = signatureHeader is { } h && h.StartsWith("v2=") ? h[3..] : "";
return CryptographicOperations.FixedTimeEquals(
Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(received));
}
Python
"""Verify everymessage webhook signatures (X-EM-Signature-V2) with FastAPI.
pip install fastapi uvicorn, then run: uvicorn verify_signature:app --host 0.0.0.0 --port 8000
"""
import hashlib
import hmac
import json
import os
import time
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
# The signing secret shown once when the webhook was created (Admin > Webhooks).
SECRET = os.environ["EM_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, timestamp: str, signature_header: str) -> bool:
# Reject anything sent more than 5 minutes ago (or in the future) to stop replays.
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
# HMAC-SHA256 of "{timestamp}.{body}", lowercase hex.
expected = hmac.new(SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature_header.removeprefix("v2="), expected)
@app.post("/everymessage/webhook")
async def webhook(request: Request):
# Verify the exact bytes received: parsing and re-serialising the JSON would change them.
raw_body = await request.body()
if not verify(raw_body,
request.headers.get("X-EM-Timestamp", ""),
request.headers.get("X-EM-Signature-V2", "")):
raise HTTPException(status_code=401)
# Signature checked: now parse and process the JSON.
print("Verified webhook:", json.loads(raw_body))
return {"ok": True}
Node.js
// Verify everymessage webhook signatures (X-EM-Signature-V2) with Express.
// npm install express, then run: node verify_signature.js
const crypto = require('crypto');
const express = require('express');
// The signing secret shown once when the webhook was created (Admin > Webhooks).
const SECRET = process.env.EM_WEBHOOK_SECRET;
function verify(rawBody, timestamp, signatureHeader) {
// Reject anything sent more than 5 minutes ago (or in the future) to stop replays.
if (!/^d+$/.test(timestamp || '') || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
// HMAC-SHA256 of "{timestamp}.{body}", lowercase hex.
const expected = crypto.createHmac('sha256', SECRET).update(timestamp + '.').update(rawBody).digest('hex');
const received = (signatureHeader || '').replace(/^v2=/, '');
return received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}
const app = express();
// express.raw keeps the exact bytes received: parsing and re-serialising the JSON would change them.
app.post('/everymessage/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body, req.get('X-EM-Timestamp'), req.get('X-EM-Signature-V2'))) {
return res.sendStatus(401);
}
// Signature checked: now parse and process the JSON.
console.log('Verified webhook:', JSON.parse(req.body));
res.sendStatus(200);
});
app.listen(8000);
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)
