---
title: Authentication
description: Create API keys, send them with each request, choose scopes and handle authentication errors.
product: signal
section: api
order: 2
---

# Authentication

Every request to the Signal REST API is authenticated with an API key. A key belongs to one workspace: every request made with it reads and writes that workspace's data, and nothing else. A key stands for the workspace, not for a person, so records it creates aren't credited to anyone unless you say who (see [Assigned To and owner fields](conventions.md#assigned-to-and-owner-fields)).

## Create a key

1. In Signal, open **Autopilot** in the sidebar.
2. Select the **Webhooks & API** tab, then **API Keys**.
3. Select **New Key**.
4. Enter a **Key Name** that says where the key is used (for example "Production integration").
5. Tick the **Scopes** the key needs. Give each key only the scopes its integration uses.
6. Select **Create Key**.
7. Copy the key from the **Save your API key** dialog and store it somewhere safe, such as your secrets manager. **This is the only time Signal shows the full key.** Afterward the list shows only its first characters.

### Who can create keys

- Anyone whose role can **view integrations** can see the workspace's keys (name, first characters, scopes, status, created and last-used dates).
- Only people whose role can **manage integrations** can create or revoke keys. Workspace admins can by default.
- A key can do no more than the person who creates it. For the scopes below that map to a role permission, the creator must hold that permission:

| Scope | Creator needs |
| --- | --- |
| `accounts:read` | Read Accounts |
| `accounts:write` | Create, update and delete Accounts |
| `contacts:read` | Read Contacts |
| `contacts:write` | Create, update and delete Contacts |
| `deals:read` | Read Deals |
| `deals:write` | Create, update and delete Deals |
| `activities:read` | Read activities |
| `activities:write` | Create, update and delete activities |
| `users:read` | Read team members |
| `marketing:read` | A plan that includes Marketing, Marketing switched on for their role, and read access to Marketing |
| `marketing:write` | A plan that includes Marketing, Marketing switched on for their role, and update access to Marketing |

The Marketing scopes are offered only when your plan includes Marketing.

### Key format

Keys look like `sk_live_` followed by 64 hexadecimal characters:

```
sk_live_3b1f0c9e7a2d4e6f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f
```

Signal stores only a hash of the key, so a lost key can't be recovered. Create a new one instead.

Keys created in Signal don't expire. They work until you revoke them.

## Send the key

Send the key on every request in either of these headers. Both work the same way.

**`Authorization` header (recommended):**

```bash
curl https://crm.empiraa.app/api/v1/me \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**`X-API-Key` header:**

```bash
curl https://crm.empiraa.app/api/v1/me \
  -H "X-API-Key: YOUR_API_KEY"
```

```js
// Either form works with fetch
await fetch("https://crm.empiraa.app/api/v1/me", {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});

await fetch("https://crm.empiraa.app/api/v1/me", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
```

If you send both, the `Authorization: Bearer` value is used.

Keep keys on your server. Don't put them in browser code, mobile apps or public repositories, where anyone can read them.

## Scopes

Each key carries a list of scopes. An endpoint checks for its scope on every request and answers `403` if the key doesn't have it. `GET /me` needs no scope.

| Scope | Allows | Endpoints |
| --- | --- | --- |
| `accounts:read` | Read Accounts and their spend records | `GET /accounts`, `GET /accounts/{id}`, `GET /accounts/{id}/spends`, `GET /accounts/{id}/spends/{spendId}` |
| `accounts:write` | Create, update and deactivate Accounts; add, change and delete spend records | `POST /accounts`, `PATCH /accounts/{id}`, `DELETE /accounts/{id}`, `POST /accounts/{id}/spends`, `PATCH /accounts/{id}/spends/{spendId}`, `DELETE /accounts/{id}/spends/{spendId}` |
| `contacts:read` | Read Contacts | `GET /contacts`, `GET /contacts/{id}` |
| `contacts:write` | Create, update and delete Contacts | `POST /contacts`, `PATCH /contacts/{id}`, `DELETE /contacts/{id}` |
| `deals:read` | Read Deals | `GET /deals`, `GET /deals/{id}` |
| `deals:write` | Create, update and delete Deals | `POST /deals`, `PATCH /deals/{id}`, `DELETE /deals/{id}` |
| `pipeline:read` | Read pipelines and their stages | `GET /pipelines`, `GET /pipelines/{id}` |
| `pipeline:write` | Create, rename and delete pipelines | `POST /pipelines`, `PATCH /pipelines/{id}`, `DELETE /pipelines/{id}` |
| `prospects:read` | Read prospect contacts, Prospect Companies and prospect sources | `GET /prospects`, `GET /prospects/{id}`, `GET /prospect-companies`, `GET /prospect-companies/{id}`, `GET /prospect-sources`, `GET /prospect-sources/{id}` |
| `prospects:write` | Create, update and delete prospect contacts and Prospect Companies | `POST /prospects`, `PATCH /prospects/{id}`, `DELETE /prospects/{id}`, `POST /prospect-companies`, `PATCH /prospect-companies/{id}`, `DELETE /prospect-companies/{id}` |
| `activities:read` | Read activities and calls | `GET /activities`, `GET /activities/{id}`, `GET /calls`, `GET /calls/{id}` |
| `activities:write` | Create, update and delete activities; log calls | `POST /activities`, `PATCH /activities/{id}`, `DELETE /activities/{id}`, `POST /calls`, `POST /call-logs` |
| `tasks:read` | Read the `/tasks` list | `GET /tasks`, `GET /tasks/{id}` |
| `tasks:write` | Create, update and delete items on the `/tasks` list | `POST /tasks`, `PATCH /tasks/{id}`, `DELETE /tasks/{id}` |
| `notes:read` | Read notes on prospect contacts | `GET /notes`, `GET /notes/{id}` |
| `notes:write` | Create, edit and delete notes | `POST /notes`, `PATCH /notes/{id}`, `DELETE /notes/{id}` |
| `bookings:read` | Read Scheduler bookings | `GET /bookings`, `GET /bookings/{id}` |
| `bookings:write` | Create, update and cancel bookings | `POST /bookings`, `PATCH /bookings/{id}`, `DELETE /bookings/{id}` |
| `decks:read` | Read Signal Decks | `GET /decks`, `GET /decks/{id}` |
| `decks:write` | Create, update and delete Decks | `POST /decks`, `PATCH /decks/{id}`, `DELETE /decks/{id}` |
| `users:read` | Read the workspace's users | `GET /users`, `GET /users/{id}` |
| `portfolio:read` | Read portfolio summary figures | `GET /portfolio` |
| `marketing:read` | Read Campaigns (needs a plan that includes Marketing) | `GET /marketing/campaigns` |
| `marketing:write` | Reserved. No endpoint uses it yet. | None |
| `property:read` | Read developments, plots, reservations and call-offs | `GET /property/developments`, `GET /property/developments/{id}`, `GET /property/plots`, `GET /property/plots/{id}`, `GET /property/reservations`, `GET /property/reservations/{id}`, `GET /property/call-offs`, `GET /property/call-offs/{id}` |
| `property:write` | Create, update and delete developments, plots and reservations; book call-offs and record their invoices and payments | `POST`, `PATCH` and `DELETE` on `/property/developments`, `/property/plots` and `/property/reservations`; `POST /property/call-offs`, `PATCH /property/call-offs/{id}` |

A key can't be given new scopes after it's created. To change what a key can do, create a new key with the scopes you want and revoke the old one.

## Rotate a key

Keys don't expire, so rotate them on your own schedule and whenever someone who had access leaves:

1. Create a new key with the same scopes.
2. Deploy the new key to your integration.
3. Check the old key's **last used** time in the API Keys list stops changing.
4. Revoke the old key.

## Revoke a key

In **Autopilot → Webhooks & API → API Keys**, select the revoke button on the key's row and confirm with **Revoke**. Revoking takes effect on the next request: the key is rejected with `401 INVALID_API_KEY`. A revoked key stays in the list, marked **Revoked**, and can't be turned back on.

## Authentication errors

Errors use the [problem details format](conventions.md#errors).

### 401 Unauthorized

No key was sent:

```json
{
  "type": "https://signal.empiraa.com/errors/missing-api-key",
  "title": "Missing API key",
  "status": 401,
  "code": "MISSING_API_KEY",
  "detail": "Provide a key via Authorization: Bearer or X-API-Key"
}
```

The key doesn't exist, was revoked or has expired:

```json
{
  "type": "https://signal.empiraa.com/errors/invalid-api-key",
  "title": "Invalid or revoked API key",
  "status": 401,
  "code": "INVALID_API_KEY"
}
```

### 403 Forbidden

The key is valid but lacks the endpoint's scope. `detail` names the scope it needs:

```json
{
  "type": "https://signal.empiraa.com/errors/insufficient-scope",
  "title": "Insufficient scope",
  "status": 403,
  "code": "INSUFFICIENT_SCOPE",
  "detail": "This key requires the 'deals:write' scope"
}
```

`GET /marketing/campaigns` also answers `403` with code `PLAN_FEATURE_REQUIRED` when the workspace's plan no longer includes Marketing. A key keeps working as a key after a downgrade, but the Marketing endpoint stops answering. See [Marketing Campaigns](reference/marketing-campaigns.md).

### 402 Payment Required

Write requests (`POST`, `PATCH`, `DELETE`) answer `402 WORKSPACE_PAYMENT_REQUIRED` when the workspace's subscription doesn't currently allow changes, for example after a failed payment. Reads keep working. Update the billing details in Signal to restore writes.

```json
{
  "type": "https://signal.empiraa.com/errors/workspace-payment-required",
  "title": "Workspace payment required. Please update your billing details.",
  "status": 402,
  "code": "WORKSPACE_PAYMENT_REQUIRED"
}
```
