Skip to content

Repository files navigation

forge-voice

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).

What this is

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.

What you bring

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.

Requirements

  • Docker with Compose (the stack is two containers: andrius/asterisk:18.x-current and this service, built on node: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.

Quickstart

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/health

Then, in the main application:

  1. Set Voice:WebhookSecret in the Forge API configuration to the same value as FORGE_WEBHOOK_SECRET here. 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.
  2. Enable the CAP-EXT-VOIP-SYNC capability, which gates the API's voice controller.
  3. Set voiceServiceUrl in forge-ui's environment.ts to this container's base URL (http://forge-voice:3030 on a shared Docker network). AsteriskOutboundService reports itself unavailable while that value is unset.
  4. Point the OutboundCallService alias in forge-ui at AsteriskOutboundService. It defaults to TelLinkOutboundService, which emits tel: links and places no calls through this service.

Configuration

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.

HTTP API

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.

Capabilities

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

Layout

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.

Why not Twilio

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.

Contributing and issues

File issues and pull requests at github.com/armoryworks/forge-voice/issues. Bugs in the main application belong on the Forge repo instead.

License

Apache License 2.0 — see LICENSE and NOTICE.

About

Self-hosted outbound calling for Forge — power dialer, voicemail drop, call recording — in its own repo so the ERP never grows a telephony surface.

Topics

Resources

Code of conduct

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages