# SIP Trunks quickstart

In this quickstart you create an outbound policy, create a trunk for an office PBX that uses it,
allow a second PBX address, run a configuration test and place a test call from your PBX. Every
step uses `curl` against `https://api.signals.pressone.africa/v1`.

The example PBX has the public addresses `203.0.113.10` and `203.0.113.11` and accepts SIP over TLS
on port 5061. Replace them with your own.

## Before you start

You need:

- An API key with the admin or engineer role. It looks like `sk_live_...`. See
  [Authentication](/getting-started/authentication).
- The ID of the Signals project the trunk belongs to. `GET /v1/projects` lists your projects.
- The public IP address of your PBX, and the address and port where it accepts SIP.

Set them as shell variables so you can paste the commands below:

```bash
export SIGNALS_API_KEY="sk_live_..."
export SIGNALS="https://api.signals.pressone.africa/v1"
export PROJECT_ID="prj_4f1c2a9e-7b3d-4e8a-9c61-2d5b8e0f3a17"
```

Three headers appear throughout:

- `Idempotency-Key` makes a create or action safe to retry. Send a new unique value for each new
  request, and the same value when you retry that request.
- `If-Match` carries the ETag of the trunk or policy you are changing, a quoted integer such as
  `"1"`. It stops you from overwriting a change someone else made. Every successful edit returns a
  new ETag.
- `X-Project-ID` selects a project. You can leave it out while your organization has one active
  project.

Every change is live once the API accepts it. There is nothing to publish. Changes take effect
within a minute.

Responses below are trimmed to the fields this guide uses.

## Step 1: Read the platform endpoints

Fetch the addresses your PBX and firewall need:

```bash
curl "$SIGNALS/platform/endpoints" \
  -H "Authorization: Bearer $SIGNALS_API_KEY"
```

```json
{
  "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 }
  ],
  "tls": { "realm": "sip.trunk.pressone.co", "trust_chain": [] },
  "media_cidrs": ["198.51.100.10/32"],
  "rtp_ports": { "start": 10000, "end": 20000 }
}
```

You will use these values in [Connect your PBX](/sip-trunks/connect-your-pbx).

## Step 2: Create an outbound policy

Outbound calls need an outbound policy. Create one with a daily spend cap of ₦50,000 (5,000,000
kobo) and recording off:

```bash
curl -X POST "$SIGNALS/outbound-policies" \
  -H "Authorization: Bearer $SIGNALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9e3a1c76-0b4d-4a28-8f97-4d2c6e1b8a05" \
  -H "X-Project-ID: $PROJECT_ID" \
  -d '{
    "name": "office-calls",
    "concurrency": null,
    "cps": null,
    "max_call_duration_seconds": 3600,
    "max_destination_rate_minor": null,
    "daily_spend_limit_minor": 5000000,
    "recording_mode": "off",
    "recording_channels": 2,
    "recording_retention_days": 30
  }'
```

The API returns `201 Created` with an `ETag` of `"1"`. The policy is live. A new policy allows calls
to Nigeria:

```json
{
  "id": "pol_01992f3b-0a27-7f13-8c4e-3b9d6a1e5f20",
  "name": "office-calls",
  "status": "active",
  "currency": "NGN",
  "revision": 1,
  "attached_services": 0,
  "destinations": { "allowed_countries": ["NG"], "allowed_prefixes": 0, "denied_prefixes": 0 }
}
```

```bash
export POLICY_ID="pol_01992f3b-0a27-7f13-8c4e-3b9d6a1e5f20"
```

## Step 3: Create the trunk

Create a trunk that carries calls in both directions, accepts calls from `203.0.113.10`, requires
TLS (`require_tls`), delivers inbound calls to the PBX over TLS on port 5061 and uses the policy
from step 2:

```bash
curl -X POST "$SIGNALS/trunks" \
  -H "Authorization: Bearer $SIGNALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6c0f9a52-3e1b-4d7a-8f25-1b9e7d4c2a60" \
  -H "X-Project-ID: $PROJECT_ID" \
  -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:203.0.113.10:5061", "priority": 10, "weight": 100, "transport": "tls" }
    ],
    "codecs": ["PCMA", "PCMU"],
    "media_security": "rtp",
    "limits": { "concurrency": 30, "cps": 5, "max_call_duration_seconds": 7200 },
    "outbound_policy_id": "'"$POLICY_ID"'",
    "number_block_ids": []
  }'
```

`number_block_ids` routes whole number blocks to the trunk in the same request. This example leaves
it empty and routes numbers later in [Numbers and routing](/sip-trunks/numbers-and-routing).

The API returns `201 Created` with the trunk under `trunk` and an `ETag` header of `"1"`:

