> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pharussecurity.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Alert destinations

> GET, POST and DELETE /api/v1/alert-destinations: list, add and remove Slack, Teams and webhook destinations with a workspace token.

<Info>
  **Plans:** Core and Plus, and the free trial, to add destinations. Listing and removing keep working after a move to
  Starter. Adding and removing are refused with 402 while the workspace is read-only. **Who:** anyone with a workspace
  token; owners and admins create workspace tokens.
</Info>

These endpoints manage the same Slack, Microsoft Teams and webhook destinations as the **Slack, Teams and webhooks** card
under **Settings → Notifications**, so you can set up alerting from Terraform, a script or an internal tool. A
destination added through the API shows in Settings, and the list returns destinations added in Settings too.

```text theme={null}
GET    https://app.pharussecurity.com/api/v1/alert-destinations
POST   https://app.pharussecurity.com/api/v1/alert-destinations
DELETE https://app.pharussecurity.com/api/v1/alert-destinations/{id}
```

The API lists, adds and removes destinations. Changing a destination's name or events, turning it off, sending a test
and making a new signing secret are under **Settings → Notifications**. See
[notifications](/notifications/overview#slack-teams-and-webhooks).

## Authentication

Every request needs a workspace token in the `Authorization` header:

```http theme={null}
Authorization: Bearer phr_...
```

Create one under **Settings → API tokens** with **Create token**, and under **Access** choose **Manage Slack, Teams and
webhook destinations**. A CI upload token for a product family gets `401` here. See
[create a token](/api-reference/introduction#create-a-token).

## Example: add a Slack channel

<Steps>
  <Step title="Get an incoming webhook URL in Slack">
    In Slack, set up an incoming webhook for the channel (a Slack app with Incoming Webhooks turned on) and copy its URL.
    It starts with `https://hooks.slack.com/`.
  </Step>

  <Step title="Add the destination">
    ```bash theme={null}
    export PHARUS_TOKEN=phr_...   # a workspace token from Settings → API tokens

    curl --fail-with-body -sS -X POST "https://app.pharussecurity.com/api/v1/alert-destinations" \
      -H "Authorization: Bearer $PHARUS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "kind": "slack",
        "name": "#security-alerts",
        "url": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX",
        "events": ["incident.opened", "deadline.warning", "approval.requested", "report.received"]
      }'
    ```

    Pharus answers `201 Created` with the new destination:

    ```json theme={null}
    {
      "id": "0b7e5c2a-3d41-4f6e-9a8b-1c2d3e4f5a60",
      "kind": "slack",
      "name": "#security-alerts",
      "host": "hooks.slack.com",
      "events": ["incident.opened", "deadline.warning", "approval.requested", "report.received"],
      "enabled": true,
      "lastDeliveryAt": null,
      "lastError": null
    }
    ```
  </Step>

  <Step title="Test it">
    Adding a destination through the API doesn't send anything. To check it, go to **Settings → Notifications** and
    select **Send a test** on the destination. A message "Test alert from Pharus" should appear in the channel.
  </Step>
</Steps>

To post to two channels, add two destinations. Keep the `id` if your tool will remove the destination later.

## List destinations

```bash theme={null}
curl --fail-with-body -sS "https://app.pharussecurity.com/api/v1/alert-destinations" \
  -H "Authorization: Bearer $PHARUS_TOKEN"
```

### 200 OK

Every destination in the workspace, oldest first, however it was added.

```json theme={null}
{
  "destinations": [
    {
      "id": "0b7e5c2a-3d41-4f6e-9a8b-1c2d3e4f5a60",
      "kind": "slack",
      "name": "#security-alerts",
      "host": "hooks.slack.com",
      "events": ["incident.opened", "deadline.warning", "approval.requested", "report.received"],
      "enabled": true,
      "lastDeliveryAt": "2026-10-14T10:00:00.000Z",
      "lastError": null
    },
    {
      "id": "9d2f4b61-7a3c-4e58-b0d1-6c8e2f5a7b93",
      "kind": "webhook",
      "name": "SIEM intake",
      "host": "hooks.example.com",
      "events": ["incident.opened", "report.received"],
      "enabled": true,
      "lastDeliveryAt": "2026-10-13T08:02:41.000Z",
      "lastError": "HTTP 401: invalid token"
    }
  ]
}
```

| Field | Meaning |
| - | - |
| `id` | The destination's id, for `DELETE` |
| `kind` | `slack`, `teams` or `webhook` |
| `name` | The name shown in Pharus |
| `host` | The host name of the destination's URL. The URL itself is never returned: Pharus stores it encrypted. |
| `events` | The events it gets: `incident.opened`, `deadline.warning`, `approval.requested`, `report.received` |
| `enabled` | `false` when it was turned off with **On** in Settings. Pharus posts nothing to it then. |
| `lastDeliveryAt` | When Pharus last posted to it, tests included, ISO 8601 in UTC. `null` when nothing was sent yet. |
| `lastError` | Why that post failed, such as `HTTP 401: invalid token`. `null` when it went through or nothing was sent yet. |

## Add a destination

```text theme={null}
POST https://app.pharussecurity.com/api/v1/alert-destinations
```

Send a JSON object with `Content-Type: application/json`:

| Field | Required | Value |
| - | - | - |
| `kind` | Yes | `slack`, `teams` or `webhook` |
| `name` | Yes | 1 to 80 characters, shown in Pharus. Spaces at either end are removed. |
| `url` | Yes | The URL Pharus posts to, up to 2 000 characters. See the rules below. |
| `events` | Yes | One or more of `incident.opened`, `deadline.warning`, `approval.requested` and `report.received`. See [events](/notifications/overview#events). |

The body may be at most 16 384 characters.

### URL rules

Every URL starts with `https://`, has a full host name and has no user name or password in it. On top of that:

| `kind` | The URL |
| - | - |
| `slack` | On `hooks.slack.com`, with a path that starts with `/services/`, `/triggers/` or `/workflows/`. An incoming webhook URL starts with `https://hooks.slack.com/services/`. |
| `teams` | A Teams Workflows URL on `logic.azure.com` or `api.powerplatform.com`, or a subdomain of either. The retired Office 365 connector URLs don't work. |
| `webhook` | Your own HTTPS endpoint. When Pharus posts, the host name must resolve only to public addresses. See the [delivery rules](/api-reference/webhooks#delivery-rules). |

### 201 Created

The destination, in the same shape as in the list, turned on. For a webhook, the response also has `secret`:

```json theme={null}
{
  "id": "9d2f4b61-7a3c-4e58-b0d1-6c8e2f5a7b93",
  "kind": "webhook",
  "name": "SIEM intake",
  "host": "hooks.example.com",
  "events": ["incident.opened", "report.received"],
  "enabled": true,
  "lastDeliveryAt": null,
  "lastError": null,
  "secret": "whsec_..."
}
```

`secret` is the webhook's signing secret, starting with `whsec_`. Pharus signs every request to the endpoint with it;
see [verify the signature](/api-reference/webhooks#verify-the-signature) for how to check it, and
[outgoing webhooks](/api-reference/webhooks) for the request and body Pharus sends.

<Warning>
  This response is the only time Pharus returns the signing secret. Store it with your endpoint straight away. If it is
  lost, select **New signing secret** on the destination under **Settings → Notifications**; the old secret stops
  working at once.
</Warning>

`POST` isn't idempotent: sending the same body twice adds two destinations, and each gets every alert. If your tool
retries, list the destinations first, or remove the extra one.

## Remove a destination

```text theme={null}
DELETE https://app.pharussecurity.com/api/v1/alert-destinations/{id}
```

```bash theme={null}
curl --fail-with-body -sS -X DELETE \
  "https://app.pharussecurity.com/api/v1/alert-destinations/0b7e5c2a-3d41-4f6e-9a8b-1c2d3e4f5a60" \
  -H "Authorization: Bearer $PHARUS_TOKEN"
```

Pharus answers `204 No Content` and stops posting to the destination. Its failed deliveries stay listed under
**Delivery problems**. Removing works after a move to Starter too, so a workspace can wind its destinations down.

When the workspace has no destination with that `id`, Pharus answers `404` with `not_found`.

## Audit trail

Destinations added and removed through the API are recorded in the [audit trail](/reports/audit-trail), with the token's
name as the actor. Listing isn't recorded.

## Errors

Errors return `{ "error", "message" }`. A `400` with `invalid_request` adds `fields`, with one English message for each
field that is missing or invalid:

```json theme={null}
{
  "error": "invalid_request",
  "message": "Some fields are missing or invalid. See `fields`.",
  "fields": {
    "url": "A Slack incoming webhook starts with https://hooks.slack.com/.",
    "events": "One or more of incident.opened, deadline.warning, approval.requested, report.received."
  }
}
```

| Status | `error` | Endpoints | What to do |
| - | - | - | - |
| 400 | `invalid_json` | `POST` | The body isn't valid JSON. Check the quoting in your shell. |
| 400 | `invalid_request` | `POST` | Fix the fields named in `fields`. See the [body](#add-a-destination) and the [URL rules](#url-rules). |
| 401 | `unauthorized` | All | Send a workspace token. Check its expiry and status under **Settings → API tokens**. A CI upload token gets this too. The response has a `WWW-Authenticate` header. |
| 402 | `billing_lapsed` | `POST`, `DELETE` | The workspace is read-only. An owner subscribes under **Settings → Billing**, or reopens a closed workspace in company settings. |
| 403 | `plan_required` | `POST` | Slack, Teams and webhook alerts are on Core and Plus. An owner changes the plan under **Settings → Billing**. |
| 404 | `not_found` | `DELETE` | The workspace has no destination with that id. List the destinations to find it. |
| 413 | `too_large` | `POST` | The body is over 16 384 characters. Send only `kind`, `name`, `url` and `events`. |
| 429 | `rate_limited` | All | Wait the number of seconds in `Retry-After`. Each token has 60 requests per hour across `GET`, `POST` and `DELETE`. |
| 500 | `internal` | `POST` | The destination couldn't be stored. Try again. |
| 503 | `not_configured` | `POST` | Pharus can't store destination URLs at the moment. Contact support. |

Pharus checks in this order: the token, the rate limit, whether the workspace is read-only, then for `POST` the plan, whether Pharus can store destination URLs,
and the body.

## Related

<CardGroup cols={2}>
  <Card title="API introduction" icon="key-round" href="/api-reference/introduction" />

  <Card title="Outgoing webhooks" icon="webhook" href="/api-reference/webhooks" />

  <Card title="Notifications" icon="bell" href="/notifications/overview" />

  <Card title="Audit trail" icon="history" href="/reports/audit-trail" />
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.