# Outbound policies

An outbound policy is a named set of rules for calls your PBX places to the phone network. It says
which countries and number prefixes may be called, how many calls may run, how much may be spent in
a day, and whether calls are recorded. You write a policy once and attach it to as many trunks as
you like. A policy is live as soon as you create it, and every change you save reaches every
attached trunk within a minute.

A trunk with no policy refuses every outbound call.

Policy IDs start with `pol_`, for example `pol_01992f3b-0a27-7f13-8c4e-3b9d6a1e5f20`.

## What a policy controls

You create a policy with `POST /v1/outbound-policies` and replace its settings with
`PATCH /v1/outbound-policies/{policy_id}`. Both take the same body, and every field is required:

```json
{
  "name": "office-calls",
  "concurrency": 20,
  "cps": 3,
  "max_call_duration_seconds": 3600,
  "max_destination_rate_minor": 15000,
  "daily_spend_limit_minor": 5000000,
  "recording_mode": "off",
  "recording_channels": 2,
  "recording_retention_days": 30
}
```

| Field | Meaning |
| --- | --- |
| `name` | Lowercase letters, digits and hyphens, up to 63 characters. Unique in your organization. |
| `concurrency` | Most calls active at once across every trunk using the policy, counting inbound and outbound calls, or `null` for no policy-level cap. |
| `cps` | Most new outbound calls per second, or `null`. Inbound calls are not refused for CPS. |
| `max_call_duration_seconds` | Longest call allowed, or `null`. |
| `max_destination_rate_minor` | Highest per-unit rate a destination may cost, as an integer of minor units (kobo for NGN), or `null`. |
| `daily_spend_limit_minor` | Most that calls under the policy may spend in a day, as an integer of minor units, or `null`. |
| `recording_mode` | `off` or `all`. |
| `recording_channels` | Always `2`: each party on its own channel. |
| `recording_retention_days` | `30`, `90` or `365`. |

A policy's currency is always NGN. Money is an integer of minor units, so `5000000` is ₦50,000.00.
Read it as an exact integer, not a floating-point number, to avoid rounding.

Signals checks policy limits together with the limits on the trunk, the subaccount and your
organization. A call must fit within all of them. The policy's `ceilings` object reports the saved
value of each limit. `ceilings.daily_spend` also shows `used_today_minor` and `resets_at`.

### Recording

Setting `recording_mode` to `all` records every outbound call under the policy. Recording needs both
of the following:

- Your organization has accepted the recording terms in the console. Without that, the request
  fails with `409 recording_consent_required`.
- Your project's price book prices recording.

The policy's `recording.applied` field tells you whether recording is in effect.

## Destination rules

Destination rules decide which numbers your PBX may call. Each rule allows or denies one country or
one prefix:

- A **country** rule names an ISO 3166-1 alpha-2 code, such as `NG` or `GH`. It matches that
  country's calling code. Countries that share a calling code, such as the United States and Canada
  on `+1`, have no code of their own, so use prefix rules for them.
- A **prefix** rule names the leading E.164 digits without the `+`, such as `233` for Ghana or
  `2348091` for a range of Nigerian mobile numbers. A prefix is 1 to 15 digits and cannot start
  with `0`.

`GET /v1/destination-countries` lists every country a rule can name, with its `iso_code`,
`calling_code`, `continent` and `shared_code`. A country with `"shared_code": true` cannot have a
country rule; use a prefix rule instead.

A new policy starts with one rule that allows Nigeria (`NG`).

### How rules are evaluated

For each call, the most specific matching rule wins: a longer prefix beats a shorter one. When an
allow rule and a deny rule are equally specific, the deny rule wins. A number no rule allows is
refused.

For example, with these rules:

| Rule | Effect |
| --- | --- |
| Country `NG` (calling code `234`) | Allow |
| Prefix `23480912` | Deny |

a call to `+2348034128890` is allowed by `NG`, and a call to `+2348091234567` is refused because the
deny rule `23480912` is more specific.

### Change rules

Send changes to `PATCH /v1/outbound-policies/{policy_id}/destination-rules` with `If-Match` and
`Idempotency-Key`. The policy ETag is its `revision` as a quoted integer, such as `"1"`. Each change has an `op` of `allow`, `deny` or `remove`, a `kind` of `country` or
`prefix`, a `value`, and an optional `label` of 1 to 80 characters:

