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

# Authentication

> How API keys work: the Bearer header, organisation and partner keys, scopes, and what an authentication failure looks like.

Every request to the Surfais API carries an API key. The key is the only credential: there is no login step, no session and no token exchange. If you do not have a key yet, see [Get a key](/api/introduction#get-a-key).

## Send the key

Put the key in the `Authorization` header of every request, using the `Bearer` scheme:

```http theme={null}
Authorization: Bearer sfs_live_…
```

The API reads the key from that header only. Never put a key in a URL or a request body.

<CodeGroup>
  ```bash cURL theme={null}
  curl -sS -w "\nHTTP %{http_code}\n" https://api.surfais.com/v1/orgs \
    -H "Authorization: Bearer $SURFAIS_API_KEY"
  ```

  ```python Python theme={null}
  import os
  import sys

  import requests

  resp = requests.get(
      "https://api.surfais.com/v1/orgs",
      headers={"Authorization": f"Bearer {os.environ['SURFAIS_API_KEY']}"},
      timeout=30,
  )
  if resp.status_code == 401:
      # Every authentication failure looks the same. A retry cannot succeed until the key is fixed.
      sys.exit(f"Key rejected (request {resp.json()['request_id']}). Check SURFAIS_API_KEY.")
  if not resp.ok:
      body = resp.json()
      sys.exit(f"{resp.status_code} {body['error']['code']} (request {body['request_id']})")
  print(resp.json()["data"])
  ```

  ```javascript Node.js theme={null}
  async function main() {
    const res = await fetch("https://api.surfais.com/v1/orgs", {
      headers: { Authorization: `Bearer ${process.env.SURFAIS_API_KEY}` },
    });
    const body = await res.json();
    if (res.status === 401) {
      // Every authentication failure looks the same. A retry cannot succeed until the key is fixed.
      throw new Error(`Key rejected (request ${body.request_id}). Check SURFAIS_API_KEY.`);
    }
    if (!res.ok) {
      throw new Error(`${res.status} ${body.error.code} (request ${body.request_id})`);
    }
    console.log(body.data);
  }

  main().catch((err) => {
    console.error(err.message);
    process.exit(1);
  });
  ```
</CodeGroup>

## Key format

A key is the prefix `sfs_live_` followed by 43 characters from the set `0–9`, `A–Z` and `a–z`: 52 characters in total. Treat it as an opaque, case-sensitive string. A value that does not have this shape is answered `401 invalid_api_key`, like any other bad key.

Keys that start with `sfs_test_` are rejected by the production API.

## Organisation keys and partner keys

|                            | Organisation key                                               | Partner key                                                                                                                                                                              |
| -------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Reaches                    | One organisation: the one it was issued for.                   | Every organisation with an active link to your platform-partner account.                                                                                                                 |
| `{orgId}` in a path        | Must be the key's own organisation.                            | Must be an organisation with an active link. The link is read on every request, so a revoked link stops working at once.                                                                 |
| Any other `{orgId}`        | `404 not_found`                                                | `404 not_found`                                                                                                                                                                          |
| `GET /orgs` returns        | One item, with `link_scope: "owner"` and `external_ref: null`. | One item per linked organisation, ordered by id. `link_scope` is `read_only` or `read_write`, and `external_ref` is your own id for the organisation, or `null` if you have not set one. |
| Webhook management         | `403 partner_only`                                             | Available.                                                                                                                                                                               |
| `PATCH /orgs/{orgId}/link` | `403 partner_only`: an organisation key has no link.           | Available. Sets your `external_ref` on the link.                                                                                                                                         |
| Keeps working while        | The organisation is on a plan that includes API access.        | Your partner account is active.                                                                                                                                                          |

The Surfais team creates and revokes the links between a partner account and its organisations.

`PATCH /orgs/{orgId}/link` is a write, and the write rule below is checked first. A key without the `write` scope gets `403 write_scope_required` there, whichever kind of key it is.

## Scopes and the write rule

Two settings decide what a request may do.

* **The key's scopes.** A key is issued with `read`, or with `read` and `write`, according to the access your plan, or your partner agreement, includes — see [Who can use it](/api/introduction#who-can-use-it). Every key carries `read`.
* **The link's scope** (partner keys only). A partner's link to each organisation is `read_only` or `read_write`. You can read it as `link_scope` on `GET /orgs`. An organisation key has no link: only its own scopes apply.

The effective permission is the lower of the two. Reads need only `read`. A write — any `POST`, `PATCH`, `PUT` or `DELETE` — needs the `write` scope and, for a partner key, a `read_write` link.

| Key scopes      | Link                        | Reads   | Writes                        |
| --------------- | --------------------------- | ------- | ----------------------------- |
| `read`          | None (organisation key)     | Allowed | `403 write_scope_required`    |
| `read`, `write` | None (organisation key)     | Allowed | Allowed                       |
| `read`          | `read_only` or `read_write` | Allowed | `403 write_scope_required`    |
| `read`, `write` | `read_only`                 | Allowed | `403 link_scope_insufficient` |
| `read`, `write` | `read_write`                | Allowed | Allowed                       |

Webhook management writes need the `write` scope too, and answer the same `403 write_scope_required` without it. They involve no organisation, so no link scope applies.

<Note>
  An organisation your key cannot reach is `404 not_found` before any write-rule `403` (`write_scope_required`, `link_scope_insufficient` or `partner_only`). The API never confirms that an organisation exists. A refused write is also rejected before its body is read, so a `403` says nothing about whether the body was valid, and it does not use up an [`Idempotency-Key`](/api/idempotency).
</Note>

## Access is re-checked on every request

An organisation key works only while its organisation is on a plan that includes API access. The plan is checked on every request. If the organisation moves to a plan without API access, the key's next call is answered `403 tier_not_entitled`:

```json theme={null}
{
  "error": {
    "code": "tier_not_entitled",
    "message": "This organisation's plan does not include API access."
  },
  "request_id": "0d6f4c1e-8a2b-4f7d-9c35-6e1b8a4d2f90"
}
```

This `403` counts against the key's per-minute allowance like any other request, so it carries the `X-RateLimit-*` headers, and a key that is already over its allowance is answered `429 rate_limited` first.

Partner keys have equivalent checks. The partner account must be active, or every request is `401 invalid_api_key`. The link to the organisation in the path must be active, or the request is `404 not_found`.

## When authentication fails

Every authentication failure is the same response, deliberately. It does not tell the caller which check failed.

```json theme={null}
{
  "error": {
    "code": "invalid_api_key",
    "message": "Invalid API key."
  },
  "request_id": "0d6f4c1e-8a2b-4f7d-9c35-6e1b8a4d2f90"
}
```

You get `401 invalid_api_key` when:

* the `Authorization` header is missing, or is not `Bearer` followed by a key;
* the key does not have the [key format](#key-format);
* the key is unknown, revoked or expired;
* the key starts with `sfs_test_`;
* the key belongs to a platform partner whose account is suspended;
* the key belongs to an organisation that has been deleted.

Do not retry a `401`. The same request fails the same way until the key is fixed.

### Repeated failures are throttled

Failed attempts are counted per client IP address, per minute. After too many in one minute, requests from that address that present a key are answered `429 rate_limited` until the minute ends — even when the key is valid. The response carries `Retry-After`, the number of seconds until the minute ends. On this `429`, the `X-RateLimit-*` headers describe the address's failed-attempt allowance, not your key's rate limit.

Every `401` for a request that presented a key counts, whatever the reason. A request with no usable `Authorization` header is answered `401` but is not counted.

<Warning>
  If several of your services share one outbound IP address, one service looping on a bad key can lock the others out until the minute ends. Alert on `401` instead of retrying it.
</Warning>

## Keep keys safe

* **Call the API from your server only.** CORS is disabled: the API never sends an `Access-Control-Allow-Origin` header, so a browser refuses the responses, and a preflight request is answered `405 cors_disabled`. Never ship a key in a web page or a mobile app.
* **Store keys in a secret manager.** Keep them out of source control, logs and error reports.
* **Use one key per integration and per environment.** One key can then be revoked without touching the others.
* **Ask for the least access you need.** Ask for a `read` key unless the integration changes what is tracked. Write access also depends on your plan.
* **Set an expiry where it helps.** A key can be issued with an expiry date, which suits a contractor or a time-boxed project. Without one, a key does not expire. After its expiry, a key is answered `401 invalid_api_key`.
* **Never send a whole key to anyone**, Surfais included. Surfais keeps only a hash of each key, so a lost key cannot be recovered — ask for a new one.

If you need to refer to a key — in a ticket, a chat or an email — quote only its first 12 characters: the `sfs_live_` prefix plus the next three. That is enough to tell your keys apart, and too little to be of use to anyone else.

### Rotate a key

Two keys work side by side, so you can rotate without downtime.

1. Ask [support@surfais.com](mailto:support@surfais.com) for a new key with the same access.
2. Deploy the new key, and confirm your traffic has moved to it.
3. Ask for the old key to be revoked. A revoked key is answered `401 invalid_api_key` from its next request.

If a key may have leaked, ask for it to be revoked straight away, then rotate.

An organisation can hold up to 5 active keys at once. A key is active until it is revoked or expires. When an organisation already holds 5, one must be revoked before another can be issued. Partner keys do not count towards an organisation's limit.

## Related

* [Errors](/api/errors) — the error envelope and every error code.
* [Rate limits](/api/rate-limits) — per-minute limits, monthly quotas and `Retry-After`.
