Send and receive SMS from a business phone system, using the same message format 3CX and Yeastar expect.
In short: POST from, to and text to https://api.everymessage.com/v1/pbx/messages with your API key as a Bearer token. Replies and delivery reports are posted to your phone system’s webhook as message.received and message.finalized events.
This API exists so phone systems can use everymessage as their SMS provider with no custom code. To set one up, follow the guide for your system: 3CX or Yeastar P-Series. For NetSapiens, use Send SMS instead. For your own software, Send SMS is usually the better fit: it adds batches, scheduling and references.
Authentication
The PBX API uses a Bearer token. The token is your API username and password, as username:password, Base64-encoded. It’s the same value as the apiKey parameter on the v1 API.
Authorization: Bearer base64(username:password)
Send a message
https://api.everymessage.com/v1/pbx/messages{
"from": "+447700900456",
"to": "+447700900123",
"text": "Your engineer is on the way."
}
| Field | Type | Description |
|---|---|---|
from required | string | Your everymessage number, in E.164 format with a +. |
to required | string | The recipient’s mobile number, in E.164 format with a +. One recipient per request. |
text required | string | The message. Sent exactly as typed: plain text uses GSM-7, and anything with emoji or other characters is sent as Unicode. |
media_urls | array | Not supported. A request with media is refused with error 10003 rather than sent without it. |
A delivery report is always requested, so the result is posted to your webhook when the network reports back.
Response
HTTP 200 with the message. data.id is the everymessage message id; the delivery report refers to it as payload.id.
{
"data": {
"record_type": "message",
"direction": "outbound",
"id": "123456",
"type": "SMS",
"from": {
"phone_number": "+447700900456"
},
"to": [
{
"phone_number": "+447700900123",
"status": "queued"
}
],
"text": "Your engineer is on the way.",
"encoding": "GSM-7",
"parts": 1,
"cost": {
"amount": "0.0400",
"currency": "GBP"
},
"received_at": "2026-10-13T09:15:00.000Z",
"errors": []
}
}
Errors
Errors return an HTTP error status and a list of errors, using the codes Yeastar defines:
{
"errors": [
{
"code": "10004",
"title": "Authentication failed",
"detail": "The API key is not valid."
}
]
}
| HTTP | code | Meaning |
|---|---|---|
| 401 | 10004 | Authentication failed: the API key is missing or not valid. |
| 400 / 422 | 10002 | Invalid parameter: the body isn’t JSON, or from, to or text is missing. |
| 422 | 10001 | The to number isn’t valid or can’t be routed. |
| 422 | 10003 | Unsupported media: MMS isn’t supported. |
| 402 | 10005 | No permission: not enough credit. |
| 422 | 10005 | No permission: the account is blocked or the from number isn’t allowed as a sender. |
| 429 | 10006 | Too many requests: the daily sending limit has been reached. |
| 500 | 10007 | Service unavailable. It is safe to retry after a short wait. |
Verify the connection
https://api.everymessage.com/v1/pbx/verify?challenge={{challenge}}An optional check that the API key works, which Yeastar runs from time to time. Send any random string as challenge with your Bearer token. A valid key returns HTTP 200 with the same string as the plain-text body.
Replies and delivery reports
We POST events to the webhook URL your phone system gives you. Send it to us and we set it on your numbers (for replies) and your account (for delivery reports) in the PBX format. Reply with any 2xx status.
Reply: message.received
{
"data": {
"event_type": "message.received",
"id": "8f1d3a52-0c1e-4b7a-9f53-2a6c1e9d4b10",
"occurred_at": "2026-10-13T09:16:04.511Z",
"record_type": "event",
"payload": {
"id": "123789",
"record_type": "message",
"direction": "inbound",
"type": "SMS",
"from": {
"phone_number": "+447700900123"
},
"to": [
{
"phone_number": "+447700900456",
"status": "webhook_delivered"
}
],
"text": "Thanks, see you soon",
"encoding": "GSM-7",
"parts": 1,
"media": [],
"errors": [],
"received_at": "2026-10-13T09:16:04.511Z"
}
},
"meta": {
"attempt": 1,
"delivered_to": "https://your-pbx.example.com/sms/webhook"
}
}
Delivery report: message.finalized
payload.id is the id returned when you sent. to[].status is delivered or delivery_failed; a failure also lists the reason in errors. Yeastar doesn’t use delivery reports, so for Yeastar we send replies only.
{
"data": {
"event_type": "message.finalized",
"id": "c2b9e0f4-5d61-4f0e-8a3c-7e14b2d9a6f1",
"occurred_at": "2026-10-13T09:15:07.102Z",
"record_type": "event",
"payload": {
"id": "123456",
"record_type": "message",
"direction": "outbound",
"type": "SMS",
"to": [
{
"phone_number": "+447700900123",
"status": "delivered"
}
],
"errors": []
}
}
}
Signature
If we set a signing secret on your webhook, every request carries an X-Signature-256 header: sha256= followed by the lowercase hex HMAC-SHA256 of the raw request body, made with the secret. Yeastar checks it automatically when you enter the Secret; 3CX ignores it. To check it yourself, sign the exact bytes you received, not re-serialised JSON.
X-Signature-256: sha256=5d1f0c…e9a4
Code examples
Send a message
Checks the API key with the verify call, then sends one SMS and prints the result or the error.
C#
// Send an SMS through the everymessage PBX API, as 3CX and Yeastar do (.NET 8 or later)
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
// The PBX API key is your API username and password, as "username:password", Base64-encoded.
var apiKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(
$"{Environment.GetEnvironmentVariable("EM_API_KEY")}:{Environment.GetEnvironmentVariable("EM_SECRET_KEY")}"));
using var http = new HttpClient { BaseAddress = new Uri("https://api.everymessage.com/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
// Optional: the connectivity check Yeastar runs. A valid key echoes the challenge back.
var challenge = Guid.NewGuid().ToString("N");
using var verify = await http.GetAsync($"v1/pbx/verify?challenge={challenge}");
Console.WriteLine(verify.IsSuccessStatusCode && await verify.Content.ReadAsStringAsync() == challenge
? "API key OK" : $"Verify failed: HTTP {(int)verify.StatusCode}");
var json = new JsonSerializerOptions(JsonSerializerDefaults.Web);
using var response = await http.PostAsJsonAsync("v1/pbx/messages",
new SendRequest("+447700900456", "+447700900123", "Your engineer is on the way."), json);
if (!response.IsSuccessStatusCode)
{
// Errors come back as { "errors": [ { "code", "title", "detail" } ] }.
var failed = await response.Content.ReadFromJsonAsync<ErrorBody>(json);
var e = failed?.Errors.FirstOrDefault();
throw new InvalidOperationException($"HTTP {(int)response.StatusCode}: {e?.Code} {e?.Title} - {e?.Detail}");
}
var sent = (await response.Content.ReadFromJsonAsync<SendResponse>(json))!.Data;
Console.WriteLine($"Message {sent.Id} to {sent.To[0].PhoneNumber}: {sent.To[0].Status}, {sent.Parts} part(s), {sent.Encoding}");
record SendRequest(string From, string To, string Text);
record SendResponse(SentMessage Data);
record SentMessage(string Id, string Type, List<Party> To, string Encoding, int Parts);
record Party(
[property: System.Text.Json.Serialization.JsonPropertyName("phone_number")] string PhoneNumber,
string? Status);
record ErrorBody(List<ApiError> Errors);
record ApiError(string Code, string Title, string Detail);
Python
"""Send an SMS through the everymessage PBX API, as 3CX and Yeastar do (pip install httpx)."""
import base64
import os
import secrets
import httpx
# The PBX API key is your API username and password, as "username:password", Base64-encoded.
credentials = f"{os.environ['EM_API_KEY']}:{os.environ['EM_SECRET_KEY']}"
api_key = base64.b64encode(credentials.encode()).decode()
with httpx.Client(
base_url="https://api.everymessage.com",
headers={"Authorization": f"Bearer {api_key}"},
timeout=30,
) as client:
# Optional: the connectivity check Yeastar runs. A valid key echoes the challenge back.
challenge = secrets.token_hex(16)
verify = client.get("/v1/pbx/verify", params={"challenge": challenge})
print("API key OK" if verify.status_code == 200 and verify.text == challenge else "Verify failed")
response = client.post("/v1/pbx/messages", json={
"from": "+447700900456",
"to": "+447700900123",
"text": "Your engineer is on the way.",
})
if response.is_error:
# Errors come back as {"errors": [{"code", "title", "detail"}]}.
e = response.json()["errors"][0]
raise RuntimeError(f"HTTP {response.status_code}: {e['code']} {e['title']} - {e['detail']}")
sent = response.json()["data"]
to = sent["to"][0]
print(f"Message {sent['id']} to {to['phone_number']}: {to['status']}, {sent['parts']} part(s), {sent['encoding']}")
Webhook receiver
Checks the signature, then handles replies and delivery reports.
C#
// Receive PBX-format replies and delivery reports, and check the signature (.NET 8 or later)
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// The signing secret we set on your PBX-format webhook.
var secret = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("EM_SIGNING_SECRET")!);
app.MapPost("/pbx/webhook", async (HttpRequest request) =>
{
// Sign the exact bytes received: re-serialising the JSON would change the signature.
using var buffer = new MemoryStream();
await request.Body.CopyToAsync(buffer);
var body = buffer.ToArray();
var expected = "sha256=" + Convert.ToHexString(HMACSHA256.HashData(secret, body)).ToLowerInvariant();
var received = request.Headers["X-Signature-256"].ToString();
if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(received)))
return Results.Unauthorized();
var data = JsonDocument.Parse(body).RootElement.GetProperty("data");
var payload = data.GetProperty("payload");
var to = payload.GetProperty("to")[0];
switch (data.GetProperty("event_type").GetString())
{
case "message.received":
Console.WriteLine($"SMS from {payload.GetProperty("from").GetProperty("phone_number")} " +
$"to {to.GetProperty("phone_number")}: {payload.GetProperty("text")}");
break;
case "message.finalized":
Console.WriteLine($"Message {payload.GetProperty("id")}: {to.GetProperty("status")}");
break;
}
return Results.Ok();
});
app.Run();
Python
"""Receive PBX-format replies and delivery reports, and check the signature.
pip install fastapi uvicorn, then run: uvicorn pbx_webhook:app --host 0.0.0.0 --port 8000
"""
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
# The signing secret we set on your PBX-format webhook.
SECRET = os.environ["EM_SIGNING_SECRET"].encode()
@app.post("/pbx/webhook")
async def pbx_webhook(request: Request):
# Sign the exact bytes received: re-serialising the JSON would change the signature.
body = await request.body()
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Signature-256", "")):
raise HTTPException(status_code=401)
data = json.loads(body)["data"]
payload = data["payload"]
to = payload["to"][0]
if data["event_type"] == "message.received":
print(f"SMS from {payload['from']['phone_number']} to {to['phone_number']}: {payload['text']}")
elif data["event_type"] == "message.finalized":
print(f"Message {payload['id']}: {to['status']}")
return {"ok": True}
