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.
media_urlsarrayNot 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."
    }
  ]
}
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: MMS isn’t supported.
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.

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}