```json
{
  "trunk": {
    "id": "trk_01992f3a-6c1e-7d42-9b8a-5e7f0c2d1a94",
    "project_id": "prj_4f1c2a9e-7b3d-4e8a-9c61-2d5b8e0f3a17",
    "name": "office-pbx",
    "status": "enabled",
    "direction": "both",
    "revision": 1,
    "signaling_fqdn": "office-pbx.sip.trunk.pressone.co",
    "currency": "NGN",
    "authentication": {
      "mode": "ip_allow_list",
      "sources": [
        { "id": "01992f3a-6c20-7a11-8d3e-2f4b6a9c0e15", "cidr": "203.0.113.10/32", "enabled": true }
      ],
      "digest": null,
      "require_tls": true,
      "source_validation": "strict"
    },
    "routing": [
      {
        "id": "01992f3a-6c21-7b02-9e4f-3a5c7b0d1f26",
        "sip_uri": "sip:203.0.113.10:5061",
        "priority": 10,
        "weight": 100,
        "transport": "tls",
        "enabled": true
      }
    ],
    "media": { "codecs": ["PCMA", "PCMU"], "security": "rtp" },
    "policy": { "id": "pol_01992f3b-0a27-7f13-8c4e-3b9d6a1e5f20", "name": "office-calls" }
  },
  "digest_password": null
}
```

The trunk is live and takes effect on the network within a minute. Save the ID:

```bash
export TRUNK_ID="trk_01992f3a-6c1e-7d42-9b8a-5e7f0c2d1a94"
```

:::tip
The trunk name must be lowercase letters, digits and hyphens, up to 63 characters, starting and
ending with a letter or digit. Signals uses it in the trunk's `signaling_fqdn`.
:::

If your PBX cannot use a fixed public IP address, create the trunk with `"auth_mode": "digest"`,
`"sources": []` and a `digest_realm` instead. The response then carries the generated password in
`digest_password`. It is shown only once, so store it before you do anything else. See
[Authentication and security](/sip-trunks/authentication-and-security#digest-credentials).

If the trunk could not run as configured, the API refuses the create with `422` and a problem
detail that starts with `the trunk would not apply:`. Nothing is created.

## Step 4: Allow a second PBX address

Your office has a second PBX at `203.0.113.11`. Add it as an IP credential. Send the ETag from step
3 in `If-Match`:

```bash
curl -X POST "$SIGNALS/trunks/$TRUNK_ID/ip-credentials" \
  -H "Authorization: Bearer $SIGNALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "1"' \
  -H "Idempotency-Key: 2b7d4e18-9a3c-4f05-b6e1-7c8a0d2f5e93" \
  -d '{ "cidr": "203.0.113.11/32" }'
```

```json
{
  "id": "01992f3b-1a40-7e62-a0c1-5d7e9f2b4a37",
  "cidr": "203.0.113.11/32",
  "selector_precedence": 100,
  "enabled": true,
  "created_at": "2026-10-01T09:16:04Z"
}
```

The response carries the new ETag `"2"`. The new address is allowed within a minute.

## Step 5: Run a configuration test

Ask Signals to check the trunk's configuration:

```bash
curl -X POST "$SIGNALS/trunks/$TRUNK_ID/tests" \
  -H "Authorization: Bearer $SIGNALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8a1d5c3e-2f74-4b90-a6e8-3c7b1d9f0e26" \
  -d '{ "kind": "config_validation" }'
```

The API returns `202 Accepted` and a `Location` header that ends with the test ID. Read the test:

```bash
curl "$SIGNALS/trunks/$TRUNK_ID/tests/tst_01992f3d-0b62-7c48-9d15-6e2a4f8c1b93" \
  -H "Authorization: Bearer $SIGNALS_API_KEY"
```

```json
{
  "run": {
    "id": "tst_01992f3d-0b62-7c48-9d15-6e2a4f8c1b93",
    "result": "passed",
    "checks": [
      {
        "sequence": 1,
        "id": "configuration",
        "state": "passed",
        "summary": "2 IP and 0 digest identities, 1 termination targets, admission admit."
      }
    ],
    "finding": null
  },
  "requested_kind": "config_validation",
  "next_sequence": 1
}
```

While `result` is `running`, read the test again. `GET /v1/trunks/{trunk_id}/tests` lists earlier
runs under `data`.

## Step 6: Place a test call from your PBX

Point your PBX at `sip.trunk.pressone.co` as described in
[Connect your PBX](/sip-trunks/connect-your-pbx), then dial a Nigerian mobile number from an
extension, for example `+2348035550142`.

- If the call connects and you hear audio both ways, the trunk works.
- If the call is refused straight away, check that the PBX sends from an allowed address and over
  TLS, since this trunk requires it.
- If the call connects with no audio, allow the Signals media addresses and RTP ports from step 1
  in your firewall.

The call needs a caller ID that is routed to the trunk. Until you route a number block in
[Numbers and routing](/sip-trunks/numbers-and-routing), outbound calls have no caller ID they may
present.

## What you built

- An outbound policy that allows calls to Nigeria and caps daily spend at ₦50,000.
- A trunk, `office-pbx`, that uses that policy, accepts calls from two office addresses and
  delivers inbound calls over TLS.

## Next steps

- Route numbers to the trunk so it can receive calls and present caller ID:
  [Numbers and routing](/sip-trunks/numbers-and-routing).
- Configure your PBX: [Connect your PBX](/sip-trunks/connect-your-pbx).
- Add more destinations or limits: [Outbound policies](/sip-trunks/outbound-policies).
