Self-hosted outbound-calling service for Forge, the manufacturing ERP from Armory Works. It wraps Asterisk so a Forge install can place and log calls against its own SIP trunk, without the main application carrying any telephony operational surface (SIP servers, RTP, codec negotiation, voicemail routing).
A small Node.js service that talks to Asterisk over
ARI (the Asterisk REST Interface)
and exposes the HTTP endpoints Forge's AsteriskOutboundService calls.
forge-ui → /api/voice/call → forge-voice → ARI → Asterisk → SIP trunk → PSTN
Call-state events travel back the other way: forge-voice POSTs them to the main API, which turns them into communication records.
The point of the split: call control is open source and self-hosted, while the SIP trunk is a commodity layer you pay per minute for, and switching providers is a credentials change rather than a rewrite.
A SIP trunk account from any provider — Telnyx, Bandwidth, Flowroute, Plivo, VoIP.ms, and others all speak plain SIP. Check current rates with the provider; they change.
asterisk/pjsip.conf assumes a username/password registration. For IP-authenticated trunks,
uncomment the [trunk-identify] block in that file and set your provider's signalling addresses.
You do not need a Twilio account, carrier licensing, or a kernel-mode SIP stack. Asterisk handles all of that inside the container.
- Docker with Compose (the stack is two containers:
andrius/asterisk:18.x-currentand this service, built onnode:24-alpine) - A SIP trunk account
- A reachable Forge API to receive call-state webhooks (optional — without it the bridge still places calls, it just does not report them)
To run the bridge outside Docker: Node.js 24 or newer, npm install, npm start. It still needs
an Asterisk instance with ARI enabled to connect to.
git clone https://github.com/armoryworks/forge-voice.git
cd forge-voice
cp .env.example .env # fill in SIP trunk credentials and the webhook secret
docker compose up -d
curl http://localhost:3030/healthThen, in the main application:
- Set
Voice:WebhookSecretin the Forge API configuration to the same value asFORGE_WEBHOOK_SECREThere. The webhook endpoint refuses events without a matching secret and returns 503 when the secret is unset, because anything that can post there can fabricate a call record. - Enable the
CAP-EXT-VOIP-SYNCcapability, which gates the API's voice controller. - Set
voiceServiceUrlinforge-ui'senvironment.tsto this container's base URL (http://forge-voice:3030on a shared Docker network).AsteriskOutboundServicereports itself unavailable while that value is unset. - Point the
OutboundCallServicealias inforge-uiatAsteriskOutboundService. It defaults toTelLinkOutboundService, which emitstel:links and places no calls through this service.
Set in .env; see .env.example.
| Variable | Purpose |
|---|---|
SIP_TRUNK_HOST |
Trunk hostname, substituted into pjsip.conf |
SIP_TRUNK_USER / SIP_TRUNK_PASS |
Trunk registration credentials |
ARI_PASS |
Password the bridge uses to authenticate to Asterisk's ARI (ARI_USER defaults to forge-voice) |
FORGE_API_WEBHOOK |
Where call-state events are posted. Default http://forge-api:8080/api/v1/voice/webhook |
FORGE_WEBHOOK_SECRET |
Shared secret sent as the X-Forge-Voice-Secret header. Without it the bridge logs an error and posts nothing |
QB_API_WEBHOOK and QB_WEBHOOK_SECRET are still read as fallbacks for installs configured
before the rename.
Known issue: docker-compose.yml still passes the webhook variables through under their
legacy QB_* names, so the FORGE_* values in .env do not reach the container. Until that is
fixed, either set the QB_* names in .env as well, or edit the voice-bridge environment block
in docker-compose.yml to forward FORGE_API_WEBHOOK and FORGE_WEBHOOK_SECRET.
The bridge listens on port 3030.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/voice/call |
Place a call. Body: { phone, callerId?, context? }. Returns { ok, callId, channel } once Asterisk accepts the Originate |
POST |
/api/voice/hangup/:callId |
Hang up a tracked call |
GET |
/api/voice/status/:callId |
Current channel state for a tracked call |
GET |
/health |
Liveness, plus whether the ARI connection is up |
Call state is held in memory for the life of the process; a restart drops tracking for calls in flight.
| Feature | Status |
|---|---|
| Outbound call placement | Implemented — ARI Originate |
| Inbound calls | Implemented — arrive via StasisStart and are reported to the main app |
| Call-status webhook to the main app | Implemented — Up and Hangup events, authenticated by shared secret |
| Call recording | Not enabled. asterisk/extensions.conf carries a commented MixMonitor template; consent law varies by jurisdiction, so enabling it is a per-install decision |
| Voicemail drop (pre-recorded) | Not implemented |
| Power dialer (auto-advance) | Not implemented |
src/server.js The ARI bridge — endpoints, call tracking, webhook posting
asterisk/pjsip.conf SIP trunk endpoint, auth, AOR, registration
asterisk/extensions.conf Minimal dialplan; both directions hand off to the Stasis app
asterisk/ari.conf ARI user and origin allowlist
asterisk/http.conf Asterisk's HTTP listener (port 8088), which ARI rides on
docker-compose.yml Asterisk + bridge
Dockerfile Bridge image
src/server.js is deliberately the shape of a bridge. The Originate call and dialplan usually
need tuning for a given trunk's endpoint naming and caller-ID rules; production deployments are
expected to adapt them.
Twilio's Programmable Voice API works well, but it is a proprietary API: every call ties you to one vendor's billing, API stability, and business decisions. Twilio's SIP Trunking product is a different matter and would work fine with this repo — it is the trunk, not the call control.
Forge ships TelLinkOutboundService as its default, which hands a tel: link to the operating
system and requires no telephony infrastructure at all. A cloud-managed provider integration is
described in the code as future work but is not implemented today. This repo is the option for
shops that want programmatic call control they host themselves.
File issues and pull requests at github.com/armoryworks/forge-voice/issues. Bugs in the main application belong on the Forge repo instead.