Phone systems (PBX)

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

POSThttps://api.everymessage.com/v1/pbx/messages
{
  "from": "+447700900456",
  "to": "+447700900123",
  "text": "Your engineer is on the way."
}
FieldTypeDescription
from requiredstringYour everymessage number, in E.164 format with a +.
to requiredstringThe recipient’s mobile number, in E.164 format with a +. One recipient per request.
text requiredstringThe 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_urlsarrayUp 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."
    }
  ]
}
HTTPcodeMeaning
40110004Authentication failed: the API key is missing or not valid.
400 / 42210002Invalid parameter: the body isn’t JSON, or from, to or text is missing.
42210001The to number isn’t valid or can’t be routed.
42210003Unsupported 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.
40210005No permission: not enough credit.
42210005No permission: the account is blocked or the from number isn’t allowed as a sender.
42910006Too many requests: the daily sending limit has been reached.
50010007Service 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"
  ]
}
LimitValue
Files per messageUp to 10
Size per fileUp to 5 MB
TypesImages (JPEG, PNG, GIF, WebP, HEIC, BMP), video (MP4, MOV, 3GP, WebM), audio (MP3, M4A, AAC, AMR, OGG, WAV), PDF and vCard
FetchingPublic URLs only: no private addresses and no redirects, 15-second timeout
StorageCopied to private UK storage and shown only through the recipient’s link. Links stay live for 30 days
TextOptional. 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

GEThttps://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}