# Authentication

Signals has two APIs, and each has its own credential:

| API | Base URL | Credential | How you send it |
| --- | --- | --- | --- |
| Signals API | `https://api.signals.pressone.africa/v1` | API key | `Authorization: Bearer <key>` |
| Calls API (Twilio-compatible) | `https://api.signals.pressone.africa/2010-04-01` | `AccountSid` and auth token | HTTP Basic |

The two credentials are not interchangeable. The Signals API refuses a voice auth token, and the Calls API refuses an API key.

## API keys

An API key is a secret string that lets your server call the Signals API on behalf of your organization. Each key carries a role and a list of scopes, which together decide what it may do. A key can also be restricted to one subaccount.

### Key format

An API key looks like this:

```text
sk_live_KQ7M3XPA2VDF_9f3c1e8a7b2d4f6091c5e3a7d8b0f2e4
```

It has three parts, separated by underscores:

| Part | Example | Meaning |
| --- | --- | --- |
| Prefix | `sk_live` | Marks the string as a Signals secret key. There is no test mode, so every key is live. |
| Public ID | `KQ7M3XPA2VDF` | 12 characters from `A` to `Z` and `2` to `7`. The console shows it so you can tell keys apart. It is not secret. |
| Secret | `9f3c1e8a7b2d4f6091c5e3a7d8b0f2e4` | 32 lowercase hexadecimal characters. Only you hold it. |

:::caution
A voice auth token also starts with `sk_live_`, but it has no second underscore: it is `sk_live_` followed by 32 letters and digits. If the Signals API answers `401` to a credential you are sure is current, check that you did not paste the voice auth token.
:::

### Create a key

Members with the Admin or Engineer role create keys in the console.

