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

:::note
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](https://console.signals.pressone.africa), 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:

```bash
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.

```bash title="Request"
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:

```json title="Response: 201 Created"
{
  "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:

```bash title="Request"
curl -u "$SIGNALS_ACCOUNT_SID:$SIGNALS_AUTH_TOKEN" \
  "https://api.signals.pressone.africa/2010-04-01/Accounts/$SIGNALS_ACCOUNT_SID/Calls/CA5f0c2a9e4b7d41c8a3e6f1b2d9c07e14.json"
```

```json title="Response: 200 OK (abridged)"
{
  "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.

```bash title="Request"
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"
```

```xml title="otp-call.xml, returned by your server"
<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>
```

:::tip
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](/programmable-voice/twilio-compatibility).
:::

Continue with the [Programmable Voice quickstart](/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](/getting-started/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](/sip-trunks/outbound-policies).
- a Signals number your organization holds, to present as the caller ID. See [Numbers and routing](/sip-trunks/numbers-and-routing).

### Step 1: find your project ID

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

```bash title="Request"
curl https://api.signals.pressone.africa/v1/projects \
  -H "Authorization: Bearer $SIGNALS_API_KEY"
```

```json title="Response: 200 OK"
{
  "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.

```bash title="Request"
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:

```http title="Response: 201 Created (abridged)"
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](/getting-started/requests-and-errors) explains ETags in detail.

```bash
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.

```bash title="Request"
curl https://api.signals.pressone.africa/v1/platform/endpoints \
  -H "Authorization: Bearer $SIGNALS_API_KEY"
```

```json title="Response: 200 OK"
{
  "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:

| Setting | Value |
| --- | --- |
| SIP server | `sip.trunk.pressone.co` |
| Transport and port | TLS on `5061`, because the trunk requires TLS |
| Media encryption | SRTP with SDES, because the trunk requires it |
| Firewall | Allow 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](/sip-trunks/connect-your-pbx) has device settings for common systems.

### Step 4: find the call in call history

```bash title="Request"
curl "https://api.signals.pressone.africa/v1/calls?trunk=$TRUNK_ID&window=last_hour" \
  -H "Authorization: Bearer $SIGNALS_API_KEY"
```

```json title="Response: 200 OK (abridged)"
{
  "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](/sip-trunks/quickstart), which covers digest authentication, inbound routing and trunk tests.

## Next steps

- [Authentication](/getting-started/authentication): API keys, scopes and the Calls API credentials.
- [Requests and errors](/getting-started/requests-and-errors): idempotency, ETags, background work and error codes.
- [Programmable Voice overview](/programmable-voice/overview): how calls, OneML and webhooks fit together.
