RustPBX provides a comprehensive set of HTTP APIs and Webhooks designed to make it a fully programmable Software Defined PBX (SD-PBX). This guide details how to integrate your external business logic (CRM, ERP, AI assistants, billing systems) with RustPBX.
RustPBX interacts with external systems in two ways:
- Inbound API (REST): Your system calls RustPBX to manage resources (extensions, trunks) or control active calls.
- Outbound Webhooks: RustPBX calls your system to make routing decisions, report events, or authenticate users.
| Mechanism | Direction | Type | Use Case |
|---|---|---|---|
| Console API | Inbound | REST | CRUD extensions, download recordings, system config |
| Active Call Control | Inbound | REST | Live hangup, transfer, mute, force-accept |
| AMI API | Inbound | REST | Health checks, hot-reload, raw dialog inspection |
| HTTP Router | Outbound | Webhook | Dynamic call routing (per-INVITE decision) |
| User Backend | Outbound | Webhook | External SIP authentication (OAuth/LDAP proxy) |
| Locator Webhook | Outbound | Webhook | Real-time registration/unregistration events |
| Call Record Push | Outbound | Webhook | Push CDR JSON + Audio files to external server |
The most powerful extension point. Instead of static routing rules, RustPBX asks your API "Receive call from A to B, what should I do?".
- Trigger: Every incoming SIP INVITE.
- Config:
[proxy.http_router] url = "https://your-api.com/pbx/route" fallback_to_static = true # If your API fails, use internal routes timeout_ms = 5000 [proxy.http_router.headers] X-API-Key = "secret-token"
Request (POST):
{
"call_id": "ab39-551-229",
"from": "<sip:1001@pbx.com>",
"to": "<sip:200@pbx.com>",
"source_addr": "1.2.3.4:5060",
"direction": "inbound", // inbound | outbound | internal
"method": "INVITE",
"uri": "sip:200@pbx.com",
"headers": {
"User-Agent": "Yealink T54W",
"X-Client-ID": "998877"
},
"body": "v=0\r\n..." // Full SDP body
}Response:
{
"action": "forward", // Actions: forward | reject | abort | spam | not_handled
"targets": [
"sip:1001@192.168.1.50:5060", // Target 1 (Extension)
"sip:1002@192.168.1.51:5060" // Target 2 (Mobile App)
],
"strategy": "parallel", // parallel (Ring All) | sequential (Failover)
"record": true, // Enable recording for this call
"record_start_at": "media", // media (includes early media) | answer (wait for 200)
"timeout": 30, // Ring timeout in seconds
"media_proxy": "auto", // all | auto | nat | none | bypass
"headers": { // Inject custom SIP headers into the INVITE sent to B
"X-Call-Reason": "support-ticket-123"
}
}record_start_at overrides [recording].auto_start_at for this call.
When omitted, the global value is inherited; if neither is set, media is the
default. The field may also be returned without record to override the timing
while retaining the global recording enablement policy.
Delegate SIP registration password checking to your external DB or API.
- Trigger: SIP REGISTER or INVITE with auth.
- Config:
[[proxy.user_backends]] type = "http" url = "https://your-api.com/pbx/auth" username_field = "u" realm_field = "r"
Request (GET): https://your-api.com/pbx/auth?u=1001&r=pbx.com
Response (200 OK):
{
"id": 1001,
"username": "1001",
"password": "hashed_password_or_plaintext", // HA1 hash preferred
"display_name": "John Doe",
"email": "john@pbx.com",
"allow_guest_calls": false
}Response (403 Forbidden):
{ "reason": "invalid_password", "message": "Account locked" }Real-time notification when devices come online or go offline.
- Config:
[proxy.locator_webhook] url = "https://your-api.com/pbx/events" events = ["registered", "unregistered", "offline"]
Payload:
{
"event": "registered",
"timestamp": 1708201234,
"location": {
"aor": "sip:1001@1.2.3.4:12345",
"home_proxy": "10.0.0.12:5060",
"destination": "TLS 1.2.3.4:12345",
"supports_webrtc": false,
"transport": "TLS",
"user_agent": "MicroSIP/3.21.3",
"expires": 3600
}
}registered and unregistered events contain one location. An offline
event contains a locations array because a transport close or expiry sweep
can remove multiple bindings at once.
| Field | Meaning |
|---|---|
event |
registered, unregistered, or offline |
timestamp |
UNIX timestamp in seconds when RustPBX constructs the webhook payload |
aor |
Device Contact URI from the REGISTER Contact header; this is not necessarily the user's canonical identity |
home_proxy |
Advertised SIP address of the RustPBX node that accepted and owns the registration |
destination |
Device network address observed by RustPBX, including transport when available |
supports_webrtc |
Whether the registered device is treated as a WebRTC endpoint |
transport |
SIP transport used by the registration, such as UDP, TCP, TLS, WS, or WSS |
user_agent |
REGISTER User-Agent value when provided |
expires |
Registration lifetime in seconds |
The REGISTER Request-URI is not included in the locator webhook. It can be the
same address as home_proxy in a direct, single-node deployment, but can differ
when clients register through a domain, load balancer, SIP proxy, or NAT. Use
home_proxy when identifying the RustPBX node that owns the registration.
Example offline payload:
{
"event": "offline",
"timestamp": 1708201294,
"locations": [
{
"aor": "sip:1001@1.2.3.4:12345",
"home_proxy": "10.0.0.12:5060",
"destination": "TLS 1.2.3.4:12345",
"supports_webrtc": false,
"transport": "TLS",
"user_agent": "MicroSIP/3.21.3",
"expires": 3600
}
]
}Push call details immediately after a call ends. Recording media upload is configured separately.
- Config:
[recording] enabled = true auto_start = true auto_start_at = "media" type = "http" path = "./config/recorders" url = "https://your-api.com/pbx/recording" [callrecord] type = "http" url = "https://your-api.com/pbx/cdr" # Maximum concurrent post-call CDR save/upload/hook tasks. Default: 64, minimum: 1. max_concurrent = 64 # Accepted for compatibility, but ignored. Use [recording] for media upload. with_media = true
CDR format: multipart/form-data
- Field
calllog.json: The full CDR JSON (see next section).
Recording format: multipart/form-data
- File field
recording: The recorded WAV file. - Fields
call_idandtrack_id: Recording metadata.
Base URL: http://<rustpbx-ip>:8080/console
Authentication: Session cookie (login via the console UI) or a static
API token:
[console]
api_tokens = [
{ token = "my-api-token", scopes = ["calls", "records", "routing"] }
]Authorization: Bearer my-api-tokenManage calls that are currently in progress.
List Active Calls:
REST endpoints are mounted under the console api_prefix
(default /api, configured via [console].api_prefix).
GET {api_prefix}/calls/active
Control a Call:
POST {api_prefix}/calls/active/{call_id}/commands
Payloads:
- Hangup:
{ "action": "hangup", "reason": "admin_kick" } - Blind Transfer:
{ "action": "transfer", "target": "sip:1002@pbx.com" } - Mute/Unmute:
{ "action": "mute", "track_id": "audio-0" } // use 'unmute' to reverse - Force Answer (for ringing channels):
{ "action": "accept", "sdp": "v=0..." // Server-generated SDP answer }
Stream real-time transcription text for an active call. Transcription starts
lazily on first subscriber and stops when the last one disconnects or the call
ends. Requires [proxy.transcript.remote] (streaming ASR, Deepgram-compatible)
in config; returns 503 when unconfigured.
Endpoint: GET /cc/calls/{call_id}/transcript (SSE, text-only events:
started / segment / error / ended)
The same segments are also delivered as standard RWI events
(transcript_started / transcript_segment / transcript_error /
transcript_ended) to webhooks / RWI WebSocket subscribers.
Full protocol, config and examples: Live Transcript SSE API
| Resource | Endpoint | Methods | Description |
|---|---|---|---|
| Extensions | {api_prefix}/extensions |
GET, POST, PUT, DELETE |
Manage SIP users |
| Trunks | {api_prefix}/sip-trunk |
GET, POST, PUT, DELETE |
Manage upstream carriers |
| Routes | {api_prefix}/routing |
GET, POST, PUT, DELETE |
Manage dial plan rules |
| CDRs | {api_prefix}/call-records |
GET, POST (Search) |
Query history |
| Recording | {api_prefix}/call-records/{id}/recording |
GET |
Stream audio file |
| SIP Flow | {api_prefix}/call-records/{id}/sip-flow |
GET |
Get PCAP-like ladder diagram JSON |
Low-level system operations. Protected by IP whitelist ([ami].allows in config).
Base URL: http://<rustpbx-ip>:8080/ami/v1
- Health:
GET /health- System vital stats (uptime, active calls, load). - Reload:
POST /reload/trunks,/reload/routes,/reload/acl- Hot reload config without restart. - Shutdown:
POST /shutdown- Graceful shutdown (stops accepting new calls, waits for active ones). - Dialogs:
GET /dialogs- Raw dump of internal SIP dialog states (for debugging). - SipFlow signaling:
GET /sipflow/flow/{call_id}- Query SIP ladder data. - SipFlow media:
GET /sipflow/media/{call_id}- Export call media as WAV.
SipFlow endpoints support optional time range query parameters:
start: range start timeend: range end time
Accepted formats:
- RFC3339 datetime, e.g.
2026-04-16T10:00:00+08:00 - Unix timestamp (seconds), e.g.
1713232800
Example:
GET /ami/v1/sipflow/flow/abc123?start=2026-04-16T10:00:00%2B08:00&end=2026-04-16T10:30:00%2B08:00
GET /ami/v1/sipflow/media/abc123?start=1713232800&end=1713234600- User clicks phone number in CRM.
- CRM backend sends
POST /api/v1/commands(Future feature) OR uses AMI to originate call. - Current workaround: CRM sends SIP REFER to RustPBX or uses a dedicated "Click-to-Dial" SIP extension that the web-app registers as.
- Inbound call hits RustPBX.
- HTTP Router sends INVITE details to AI backend.
- AI Backend returns
{"action": "forward", "targets": ["sip:ai-bot-service@internal"]}. - RustPBX routes audio to the AI bot via SIP/RTP.
- User Backend authenticates user, checking balance > 0.
- Call proceeds.
- On hangup, CDR Push sends via HTTP POST to Billing System.
- Billing system calculates duration * rate and deducts balance.
[recording].enabled turns media capture on. [recording].type selects where
media goes: local / http / s3 write a WAV (uploaded by [recording]);
sipflow writes RTP into the [sipflow] backend (uploaded by
[sipflow.upload].media). SIP signalling is captured whenever [sipflow] is
configured. auto_start_at = "media" (default) installs the recorder after the
first caller-media setup; use "answer" to wait for the final 200.
[recording]
enabled = true
type = "sipflow"
auto_start = true
auto_start_at = "media"
[sipflow]
type = "local"
root = "./config/sipflow"
[sipflow.upload]
type = "s3"
vendor = "aliyun"
bucket = "my-bucket"
region = "oss-cn-hangzhou"
endpoint = "https://oss-cn-hangzhou.aliyuncs.com"
root = "recordings"
media = true
signaling = trueWAV is generated on-demand from stored RTP via GET /sipflow/media/{call_id}
and uploaded by [sipflow.upload]. Signalling is uploaded as JSONL to the same
target.
[recording]
enabled = true
type = "local" # or "http" / "s3"
auto_start = true
auto_start_at = "media"
path = "./config/recorders"
[sipflow]
type = "local"
root = "./config/sipflow"
[sipflow.upload]
signaling = true
media = falseMedia stays on the [recording] path; SipFlow stores SIP only (no RTP).
[recording]
enabled = true
type = "local"
auto_start = true
# No [sipflow] section β WAV only, no signalling capturePOST {ami_path}/outbound/dial originates a call and streams every RWI
event for it over one SSE connection. Full request/response contract and
the [outbound] config: see Outbound Dial SSE API.