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. Optional when media_urls is included. |
media_urls | array | Up to 10 public URLs of pictures, video, audio, PDFs or contact cards. Delivered as an SMS with a personal link to the media: see Picture messages. |
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: a file couldn’t be fetched, isn’t a supported type, is over 5 MB, or there are more than 10. Nothing is sent. |
| 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. |
Picture messages
A request with media_urls is delivered as an ordinary SMS: the text followed by a personal, secure link (for example https://rcsview.uk/AbC123xYz789) that opens a page showing the text and every file. It reaches every phone and network, which MMS still doesn’t in the UK. Delivery reports work as for SMS, and the link is counted in the message length. A picture message is charged as an SMS plus a media hosting fee.
{
"from": "+447700900456",
"to": "+447700900123",
"text": "Here's the quote you asked for.",
"media_urls": [
"https://files.example.com/quote-1042.pdf"
]
}
| Limit | Value |
|---|---|
| Files per message | Up to 10 |
| Size per file | Up to 5 MB |
| Types | Images (JPEG, PNG, GIF, WebP, HEIC, BMP), video (MP4, MOV, 3GP, WebM), audio (MP3, M4A, AAC, AMR, OGG, WAV), PDF and vCard |
| Fetching | Public URLs only: no private addresses and no redirects, 15-second timeout |
| Storage | Copied to private UK storage and shown only through the recipient’s link. Links stay live for 30 days |
| Text | Optional. Without it we send a short line such as "You’ve been sent a picture. View it here:" |
Processing is all or nothing: if any file can’t be used, the request fails with 10003 and the reason, and nothing is sent.
Sender name
A phone system always sends from a number. If one of your numbers has a sender name set (up to 11 characters, for example BrightDent), messages sent from that number through this API go out from the name instead. Set it in the everymessage portal under Numbers → edit the number → PBX sender name, or ask us to set one.
- The send response still shows your number in
from, so the phone system keeps the conversation together. - The delivery report shows the name as the sender. Match reports on
payload.id, as 3CX does. - Names are one-way: customers can’t reply to them. The name must follow UK sender ID rules.
- This applies to the PBX API only. Send SMS sets the sender per message instead.
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}