1. Open the [Signals console](https://console.signals.pressone.africa) and choose **API keys** in the **Develop** section of the sidebar.
2. Start a new key.
3. Enter a name that says where the key is used, such as `billing-sync`.
4. Choose a role and the scopes the key needs. The console offers only the roles and scopes you are allowed to grant.
5. Optionally, restrict the key to one subaccount and choose when it expires.
6. Copy the key and store it in a secrets manager.

The console shows the full key once, at creation. Signals stores only a hash of the secret part, so nobody can show you the key again. If you lose it, rotate the key or create a new one.

### Send a key

Send the key in the `Authorization` header with the `Bearer` scheme:

```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
    }
  ]
}
```

Send exactly one `Authorization` header, with one space between `Bearer` and the key. The scheme name is case-insensitive. A missing, malformed, unknown, expired, rotated-out or revoked key gets the same answer:

```http title="Response: 401 Unauthorized"
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
X-Request-ID: req_0192b7e93c4f7a2e9d1b6c8f0a5e3d27

{
  "type": "https://signals.pressone.africa/problems/unauthenticated",
  "title": "Unauthorized",
  "status": 401,
  "detail": "A valid bearer credential is required.",
  "code": "unauthenticated",
  "request_id": "req_0192b7e93c4f7a2e9d1b6c8f0a5e3d27"
}
```

Signals records when each key was last used, to the minute. The console shows it in the key list, which helps you find keys nothing uses any more.

### What a key can do

Three settings bound every key. A request must pass all three.

**Role.** A key has one of the four organization roles: `org:admin`, `org:engineer`, `org:billing` or `org:viewer`. A key never does more than a member with that role. You can grant only a role at or below your own: an Admin can create a key with any role, and an Engineer can create Engineer and Viewer keys.

**Scopes.** A scope names one kind of action, such as `call_history:read`. A key holds at least one scope, and each scope must be one its role allows. A request that needs a scope the key lacks is refused with `403`. Common scopes:

| Scope | Allows |
| --- | --- |
| `organization:read` | Reading the organization record. |
| `call_history:read` | Reading call records and their detail. |
| `live_calls:read` | Reading calls in progress. |
| `call_diagnostics:read` | Reading SIP ladders and media diagnostics. |
| `service_configuration:read` | Reading trunks, policies, numbers and voice applications. |
| `service_configuration:write` | Creating and editing trunks, policies and voice applications. |
| `numbers:manage` | Holding, routing and releasing numbers. |
| `numbers:order` | Buying numbers. |
| `billing:read` | Reading balance, usage, rates and billing documents. |
| `recordings:manage` | Managing recordings and retention. |
| `developers:manage` | Managing webhook endpoints. |
| `tests:manage` | Running trunk tests. |
| `subaccounts:read` | Reading subaccounts and their limits. |
| `subaccounts:write` | Creating and changing subaccounts. Admin keys only. |
| `audit:read` | Reading the audit trail. |

Some actions need a person signed in to the console, and no key can perform them: managing API keys, managing members, viewing or rotating the voice auth token, rotating webhook signing secrets and accepting recording consent.

**Subaccount.** A key restricted to a subaccount reaches only the resources of that subaccount. A key without a subaccount reaches its whole project, within its role and scopes. The subaccount must be in the key's project.

A key also belongs to the project it was created in.

### Rotate a key

Rotating replaces a key with a new one without downtime. In the console, choose **Rotate** on the key's row and pick how long the old key keeps working: 24 hours, 1 hour or none. The overlap can be any whole number of hours from 0 to 24, and never more.

- Signals creates a new key with the same name, role, scopes, subaccount and expiry, and shows it once.
- The old key keeps working until the overlap ends, then stops. With an overlap of 0 it stops at once.
- The old key's record shows when it stops in `retires_at`, and the new key records the key it replaced in `rotated_from_id`.

Deploy the new key everywhere during the overlap. A key that has already been rotated cannot be rotated again; rotate its replacement instead.

### Revoke a key

Revoke a key when it leaks or is no longer needed. In the console, choose **Revoke** on the key's row, type the key name to confirm and give a reason, which is kept in the audit trail. Revocation takes effect immediately and cannot be undone. Requests that use the key then fail with `401`.

### Expiry

A key with an expiry date stops working at that time. A rotated key inherits its predecessor's expiry. Set an expiry for keys you hand to contractors or short projects.

## Choosing a project

A project is the unit that owns your resources and pays for them; see [Introduction](/getting-started/introduction#project). Most endpoints act on one project. Name it with the optional `X-Project-ID` header, set to the project's ID. A project ID is `prj_` followed by a lowercase UUID, as `GET /v1/projects` returns it:

```bash
curl https://api.signals.pressone.africa/v1/trunks \
  -H "Authorization: Bearer $SIGNALS_API_KEY" \
  -H "X-Project-ID: prj_0192a4c7-5e1b-7d3f-9a28-6c4e8b1f2d07"
```

| Situation | Result |
| --- | --- |
| The organization has one active project and you omit the header | Signals uses that project. |
| The organization has several projects and you omit the header | `400` with code `project_context_required`. The problem lists your projects in `available_projects`. |
| The header is not a project ID | `400` with code `project_invalid`. |
| The project does not belong to your organization | `404` with code `project_not_found`. |

Today every organization has one active project, so you can leave the header out. Sending it anyway keeps your code working if you add a project later.

## Calls API credentials

The Calls API is the Twilio-compatible API for Programmable Voice calls. It does not use API keys. It uses your voice account's credentials:

- **`AccountSid`**: the public name of your voice account, `AC` followed by 32 lowercase hexadecimal characters, such as `AC8d2e61f04a9b3c7e5d1f2a6b0c4e9d37`.
- **Auth token**: the voice account's secret.

Each project has one voice account. In the console, open **Developers**, then **Voice Account**: the `AccountSid` is shown with a copy button, and **Show** displays the auth token. Admins and engineers can view and rotate the token at any time; viewers and billing members cannot.

Rotating replaces the token in one step. The old token stops working when the change applies, usually within seconds. Update every client with the new token as soon as you rotate.

The auth token also signs the webhooks Signals sends you. See [Request signatures](/programmable-voice/webhooks#request-signatures).

Authenticate with HTTP Basic. The username is the `AccountSid` and the password is the auth token:

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

The username must equal the `AccountSid` in the path. If the header is missing, is not Basic, or names a different account, Signals refuses the request before it reaches the voice platform:

```json title="Response: 401 Unauthorized"
{
  "type": "https://signals.pressone.africa/problems/unauthenticated",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Basic authentication with the AccountSid as the username is required.",
  "code": "unauthenticated",
  "request_id": "req_0192b7f03a1e7c5d8b2f4e6a9c0d1b37"
}
```

A wrong or rotated auth token is rejected by the voice platform with `401` in Twilio's error format. An unknown or inactive `AccountSid` returns `404` with code `voice_route_not_proxied`.

The Calls API serves these resources:

| Method | Path under `/2010-04-01/Accounts/{AccountSid}` |
| --- | --- |
| `GET`, `POST` | `/Calls.json` |
| `GET`, `POST`, `DELETE` | `/Calls/{CallSid}.json` |
| `GET` | `/Calls/{CallSid}/Recordings.json` |
| `GET` | `/Calls/{CallSid}/Events.json` |
| `GET` | `/Recordings/{RecordingSid}` |

Any other path returns `404` with code `voice_route_not_proxied`. See [Moving from Twilio](/programmable-voice/twilio-compatibility) for parameter-level differences.

## Keep credentials safe

- **Keep keys on your server.** Never put an API key or auth token in a web page, mobile app, desktop app or anything else you ship to users. Anyone who has the file has the key.
- **Load keys from a secrets manager or environment variables.** Never commit them to source control or write them to logs.
- **Use one key per service.** A key named after the system that uses it can be rotated or revoked without touching anything else.
- **Grant the least access.** Choose the lowest role and the fewest scopes that work, and restrict the key to a subaccount when the service serves one customer.
- **Rotate on a schedule and after staff changes.** Use the overlap to deploy the new key before the old one stops.
- **Revoke at once if a key leaks.** Then create a replacement and check the audit trail for requests you do not recognize.
