---
title: "Authentication"
description: "One credential type, and the ways a request is refused."
url: "https://openemail.uk/docs/api/authentication"
area: "API"
category: "Getting started"
---

# Authentication

One credential type, and the ways a request is refused.

## The header

The base URL is `api.openemail.uk`. Every request carries the key in an `Authorization` header.

**Authorization**

```
Authorization: Bearer oe_live_9f2c1a4b7e05d3862c1f0a44_kX7…
```

Nothing else authenticates here. A session cookie and a session token are both refused with `invalid_credential_type`, which names the credential to send instead rather than leaving you to guess at a bare 401.

## Check a key works

`GET /ping` is the smoke test: it needs no scope and tells you what the key is.

**curl**

```
curl "$OE/ping" -H "$AUTH"
```

**Response**

```
{
  "ok": true,
  "keyId": "4c1b257a66287fd113bd89d0",
  "mode": "live",
  "scopes": ["emails:send", "emails:read"],
  "roleId": null,
  "grantedScopes": ["emails:send", "emails:read"],
  "workspaceId": "10417196-e324-4283-af98-66ec62167c47"
}
```

> If this works and something else 401s, the problem is the scope, not the key.

> `scopes` is the EFFECTIVE list and the only one that authorises anything. `grantedScopes` is what the key was issued with, and the two differ only when a role is capping the key. The Scopes page explains that intersection. A `roleId` of `null` means no ceiling, which is the widest a key gets.

## See what a key may send as

`GET /addresses` is the answer to a 403 you did not expect.

**curl**

```
curl "$OE/addresses" -H "$AUTH"
```

**Response**

```
{
  "object": "list",
  "unrestricted": false,
  "data": [
    { "object": "address", "address": "billing@acme.com", "enabled": true, "canSend": true },
    { "object": "address", "address": "postmaster@acme.com", "enabled": true, "canSend": false }
  ],
  "domains": [
    { "domain": "acme.com", "receivingVerified": true, "sendingVerified": true, "catchAll": false }
  ]
}
```

> `canSend: false` has three causes: the address is switched off, the key's send scope leaves it out (neither its domain nor the address itself is on the key), or the domain cannot sign yet. `enabled` on the address and `sendingVerified` on its domain tell those apart, and that is most of the debugging time this endpoint saves. A domain can be verified for receiving and still unable to send.

> `unrestricted: true` means any local-part on a verified domain is accepted, including ones nobody has created yet.

## How a key is refused

| Code | Means |
| --- | --- |
| missing_api_key | No Authorization header at all. |
| invalid_credential_type | A cookie or a session token. Send an API key. |
| invalid_api_key | Not a key we issued, or the secret does not match. |
| revoked_api_key | Issued here, then revoked. Distinct on purpose. It is the difference between a five-minute fix and an afternoon. |
| expired_api_key | Issued here, then expired. |
| insufficient_scope | A real key, without the scope this endpoint needs. |

Revoking takes effect on the next call. The row stays on the keys page afterwards, so you can still tell whether anything was using the key when you killed it. The most useful state on that screen is "never used", because it is how a leaked key is told from a live dependency.

Rotating is the other way to retire a secret. It mints a new secret for the same key, so the id, the name, the scopes, the role, the send scope and every request and activity row all carry on; only the secret changes. The old one stops working the instant the rotation completes, with no overlap window, and the replacement is shown once. From the console it is in the same menu as Revoke and asks you to re-verify first. A key holding `keys:write` can also rotate itself with `POST /keys/self/rotate`, which is how an integration rotates on a schedule without anyone opening the console.
