Signals Docs
Getting started

Your first call

This page places the same call to the same phone in two ways. The first way uses Programmable Voice: one HTTP request places the call, and OneML tells Signals what to say. The second way uses a SIP trunk: you create a trunk, then dial from your PBX as usual.

Both examples call +234 803 555 0142 from the Signals number +234 201 330 0117. Replace them with a phone you can answer and a number your project owns.

Automations offers a third way: describe the outcome you want, and Signals runs the call.

Path 1: Programmable Voice

Programmable Voice places calls from your code. You send one request to the Calls API, and when the person answers, Signals follows the OneML you supplied. OneML is an XML format for call instructions. <Say> reads text aloud, <Gather> collects keypad digits, and <Dial> connects another party.

Before you start with Programmable Voice

You need:

  • your voice account's AccountSid and auth token. The voice account is the Programmable Voice identity of your project. The AccountSid is its public name, AC followed by 32 hexadecimal characters. The auth token is its password.
  • a Signals number owned by your project, to use as the caller ID.
  • a phone to ring.

Step 1: get your AccountSid and auth token

In the Signals console, open Developers, then Voice Account. Copy the AccountSid, then choose Show and copy the auth token. Admins and engineers can view it at any time. Store it in a secrets manager.

Keep both values in environment variables:

TerminalCode
export SIGNALS_ACCOUNT_SID="AC8d2e61f04a9b3c7e5d1f2a6b0c4e9d37" export SIGNALS_AUTH_TOKEN="<your auth token>"

Step 2: place the call

Create the call with POST /2010-04-01/Accounts/{AccountSid}/Calls.json. Authenticate with HTTP Basic: the username is the AccountSid and the password is the auth token. Pass the OneML inline in the Twiml parameter, so Signals does not need to fetch it from your server.

TerminalCode
curl -X POST "https://api.signals.pressone.africa/2010-04-01/Accounts/$SIGNALS_ACCOUNT_SID/Calls.json" \ -u "$SIGNALS_ACCOUNT_SID:$SIGNALS_AUTH_TOKEN" \ --data-urlencode "To=+2348035550142" \ --data-urlencode "From=+2342013300117" \ --data-urlencode "Twiml=<Response><Say>Your PressOne code is 4, 8, 1, 5, 2, 9.</Say></Response>"

Signals accepts the call and returns it in the queued status. The response follows Twilio's call resource; this one is abridged:

JSONCode
{ "sid": "CA5f0c2a9e4b7d41c8a3e6f1b2d9c07e14", "account_sid": "AC8d2e61f04a9b3c7e5d1f2a6b0c4e9d37", "to": "+2348035550142", "from": "+2342013300117", "status": "queued", "subresource_uris": { "events": "/2010-04-01/Accounts/AC8d2e61f04a9b3c7e5d1f2a6b0c4e9d37/Calls/CA5f0c2a9e4b7d41c8a3e6f1b2d9c07e14/Events.json" }, "uri": "/2010-04-01/Accounts/AC8d2e61f04a9b3c7e5d1f2a6b0c4e9d37/Calls/CA5f0c2a9e4b7d41c8a3e6f1b2d9c07e14.json" }

The phone rings. When you answer, you hear the code read aloud, and the call ends.

Step 3: check the call

Fetch the call by its sid to follow its status:

TerminalCode
curl -u "$SIGNALS_ACCOUNT_SID:$SIGNALS_AUTH_TOKEN" \ "https://api.signals.pressone.africa/2010-04-01/Accounts/$SIGNALS_ACCOUNT_SID/Calls/CA5f0c2a9e4b7d41c8a3e6f1b2d9c07e14.json"
JSONCode
{ "sid": "CA5f0c2a9e4b7d41c8a3e6f1b2d9c07e14", "to": "+2348035550142", "from": "+2342013300117", "status": "completed" }

The subresource_uris.events link lists the HTTP exchanges Signals recorded for the call, which helps when a call does not behave as you expect.

Serve OneML from your own server

Inline OneML suits a fixed message. For anything that reacts to the caller, pass a Url instead of Twiml. When the call is answered, Signals requests that URL and follows the OneML your server returns.

TerminalCode
curl -X POST "https://api.signals.pressone.africa/2010-04-01/Accounts/$SIGNALS_ACCOUNT_SID/Calls.json" \ -u "$SIGNALS_ACCOUNT_SID:$SIGNALS_AUTH_TOKEN" \ --data-urlencode "To=+2348035550142" \ --data-urlencode "From=+2342013300117" \ --data-urlencode "Url=https://example.com/otp-call.xml"
XMLCode
<Response> <Say>Your PressOne code is 4, 8, 1, 5, 2, 9.</Say> <Gather numDigits="1" action="/otp/repeat"> <Say>Press 1 to hear it again.</Say> </Gather> </Response>

Coming from Twilio? Change api.twilio.com to api.signals.pressone.africa and use your Signals AccountSid and auth token. Your requests and markup stay the same. See Moving from Twilio.

Continue with the Programmable Voice quickstart.

Path 2: SIP trunk

A SIP trunk connects a phone system you already run, such as a PBX, a contact centre platform or an AI voice agent, to Signals over SIP. Your PBX sends each call to Signals. Signals checks that the call came from your trunk, applies the trunk's outbound policy and routes the call to the phone network.

A trunk is live as soon as you create it. There is no draft and nothing to publish: Signals checks the request, saves the trunk and applies it to the network within a minute.

Before you start with a SIP trunk