```bash
curl -X PATCH "$SIGNALS/outbound-policies/$POLICY_ID/destination-rules" \
  -H "Authorization: Bearer $SIGNALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "1"' \
  -H "Idempotency-Key: 1f9c3e57-6a2d-4b08-9e41-c7d5a0b3f862" \
  -d '{
    "changes": [
      { "op": "allow", "kind": "country", "value": "GH", "label": "Ghana" },
      { "op": "deny", "kind": "prefix", "value": "23480912", "label": "Blocked range" }
    ]
  }'
```

The response is the policy with its updated rules (trimmed), and a new ETag of `"2"`:

```json
{
  "id": "pol_01992f3b-0a27-7f13-8c4e-3b9d6a1e5f20",
  "revision": 2,
  "destination_rules": {
    "allowed": [
      { "match": "GH", "kind": "country", "label": "Ghana" },
      { "match": "NG", "kind": "country", "label": "Nigeria" }
    ],
    "denied": [
      { "match": "23480912", "label": "Blocked range", "source": "customer", "case_ref": null }
    ]
  }
}
```

- Send 1 to 250 changes per request. A destination may appear only once in a request.
- Changes are keyed by kind and value, so sending the same change twice leaves one rule.
- Allowed rules also show `rate_minor`, `rate_unit` and the billing increments from your project's
  price book.
- A denied rule with `"source": "containment"` was added by Signals fraud containment and carries a
  `case_ref`. You cannot remove it yourself.

Rule changes are live once saved. Calls already in progress keep the terms they started with; new
calls use the new rules.

## Attachments

An attachment links a policy to a service, such as a trunk. A service has at most one policy at a
time.

### Attach a policy to a trunk

You can attach a policy when you create the trunk, with `outbound_policy_id` in the create request.
To attach or replace it later, use the trunk's ETag:

```bash
curl -X PUT "$SIGNALS/trunks/$TRUNK_ID/outbound-policy" \
  -H "Authorization: Bearer $SIGNALS_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "2"' \
  -d '{ "policy_id": "pol_01992f3b-0a27-7f13-8c4e-3b9d6a1e5f20" }'
```

- The response is `200 OK` with the trunk, its `policy` set to the new policy, and a new ETag.
- Send `{ "policy_id": null }` to remove the policy.
- The change reaches the network within a minute.

### List attachments

`GET /v1/outbound-policies/{policy_id}/attachments` lists the services using a policy, with
`service_type`, `service_id`, `service_name`, `attached_at`, `calls_in_flight` and an `apply_state`
of `applied` or `pending`. A `pending` attachment is a saved change that has not reached the network
yet.

Attachment IDs start with `pat_`. To move a trunk to another policy, attach the replacement with
`PUT /v1/trunks/{trunk_id}/outbound-policy`. To remove a trunk's policy, send `{ "policy_id": null }`
there; the trunk then refuses outbound calls.

## Delete a policy

`DELETE /v1/outbound-policies/{policy_id}` with `Idempotency-Key` archives the policy and returns
`204 No Content`. It returns `409 outbound_policy_attached` while any service is attached. Replace
the policy on every trunk first, or send `?detach=true` to detach every service in the same request.
Detached trunks then refuse outbound calls.

## Errors you may see

| Status and code | Cause |
| --- | --- |
| `409 outbound_policy_name_conflict` | Another policy has this name. |
| `409 recording_consent_required` | `recording_mode` is `all` but your organization has not accepted the recording terms. |
| `409 outbound_policy_attached` | The policy is attached to a service and cannot be deleted without `detach=true`. |
| `409 idempotency_conflict` | The `Idempotency-Key` was used before with a different body. |
| `412 outbound_policy_revision_conflict` | The `If-Match` ETag is stale. Read the policy again. |
| `428 precondition_required` | The request has no `If-Match` header. |
| `422 invalid_outbound_policy` | A field is invalid, for example a prefix starting with `0` or a retention of 60 days. |

## Related

- [Quickstart](/sip-trunks/quickstart) creates a policy and attaches it to a new trunk.
- [SIP Trunking API reference](/api/sip-trunking)
