Custom SMS Connector Guide
SMS Connector is a flexible, standardized interface for sending and receiving SMS and MMS messages on PBXware using custom SMS trunks. Available since PBXware 7.2, it lets you connect PBXware's SMS Service to any SMS provider - including providers not natively supported - by building a middleware application.
How It Works
The SMS Connector sits between PBXware and your SMS provider. Your middleware handles both directions:
Outbound: gloCOM → PBXware → [Your Middleware] → SMS Provider
Inbound: SMS Provider → [Your Middleware] → PBXware → gloCOM
- Configure a Custom trunk - point PBXware at your middleware's Webhook URL and set a shared Auth Token
- Outbound messages - PBXware POSTs each outgoing SMS to your Webhook URL; your middleware forwards it to the provider
- Inbound messages - your middleware receives messages from the provider and POSTs them to PBXware's
/smsservice/connectorendpoint
Prerequisites
- PBXware 7.2 or later
- SMS Connector feature enabled in your PBXware license
- A middleware application with a publicly reachable HTTPS endpoint
Configure a Custom SMS Trunk
Multi-Tenant Edition: System → SMS → Trunks → Add SMS Trunk
CC/Business Edition: SMS → Trunks → Add SMS Trunk
| Field | Description |
|---|---|
| Enable | Toggle to enable or disable the trunk |
| Name | Display name for the trunk (e.g. Custom SMS Provider) |
| Provider | Select Custom from the dropdown |
| Webhook URL | URL of your middleware - PBXware POSTs outbound messages here |
| Auth Token | Shared secret used to authenticate requests between PBXware and your middleware. Enter manually or generate one using the icon on the right. |
| Description | Optional notes for administrators |
Note: The Custom option only appears in the Provider dropdown when the SMS Connector feature is enabled in your PBXware license.
Click Save to create the trunk. Once the middleware is operational, the custom trunk works identically to any other SMS trunk on PBXware.
API Reference
Outbound: PBXware → Middleware
When a user sends an SMS via gloCOM, PBXware POSTs to your Webhook URL:
POST {webhook_url}
Content-Type: application/json
Authorization: Bearer {auth_token}
{
"from": "string",
"to": "string",
"text": "string",
"media_urls": ["string"]
}
| Field | Description |
|---|---|
from | Sender's phone number |
to | Recipient's phone number |
text | Message content |
media_urls | Media attachment URLs (MMS); empty array for SMS |
Your middleware must respond with one of:
{ "status": "success", "message": "" }
{ "status": "error", "message": "description of what went wrong" }
Note: Any HTTP response status other than
200causes PBXware to treat the message as failed.
Inbound: Middleware → PBXware
To deliver a message received from the provider into PBXware, POST to:
POST /smsservice/connector
Content-Type: application/json
Authorization: Bearer {auth_token}
{
"from": "string",
"to": "string",
"text": "string",
"media_urls": ["string"]
}
| Status | Meaning |
|---|---|
200 OK | SMS received successfully |
401 Unauthorized | Missing or invalid Auth Token |
500 Internal Server Error | Server-side error - details in response body |
Examples
Outbound Handler (PBXware → Middleware → Provider)
Accepts outbound messages from PBXware, validates the Auth Token, and forwards to your SMS provider.
- Node.js
- Python
- Go
- Rust
Install: npm install express
import express from 'express';
const AUTH_TOKEN = process.env.AUTH_TOKEN ?? 'YOUR_AUTH_TOKEN';
const app = express();
app.use(express.json());
function sendMessageToProvider(messageData) {
// Replace with actual provider integration
console.log('Forwarding to provider:', messageData);
return { status: 'success', message: '' };
}
app.post('/messages', (req, res) => {
try {
const authHeader = req.headers['authorization'] ?? '';
if (authHeader !== `Bearer ${AUTH_TOKEN}`) {
return res.status(401).json({ status: 'error', message: 'Unauthorized' });
}
if (req.headers['content-type'] !== 'application/json') {
return res.status(400).json({ status: 'error', message: 'Invalid content type' });
}
const result = sendMessageToProvider(req.body);
res.json(result);
} catch (e) {
res.status(500).json({ status: 'error', message: e.message });
}
});
app.listen(5000, () => console.log('Server running on http://localhost:5000'));
Install: pip install flask
import os
from flask import Flask, request, jsonify
AUTH_TOKEN = os.getenv('AUTH_TOKEN', 'YOUR_AUTH_TOKEN')
app = Flask(__name__)
def send_message_to_provider(message_data):
# Replace with actual provider integration
print("Forwarding to provider:", message_data)
return {"status": "success", "message": ""}
@app.route('/messages', methods=['POST'])
def messages():
try:
auth_header = request.headers.get('Authorization', '')
if auth_header != f'Bearer {AUTH_TOKEN}':
return jsonify({"status": "error", "message": "Unauthorized"}), 401
if request.headers.get('Content-Type') != 'application/json':
return jsonify({"status": "error", "message": "Invalid content type"}), 400
message_data = request.get_json()
result = send_message_to_provider(message_data)
return jsonify(result)
except Exception as e:
return jsonify({"status": "error", "message": str(e)}), 500
if __name__ == '__main__':
app.run(debug=True)
No external dependencies required.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
)
var authToken = getEnv("AUTH_TOKEN", "YOUR_AUTH_TOKEN")
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
type Message struct {
From string `json:"from"`
To string `json:"to"`
Text string `json:"text"`
MediaURLs []string `json:"media_urls"`
}
type Response struct {
Status string `json:"status"`
Message string `json:"message"`
}
func sendMessageToProvider(msg Message) Response {
// Replace with actual provider integration
fmt.Printf("Forwarding to provider: %+v\n", msg)
return Response{Status: "success", Message: ""}
}
func messagesHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
authHeader := r.Header.Get("Authorization")
if !strings.EqualFold(authHeader, "Bearer "+authToken) {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(Response{Status: "error", Message: "Unauthorized"})
return
}
if r.Header.Get("Content-Type") != "application/json" {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(Response{Status: "error", Message: "Invalid content type"})
return
}
var msg Message
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(Response{Status: "error", Message: err.Error()})
return
}
json.NewEncoder(w).Encode(sendMessageToProvider(msg))
}
func main() {
http.HandleFunc("/messages", messagesHandler)
log.Println("Server running on http://localhost:5000")
log.Fatal(http.ListenAndServe(":5000", nil))
}
Add to Cargo.toml:
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
use axum::{
extract::Json,
http::{HeaderMap, StatusCode},
response::IntoResponse,
routing::post,
Router,
};
use serde::{Deserialize, Serialize};
const AUTH_TOKEN: &str = "YOUR_AUTH_TOKEN";
#[derive(Deserialize)]
struct Message {
from: String,
to: String,
text: String,
media_urls: Vec<String>,
}
#[derive(Serialize)]
struct Response {
status: String,
message: String,
}
fn send_message_to_provider(msg: &Message) -> Response {
// Replace with actual provider integration
println!("Forwarding to provider: {} -> {}: {}", msg.from, msg.to, msg.text);
Response { status: "success".into(), message: "".into() }
}
async fn messages(headers: HeaderMap, Json(msg): Json<Message>) -> impl IntoResponse {
let auth = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if auth != format!("Bearer {AUTH_TOKEN}") {
return (
StatusCode::UNAUTHORIZED,
Json(Response { status: "error".into(), message: "Unauthorized".into() }),
);
}
(StatusCode::OK, Json(send_message_to_provider(&msg)))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/messages", post(messages));
let listener = tokio::net::TcpListener::bind("0.0.0.0:5000").await.unwrap();
println!("Server running on http://localhost:5000");
axum::serve(listener, app).await.unwrap();
}
Inbound Handler (Provider → Middleware → PBXware)
Accepts messages from your SMS provider, maps the provider's field names to PBXware's format, and forwards to PBXware.
- Node.js
- Python
- Go
- Rust
Install: npm install express node-fetch
import express from 'express';
import fetch from 'node-fetch';
const PBXWARE_URL = 'https://my.pbxware.com/smsservice/connector';
const AUTH_TOKEN = process.env.AUTH_TOKEN ?? 'YOUR_AUTH_TOKEN';
const app = express();
app.use(express.json());
function parseProviderRequest(body) {
return {
from: body.sender_number ?? '',
to: body.recipient_number ?? '',
text: body.message_content ?? '',
media_urls: body.media_urls ?? [],
};
}
app.post('/messages', async (req, res) => {
try {
if (req.headers['content-type'] !== 'application/json') {
return res.status(400).json({ status: 'error', message: 'Invalid content type' });
}
const formatted = parseProviderRequest(req.body);
const response = await fetch(PBXWARE_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${AUTH_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(formatted),
});
if (response.ok) {
res.json({ status: 'success', message: 'Message sent successfully' });
} else {
const text = await response.text();
res.status(response.status).json({ status: 'error', message: `Failed to send message. Error: ${text}` });
}
} catch (e) {
res.status(500).json({ status: 'error', message: e.message });
}
});
app.listen(5001, () => console.log('Server running on http://localhost:5001'));
Install: pip install flask requests
import os
from flask import Flask, request, jsonify
import requests
PBXWARE_URL = 'https://my.pbxware.com/smsservice/connector'
AUTH_TOKEN = os.getenv('AUTH_TOKEN', 'YOUR_AUTH_TOKEN')
app = Flask(__name__)
def parse_provider_request():
if request.headers.get('Content-Type') != 'application/json':
raise ValueError("Invalid content type")
data = request.get_json()
return {
"from": data.get("sender_number", ""),
"to": data.get("recipient_number", ""),
"text": data.get("message_content", ""),
"media_urls": data.get("media_urls", [])
}
@app.route('/messages', methods=['POST'])
def messages():
try:
formatted_message = parse_provider_request()
response = requests.post(
PBXWARE_URL,
json=formatted_message,
headers={
'Authorization': f'Bearer {AUTH_TOKEN}',
'Content-Type': 'application/json'
}
)
if response.ok:
return jsonify({"status": "success", "message": "Message sent successfully"})
return jsonify({"status": "error", "message": f"Failed to send message. Error: {response.text}"}), response.status_code
except ValueError as ve:
return jsonify({"status": "error", "message": str(ve)}), 400
except Exception as e:
return jsonify({"status": "error", "message": str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, port=5001)
No external dependencies required.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
)
var (
pbxwareURL = "https://my.pbxware.com/smsservice/connector"
authToken = getEnv("AUTH_TOKEN", "YOUR_AUTH_TOKEN")
)
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
type ProviderMessage struct {
SenderNumber string `json:"sender_number"`
RecipientNumber string `json:"recipient_number"`
MessageContent string `json:"message_content"`
MediaURLs []string `json:"media_urls"`
}
type PBXMessage struct {
From string `json:"from"`
To string `json:"to"`
Text string `json:"text"`
MediaURLs []string `json:"media_urls"`
}
type Response struct {
Status string `json:"status"`
Message string `json:"message"`
}
func messagesHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Header.Get("Content-Type") != "application/json" {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(Response{Status: "error", Message: "Invalid content type"})
return
}
var provider ProviderMessage
if err := json.NewDecoder(r.Body).Decode(&provider); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(Response{Status: "error", Message: err.Error()})
return
}
msg := PBXMessage{
From: provider.SenderNumber,
To: provider.RecipientNumber,
Text: provider.MessageContent,
MediaURLs: provider.MediaURLs,
}
body, _ := json.Marshal(msg)
req, _ := http.NewRequest(http.MethodPost, pbxwareURL, bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+authToken)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(Response{Status: "error", Message: err.Error()})
return
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
json.NewEncoder(w).Encode(Response{Status: "success", Message: "Message sent successfully"})
} else {
respBody, _ := io.ReadAll(resp.Body)
w.WriteHeader(resp.StatusCode)
json.NewEncoder(w).Encode(Response{Status: "error", Message: fmt.Sprintf("Failed to send message. Error: %s", respBody)})
}
}
func main() {
http.HandleFunc("/messages", messagesHandler)
log.Println("Server running on http://localhost:5001")
log.Fatal(http.ListenAndServe(":5001", nil))
}
Add to Cargo.toml:
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
use axum::{extract::Json, http::StatusCode, response::IntoResponse, routing::post, Router};
use reqwest::Client;
use serde::{Deserialize, Serialize};
const PBXWARE_URL: &str = "https://my.pbxware.com/smsservice/connector";
const AUTH_TOKEN: &str = "YOUR_AUTH_TOKEN";
#[derive(Deserialize)]
struct ProviderMessage {
sender_number: Option<String>,
recipient_number: Option<String>,
message_content: Option<String>,
media_urls: Option<Vec<String>>,
}
#[derive(Serialize)]
struct PBXMessage {
from: String,
to: String,
text: String,
media_urls: Vec<String>,
}
#[derive(Serialize)]
struct Response {
status: String,
message: String,
}
async fn messages(Json(provider): Json<ProviderMessage>) -> impl IntoResponse {
let msg = PBXMessage {
from: provider.sender_number.unwrap_or_default(),
to: provider.recipient_number.unwrap_or_default(),
text: provider.message_content.unwrap_or_default(),
media_urls: provider.media_urls.unwrap_or_default(),
};
let client = Client::new();
match client
.post(PBXWARE_URL)
.bearer_auth(AUTH_TOKEN)
.json(&msg)
.send()
.await
{
Ok(resp) if resp.status().is_success() => (
StatusCode::OK,
Json(Response { status: "success".into(), message: "Message sent successfully".into() }),
),
Ok(resp) => {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
(
StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
Json(Response { status: "error".into(), message: format!("Failed to send message. Error: {text}") }),
)
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(Response { status: "error".into(), message: e.to_string() }),
),
}
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/messages", post(messages));
let listener = tokio::net::TcpListener::bind("0.0.0.0:5001").await.unwrap();
println!("Server running on http://localhost:5001");
axum::serve(listener, app).await.unwrap();
}
Running the Server
- Node.js
- Python
- Go
- Rust
Save as server.mjs (or .js with "type": "module" in package.json), then:
AUTH_TOKEN=your_secret_token node server.mjs
Save as server.py, then:
AUTH_TOKEN=your_secret_token python server.py
Save as main.go in a Go module, then:
AUTH_TOKEN=your_secret_token go run main.go
Inside your Cargo project:
AUTH_TOKEN=your_secret_token cargo run --release
Tip: Replace
YOUR_AUTH_TOKENwith the Auth Token configured on the PBXware trunk, or set it via theAUTH_TOKENenvironment variable. Both middleware servers must use the same token as configured in PBXware.