You need:

  • an API key with the service_configuration:read, service_configuration:write, device_config:read and call_history:read scopes. See Authentication.
  • the public IP address your PBX sends calls from, in this example 203.0.113.10.
  • an outbound policy that allows calls to Nigeria. See Outbound policies.
  • a Signals number your organization holds, to present as the caller ID. See Numbers and routing.

Step 1: find your project ID

A trunk is created inside a project. Read your project ID:

TerminalCode
curl https://api.signals.pressone.africa/v1/projects \ -H "Authorization: Bearer $SIGNALS_API_KEY"
JSONCode
{ "data": [ { "id": "prj_0192a4c7-5e1b-7d3f-9a28-6c4e8b1f2d07", "name": "Production", "status": "active", "billing_account_id": "0192a4c6-9d2e-7b31-8f45-a1c3e5d7b902", "billing_assignment_revision": 1, "billing_effective_from": "2026-09-01T00:00:00Z", "currency": "NGN", "pending_payer": null } ] }

Step 2: create the trunk

This trunk accepts calls only from your PBX's IP address, requires TLS for signalling and SRTP for audio, sends inbound calls to pbx.example.com and uses your outbound policy. Every create request carries an Idempotency-Key, so a retry cannot create a second trunk.

TerminalCode
curl -X POST https://api.signals.pressone.africa/v1/trunks \ -H "Authorization: Bearer $SIGNALS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -H "X-Project-ID: prj_0192a4c7-5e1b-7d3f-9a28-6c4e8b1f2d07" \ -d '{ "name": "office-pbx", "subaccount_id": null, "direction": "both", "auth_mode": "ip_allow_list", "sources": ["203.0.113.10/32"], "require_tls": true, "source_validation": "strict", "targets": [ { "sip_uri": "sip:pbx.example.com", "priority": 10, "weight": 100, "transport": "tls" } ], "codecs": ["PCMA", "PCMU"], "media_security": "sdes_srtp_required", "outbound_policy_id": "pol_0192a9d3-8b4e-7c61-a5f2-3e7d1c0b9a86" }'

The response holds the full trunk under trunk. This excerpt shows the fields this guide uses:

Code
HTTP/1.1 201 Created ETag: "1" Location: /v1/trunks/trk_0192b7e1-4c2a-7f58-b913-2d6e0a9c5f41 Content-Type: application/json { "trunk": { "id": "trk_0192b7e1-4c2a-7f58-b913-2d6e0a9c5f41", "name": "office-pbx", "status": "enabled", "direction": "both", "revision": 1, "policy": { "id": "pol_0192a9d3-8b4e-7c61-a5f2-3e7d1c0b9a86", "name": "office-calls" } }, "digest_password": null }

The trunk is saved and takes effect within a minute. If Signals cannot apply it, PressOne staff are alerted; you do not need to poll anything.

The ETag header holds the trunk's revision, a number that goes up with every change. Send it back in If-Match on your next change, so Signals can refuse the change if someone else edited the trunk in between. Requests and errors explains ETags in detail.

TerminalCode
export TRUNK_ID="trk_0192b7e1-4c2a-7f58-b913-2d6e0a9c5f41"

Step 3: point your PBX at Signals and dial

Read the current signalling and media addresses. They can change, so read them before you configure a device rather than copying them from this page.

TerminalCode
curl https://api.signals.pressone.africa/v1/platform/endpoints \ -H "Authorization: Bearer $SIGNALS_API_KEY"
JSONCode
{ "version": 1, "as_of": "2026-09-07T00:00:00Z", "signaling_fqdn": "sip.trunk.pressone.co", "signaling_nodes": ["198.51.100.10"], "signaling_ports": [ { "transport": "udp", "port": 5060 }, { "transport": "tcp", "port": 5060 }, { "transport": "tls", "port": 5061 } ], "signaling_port": 5061, "tls": { "realm": "sip.trunk.pressone.co", "trust_chain": [] }, "media_anchors": ["198.51.100.10"], "media_cidrs": ["198.51.100.10/32"], "rtp_port_range": "10000-20000", "rtp_ports": { "start": 10000, "end": 20000 } }

Configure your PBX with these values:

SettingValue
SIP serversip.trunk.pressone.co
Transport and portTLS on 5061, because the trunk requires TLS
Media encryptionSRTP with SDES, because the trunk requires it
FirewallAllow UDP audio from the ranges in media_cidrs on ports 10000 to 20000
Caller ID+2342013300117, a number your organization holds

Then dial +2348035550142 from a PBX extension. The phone rings.

Connect your PBX has device settings for common systems.

Step 4: find the call in call history

TerminalCode
curl "https://api.signals.pressone.africa/v1/calls?trunk=$TRUNK_ID&window=last_hour" \ -H "Authorization: Bearer $SIGNALS_API_KEY"
JSONCode
{ "as_of": "2026-10-01T09:20:05Z", "data": [ { "id": "cal_0192b7f2-6d81-7a3b-9c4e-1f0a8d5e2b63", "direction": "outbound", "source_product": "trunking", "from_number": "+2342013300117", "to_number": "+2348035550142", "received_at": "2026-10-01T09:18:41Z", "answered_at": "2026-10-01T09:18:47Z", "ended_at": "2026-10-01T09:19:05Z", "observed_duration_ns": 18000000000, "outcome": "answered" } ], "total": 1, "answered": 1, "failed": 0, "next_cursor": null, "previous_cursor": null }

Continue with the SIP trunk quickstart, which covers digest authentication, inbound routing and trunk tests.

Next steps

Last modified on