DNO Custom Provider Guide
PBXware's DNO (Do Not Originate) service validates caller IDs against a DNO list before allowing calls to go out. Out of the box it integrates with the Somos registry, but the Custom provider option lets you connect it to any DNO list provider by placing a middleware application between dno-service and your provider.
How It Works
Your middleware sits between dno-service and your DNO provider. For every validation request, dno-service calls your middleware, which translates the request into whatever format your provider expects, performs the lookup, and maps the result back to one of the three statuses dno-service understands.
PBXware → dno-service → [Your Middleware] → Your DNO Provider
- Configure dno-service - set the provider to
customand pointapi_endpointat your middleware - Receive the lookup request - dno-service POSTs a JSON body with the E.164-normalised number and service type
- Perform the lookup - forward the request to your provider in their required format
- Return the result - respond with one of the three recognised DNO status values
Prerequisites
- A middleware application with an HTTP endpoint reachable from the dno-service host
- Access to the PBXware administration interface or dno-service API to configure the DNO settings
Configure dno-service
Via the PBXware Admin Interface
Navigate to Settings → DNO → Configuration.
| Field | Description |
|---|---|
| Enable DNO | Set to Yes to enable DNO caller ID validation |
| DNO Provider | Select Custom to use your own middleware |
| API Endpoint | Full URL of your middleware endpoint - dno-service POSTs lookup requests here |
| API Key | Optional. When set, dno-service sends it as a Bearer token in the Authorization header |
| Cache TTL (minutes) | How long to cache lookup results. Set to 0 to disable caching. failed_dno_lookup results are never cached |
| Enable Notifications | Enables or disables email notifications for DNO events |
| Notification Email | One or more comma-separated email addresses to receive notifications |
| Notification Interval (minutes) | Minimum time between notifications of the same type |
Click Save. Changes take effect immediately on the next validation request - no restart required.
Via the API
PUT /api/system/v2/dno/configuration
Content-Type: application/json
{
"enabled": true,
"provider": "custom",
"api_endpoint": "https://your-middleware.example.com/validate",
"api_key": "your-api-key",
"cache_ttl": 5,
"notifications_enabled": true,
"notifications_email": ["admin@example.com", "user@example.com"],
"notifications_interval": 60
}
| Field | Description |
|---|---|
enabled | Set to true to enable DNO caller ID validation |
provider | Set to "custom" to use your own middleware |
api_endpoint | Full URL of your middleware endpoint - dno-service POSTs lookup requests here |
api_key | Optional. When set, dno-service sends it as a Bearer token in the Authorization header |
cache_ttl | How long to cache lookup results, in minutes. Set to 0 to disable caching. failed_dno_lookup results are never cached |
notifications_enabled | Enables or disables email notifications for DNO events |
notifications_email | Array of email addresses to receive notifications |
notifications_interval | Minimum time between notifications of the same type, in minutes |
Changes take effect immediately on the next validation request - no restart required.
API Reference
Request: dno-service → Middleware
dno-service sends a POST to your api_endpoint:
POST {api_endpoint}
Content-Type: application/json
Accept: application/json
Authorization: Bearer {api_key}
The Authorization header is only included when api_key is configured.
{
"number": "+12408052400",
"type": "voice"
}
| Field | Description |
|---|---|
number | E.164-normalised phone number to look up |
type | "voice" or "text". Defaults to "voice" if the original request did not include a type |
Response: Middleware → dno-service
Your middleware must respond with HTTP 200 and a JSON body:
{
"status": "not_on_dno_list"
}
The status field must be exactly one of:
status | Meaning |
|---|---|
not_on_dno_list | Number is not found on the DNO list - allow the call |
found_on_dno_list | Number is on the DNO list - block the call |
failed_dno_lookup | Lookup could not be completed - allow the call |
Any unrecognised status value causes dno-service to treat the response as a provider error and return a 502 response to PBXware.
Error Responses
If your middleware cannot process the request, respond with the appropriate HTTP status and a JSON error body:
{
"error": "description of what went wrong"
}
| HTTP Status | When to use | Effect on dno-service |
|---|---|---|
400 Bad Request | The request was malformed or missing required fields | dno-service returns a 502 to its caller |
401 Unauthorized | The Authorization header is missing or invalid | dno-service returns a 502 to its caller |
Any status other than 200, 400, or 401 is treated as an unexpected provider error and also results in a 502.
Middleware Examples
The examples below show a minimal middleware that receives a lookup request from dno-service, performs a placeholder provider call, and returns the mapped result.
- Node.js
- Python
- Go
- Rust
Install: npm install express
import express from 'express';
const API_KEY = process.env.API_KEY ?? '';
const app = express();
app.use(express.json());
async function lookupWithProvider(number, type) {
// Replace with your actual provider integration.
// Return one of: 'not_on_dno_list', 'found_on_dno_list', 'failed_dno_lookup'
console.log(`Looking up ${number} (${type}) with provider`);
return 'not_on_dno_list';
}
app.post('/validate', async (req, res) => {
try {
if (API_KEY) {
const authHeader = req.headers['authorization'] ?? '';
if (authHeader !== `Bearer ${API_KEY}`) {
return res.status(401).json({ error: 'Unauthorized' });
}
}
const { number, type } = req.body;
if (!number) {
return res.status(400).json({ error: 'number is required' });
}
const status = await lookupWithProvider(number, type ?? 'voice');
res.json({ status });
} catch (e) {
console.error(e);
res.status(500).json({ error: e.message });
}
});
app.listen(8080, () => console.log('Middleware running on http://localhost:8080'));
Install: pip install flask
import os
from flask import Flask, request, jsonify
API_KEY = os.getenv('API_KEY', '')
app = Flask(__name__)
def lookup_with_provider(number, service_type):
# Replace with your actual provider integration.
# Return one of: 'not_on_dno_list', 'found_on_dno_list', 'failed_dno_lookup'
print(f"Looking up {number} ({service_type}) with provider")
return 'not_on_dno_list'
@app.route('/validate', methods=['POST'])
def validate():
try:
if API_KEY:
auth_header = request.headers.get('Authorization', '')
if auth_header != f'Bearer {API_KEY}':
return jsonify({'error': 'Unauthorized'}), 401
data = request.get_json(silent=True) or {}
number = data.get('number')
if not number:
return jsonify({'error': 'number is required'}), 400
service_type = data.get('type', 'voice')
status = lookup_with_provider(number, service_type)
return jsonify({'status': status})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(port=8080)
No external dependencies required.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
var apiKey = os.Getenv("API_KEY")
type lookupRequest struct {
Number string `json:"number"`
Type string `json:"type"`
}
type lookupResponse struct {
Status string `json:"status"`
}
type errorResponse struct {
Error string `json:"error"`
}
func lookupWithProvider(number, serviceType string) (string, error) {
// Replace with your actual provider integration.
// Return one of: "not_on_dno_list", "found_on_dno_list", "failed_dno_lookup"
fmt.Printf("Looking up %s (%s) with provider\n", number, serviceType)
return "not_on_dno_list", nil
}
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(body)
}
func validateHandler(w http.ResponseWriter, r *http.Request) {
if apiKey != "" {
auth := r.Header.Get("Authorization")
if auth != "Bearer "+apiKey {
writeJSON(w, http.StatusUnauthorized, errorResponse{Error: "Unauthorized"})
return
}
}
var req lookupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "invalid request body"})
return
}
if req.Number == "" {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "number is required"})
return
}
if req.Type == "" {
req.Type = "voice"
}
status, err := lookupWithProvider(req.Number, req.Type)
if err != nil {
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, lookupResponse{Status: status})
}
func main() {
http.HandleFunc("/validate", validateHandler)
log.Println("Middleware running on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Add to Cargo.toml:
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use axum::{
body::Bytes,
extract::Json,
http::{HeaderMap, StatusCode},
response::IntoResponse,
routing::post,
Router,
};
use serde::Deserialize;
use std::env;
fn default_type() -> String {
"voice".to_string()
}
#[derive(Deserialize)]
struct LookupRequest {
number: String,
#[serde(default = "default_type")]
r#type: String,
}
fn lookup_with_provider(number: &str, service_type: &str) -> String {
// Replace with your actual provider integration.
// Return one of: "not_on_dno_list", "found_on_dno_list", "failed_dno_lookup"
println!("Looking up {number} ({service_type}) with provider");
"not_on_dno_list".to_string()
}
async fn validate(headers: HeaderMap, body: Bytes) -> impl IntoResponse {
let api_key = env::var("API_KEY").unwrap_or_default();
if !api_key.is_empty() {
let auth = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if auth != format!("Bearer {api_key}") {
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({ "error": "Unauthorized" })),
);
}
}
let req: LookupRequest = match serde_json::from_slice(&body) {
Ok(r) => r,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "invalid request body" })),
);
}
};
if req.number.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "number is required" })),
);
}
let status = lookup_with_provider(&req.number, &req.r#type);
(StatusCode::OK, Json(serde_json::json!({ "status": status })))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/validate", post(validate));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
println!("Middleware running on http://localhost:8080");
axum::serve(listener, app).await.unwrap();
}
Running the Middleware
- Node.js
- Python
- Go
- Rust
Save as server.mjs (or .js with "type": "module" in package.json), then:
API_KEY=your-api-key node server.mjs
Save as server.py, then:
API_KEY=your-api-key python server.py
Save as main.go in a Go module, then:
API_KEY=your-api-key go run main.go
Inside your Cargo project:
API_KEY=your-api-key cargo run --release
Once your middleware is running and dno-service is configured to point at it, validation requests will flow through your middleware automatically. Set api_endpoint to the full URL of your /validate route (e.g. https://your-middleware.example.com/validate) and set api_key to the same value as your API_KEY environment variable.