---
title: "Authentication and API keys"
description: "Create an API key, see which routes accept it, and handle responses, errors, pagination, rate limits and idempotency."
---

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

# Authentication and API keys

Your code authenticates to the Sureshake API with an API key. A key acts as the person who created it, so it can see and change only what that person can.

## Create an API key

You create and manage keys in the product. The `/v2/api-keys` routes accept only a signed-in session, so a key cannot create or rotate another key.

1. Sign in and open [Settings](https://sureshake.com/settings).
2. Open the **Security** section.
3. Under **API Keys**, select **Create API Key**.
4. Fill in the form:
   - **Key Name**, which is required.
   - **Description (Optional)**.
   - **Permissions**. See the note below.
   - **Expiration**: **Never**, **30 days**, **90 days** or **1 year**.
5. Select **Create Key** and copy the key. Sureshake shows it once and keeps only a hash, so you cannot see it again.

> **To create a write key, clear Read first**
>
> A key has one permission level, and the form uses the first box you selected.
> **Read** starts selected, so ticking **Write** as well still creates a
> read-only key. Clear **Read** and leave only **Write** selected. A write key
> can also make read calls. After you create the key, check the permission shown
> next to it in the list.

The form also has a **Rate Limit (requests/hour)** field. It has no effect at the moment. Every key gets the [same limits](#rate-limits).

To replace a key, create the new one, switch your code to it, then select **Revoke key** on the old one in the list. A revoked key stops working at once.

## Send the key

Send the key in the `X-API-Key` header:

```sh
curl -sS https://api.sureshake.com/v2/organizations/me \
  -H "X-API-Key: $SURESHAKE_API_KEY"
```

`Authorization: Bearer sk_...` works the same way on any route that accepts keys. If you send both, `X-API-Key` wins.

Use keys from a server. The API's CORS rules do not allow the `X-API-Key` header, and a key in browser code is visible to anyone who loads the page.

## Permissions

| Permission | Allows                          |
| ---------- | ------------------------------- |
| Read       | Routes that read data           |
| Write      | Routes that read or change data |

## Which routes accept an API key

> **Most routes do not accept a key**
>
> Of the routes in the [API reference](/reference/), about one in six accepts an
> API key. The others accept only the session token that the Sureshake web and
> mobile apps get when a person signs in. Sureshake has no public way for your
> code to get that token, so treat those routes as out of reach.

These routes accept a key:

| Area                              | Routes                                                                                                                                                                                                                                                                                                                                                           | Permission                                                        |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Organizations                     | Everything under `/v2/organizations`: your organizations and invitations, members, creating an organization, the organization wallet                                                                                                                                                                                                                             | Read for `GET`, Write otherwise                                   |
| Directory search and profiles     | `GET /v2/directory`, `/summary`, `/compare`, `/{subjectType}/{slug}` and its `/record-summary` and `/profile-experience`, and `POST /v2/directory/engagement/clicks`                                                                                                                                                                                             | Optional. These work without a key                                |
| Directory listings                | `/v2/directory/listings/...` (provision, draft, preview, publish, unpublish, rename, analytics, packages, claim requests, verification exceptions), `/v2/directory/packages/{id}`, `/v2/directory/person-profile/publication`, `/v2/directory/entity-profile/{entityId}/publication`, `/v2/directory/workflows/{id}`, `/v2/directory/collections/{collectionId}` | Read for `GET`, Write otherwise                                   |
| Directory follows and corrections | `/v2/directory/{subjectType}/{listingId}/follow`, `/follow-state`, `/follow-preferences`, `/follow-requests`, `/corrections`                                                                                                                                                                                                                                     | Read for `GET`, Write otherwise                                   |
| Collections                       | Everything under `/v2/entities/{entityId}/collections`, plus `GET /v2/collections/templates`, `GET /v2/collections/{id}/shared` and `POST /v2/collections/invites/{code}`                                                                                                                                                                                        | Read for `GET` and for `POST .../agent/messages`, Write otherwise |
| Companies                         | `POST /v2/entities`, and `/v2/entities/{id}/models/{templateKey}/scenarios`                                                                                                                                                                                                                                                                                      | Read for `GET`, Write otherwise                                   |
| Reports                           | `GET /v2/reports/{id}`, `/document`, `/publication-composition`, `POST /v2/reports/{id}/views`, `POST /v2/reports/shares/{shareId}/viewed`                                                                                                                                                                                                                       | Read                                                              |
| Search                            | `GET /v2/search/omni`                                                                                                                                                                                                                                                                                                                                            | Optional. Without a key you get directory results only            |

The other parts of the reference don't accept keys at all: `users`, `api-keys`, `filings`, `execution`, `analytics`, `billing`, `documents`, `hiring`, `vault`, `notifications`, `reporter`, `startups` and `watchlists`.

Within the areas above, routes not listed here need a session too. For example, a key can read a report by ID but cannot list your reports, and it can create a company but cannot list your companies. To get an ID, open the company or report in Sureshake and copy it from the URL: `/entities/<id>` or `/reports/<id>`.

## Errors when authenticating

| Status | `error.code`   | `error.message`                                                               | Meaning                                                        |
| ------ | -------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------- |
| 401    | `UNAUTHORIZED` | `Invalid or expired API key`                                                  | The key is wrong, revoked or expired                           |
| 401    | `UNAUTHORIZED` | `Authentication failed.`                                                      | The route does not accept keys, or you sent no credential      |
| 403    | `FORBIDDEN`    | `API key scope 'read' is not permitted for this operation (requires 'write')` | Your key's permission is too low                               |
| 403    | `FORBIDDEN`    | Varies                                                                        | The key is valid, but its owner has no access to this resource |

On routes where a credential is optional, such as `GET /v2/directory` and `GET /v2/search/omni`, an invalid key does not cause an error. The request runs as anonymous and you get public results only.

## Response formats

Dates are ISO 8601 strings. Most routes return one of two shapes.

### Single item

```json
{
  "data": { "id": "org-acme-holdings", "name": "Acme Holdings" },
  "meta": { "timestamp": "2026-11-03T18:00:00.000Z" }
}
```

### List

```json
{
  "data": [{ "id": "org-acme-holdings" }],
  "meta": { "total": 1, "limit": 20, "offset": 0, "hasMore": false }
}
```

Some routes use their own shape. `GET /v2/directory` returns `data`, `pagination`, `resultCount` and `facets`. `GET /v2/search/omni` returns `status`, `workspace` and `directory`. The response schema on each route's [reference page](/reference/) is the one to follow.

## Errors

Errors have this shape:

```json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Entity not found",
    "details": {},
    "traceId": "V1StGXR8_Z5jdHi6B-myT"
  }
}
```

Branch on the status and `code`, not on `message`. `details` and `traceId` are optional. Errors from the authentication step have no `traceId` in the body.

| Status | `error.code`                        | Notes                                                                                                                   |
| ------ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| 400    | `FST_ERR_VALIDATION`                | The request failed schema validation. `message` is a JSON array of issues as a string, each with a `path` and `message` |
| 400    | `BAD_REQUEST`                       | The request was well formed but can't be carried out                                                                    |
| 401    | `UNAUTHORIZED`                      | See [errors when authenticating](#errors-when-authenticating)                                                           |
| 403    | `FORBIDDEN`                         | You don't have access                                                                                                   |
| 404    | `NOT_FOUND`                         | The resource wasn't found                                                                                               |
| 409    | `CONFLICT`                          | For example, an organization with this name or slug already exists                                                      |
| 410    | `GONE`                              | The resource is no longer available                                                                                     |
| 412    | `PRECONDITION_FAILED`               | A condition for the operation isn't met                                                                                 |
| 422    | `UNPROCESSABLE_ENTITY`              | See [idempotency](#idempotency)                                                                                         |
| 429    | `TOO_MANY_REQUESTS`, `RATE_LIMITED` | See [rate limits](#rate-limits)                                                                                         |
| 500    | `INTERNAL_SERVER_ERROR`             | An unexpected error. If it keeps happening, report it with the trace ID                                                 |
| 503    | `DEPENDENCY_UNAVAILABLE`            | Something the API depends on is down. Retry with backoff                                                                |

You may see other codes. Go by the status when you don't recognize one.

Validation runs before authentication, so a malformed request can return `400` even if your key is invalid.

A path that doesn't exist returns a different shape: `{ "message": "...", "error": "Not Found", "statusCode": 404 }`.

### Trace IDs

Every response has an `x-trace-id` header. You can also set your own by sending `x-trace-id`. Include the value when you report a problem.

## Pagination

List routes page in one of two ways. The route's reference page shows which.

**Offset.** Pass `limit` and `offset`. `limit` is usually 1 to 100 and defaults to 20. Keep adding `limit` to `offset` while `meta.hasMore` is `true`.

```sh
curl -sS "https://api.sureshake.com/v2/entities/$ENTITY_ID/collections?limit=100&offset=100" \
  -H "X-API-Key: $SURESHAKE_API_KEY"
```

**Cursor.** `GET /v2/directory` returns `pagination.nextCursor`. Pass it back as `cursor` while `pagination.hasMore` is `true`.

```sh
curl -sS "https://api.sureshake.com/v2/directory?limit=100&cursor=$NEXT_CURSOR" \
  -H "X-API-Key: $SURESHAKE_API_KEY"
```

## Rate limits

| Applies to                                                         | Limit                                                                                          |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| All routes                                                         | 2,000 requests per minute for each key. Without a credential, the limit applies per IP address |
| `GET /v2/directory`, `/compare`, `/summary`, `GET /v2/search/omni` | 240 per minute with a valid credential, 60 per minute without                                  |
| `POST /v2/directory/engagement/clicks`                             | 120 per minute with a valid credential, 30 per minute without                                  |
| `/v2/mcp`                                                          | 120 per minute                                                                                 |

Responses carry `x-ratelimit-limit` and `x-ratelimit-remaining`, plus `x-ratelimit-reset`. Don't parse the reset value on its own: on the directory and search routes above it is a timestamp, and elsewhere it is a number of seconds.

When you go over a limit, you get a `429` with a `retry-after` header in seconds. Wait that long, then retry. The `code` is `RATE_LIMITED` on the directory and search routes above and `TOO_MANY_REQUESTS` elsewhere, so check the status rather than the code.

## Idempotency

Some write routes let you retry safely. Send a fresh UUID in `X-Idempotency-Key` for each logical operation, and reuse it if you retry that operation.

With an API key, this works on these routes:

- [`POST /v2/organizations`](/reference/organizations/create/)
- `POST /v2/organizations/{id}/members`
- `PATCH /v2/organizations/members/{memberId}`
- `POST /v2/organizations/invitations/{memberId}/accept`
- `POST /v2/organizations/invitations/{memberId}/decline`

Other routes ignore the header.

```sh
curl -sS -X POST https://api.sureshake.com/v2/organizations \
  -H "X-API-Key: $SURESHAKE_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme Holdings"}'
```

This needs a key with **Write** permission, and it creates a real organization. On success you get `201` and the new organization in `data`.

| What happens                                                 | Response                                                       |
| ------------------------------------------------------------ | -------------------------------------------------------------- |
| Same key and body, and the first request finished with a 2xx | The stored response again, with `x-idempotency-replayed: true` |
| Same key, and the first request is still running             | `409 CONFLICT`                                                 |
| Same key, different method, URL or body                      | `422 UNPROCESSABLE_ENTITY`                                     |
| The first request failed                                     | The key is released, so a retry runs the operation again       |

Keys belong to the user who sent them, and only 2xx responses are stored.

Source: https://docs.sureshake.com/developers/authentication/index.mdx
