> ## 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.

# Quickstart

> Make your first two calls to the Surfais API in about five minutes.

This page takes you from an API key to two successful calls: listing the organisations your key can reach, then reading a rollup of every own brand in one of them.

## Before you start

You need an API key. If you do not have one, see [Get a key](/api/introduction#get-a-key).

Export the key as an environment variable. Every sample in these docs reads it from `SURFAIS_API_KEY`.

```bash theme={null}
export SURFAIS_API_KEY="sfs_live_…"
```

The samples use cURL, Python 3 with [`requests`](https://pypi.org/project/requests/), and Node.js 18 or later, which has `fetch` built in. Run them from a server or your own machine — the API does not accept calls from a browser.

<Steps>
  <Step title="List the organisations your key can reach">
    Call `GET /orgs`.

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

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

      import requests

      API = "https://api.surfais.com/v1"
      HEADERS = {"Authorization": f"Bearer {os.environ['SURFAIS_API_KEY']}"}

      resp = requests.get(f"{API}/orgs", headers=HEADERS, timeout=30)
      if not resp.ok:
          body = resp.json()
          sys.exit(f"{resp.status_code} {body['error']['code']} (request {body['request_id']})")

      for org in resp.json()["data"]:
          print(org["id"], org["name"], org["link_scope"])
      ```

      ```javascript Node.js theme={null}
      const API = "https://api.surfais.com/v1";
      const headers = { Authorization: `Bearer ${process.env.SURFAIS_API_KEY}` };

      async function main() {
        const res = await fetch(`${API}/orgs`, { headers });
        const body = await res.json();
        if (!res.ok) {
          throw new Error(`${res.status} ${body.error.code} (request ${body.request_id})`);
        }
        for (const org of body.data) {
          console.log(org.id, org.name, org.link_scope);
        }
      }

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

    With an organisation key, the response holds exactly one organisation:

    ```json Response theme={null}
    {
      "data": [
        {
          "id": "7b1e6c1a-3f52-4d0e-9a57-2c8f1e0b4d11",
          "name": "Hotel Aurora Group",
          "tier": "scale",
          "link_scope": "owner",
          "external_ref": null
        }
      ],
      "pagination": {
        "next_cursor": null,
        "has_more": false
      }
    }
    ```

    * `data` holds the payload. Here it is a list, so `pagination` comes with it.
    * `pagination` appears on every list. `has_more: false` with `next_cursor: null` means this is the last page. When `has_more` is `true`, pass `next_cursor` back as `?cursor=` to read the next page — see [Pagination](/api/pagination).
    * `link_scope: "owner"` means you are using an organisation key. A partner key returns one item per linked organisation, with `link_scope` set to `read_only` or `read_write`, and `external_ref` set to your own id for that organisation if you have stored one.
    * `tier` is the identifier of the organisation's plan, in lower case (`scale` here). Treat it as an open string rather than a fixed list.

    Copy the organisation's `id`. Every endpoint that reads or changes an organisation's data sits under `/orgs/{orgId}`.

    ```bash theme={null}
    export SURFAIS_ORG_ID="7b1e6c1a-3f52-4d0e-9a57-2c8f1e0b4d11"
    ```
  </Step>

  <Step title="Get a one-call rollup of every own brand">
    Call `GET /orgs/{orgId}/brands/summary`. It returns one item per own brand, leaving out archived brands.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -sS "https://api.surfais.com/v1/orgs/$SURFAIS_ORG_ID/brands/summary" \
        -H "Authorization: Bearer $SURFAIS_API_KEY"
      ```

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

      import requests

      API = "https://api.surfais.com/v1"
      HEADERS = {"Authorization": f"Bearer {os.environ['SURFAIS_API_KEY']}"}
      ORG_ID = os.environ["SURFAIS_ORG_ID"]

      resp = requests.get(f"{API}/orgs/{ORG_ID}/brands/summary", headers=HEADERS, timeout=30)
      if not resp.ok:
          body = resp.json()
          sys.exit(f"{resp.status_code} {body['error']['code']} (request {body['request_id']})")

      for brand in resp.json()["data"]:
          latest = brand["latest"]
          score = latest["score"] if latest else None
          print(brand["name"], score, brand["delta"], brand["delta_reason"])
      ```

      ```javascript Node.js theme={null}
      const API = "https://api.surfais.com/v1";
      const headers = { Authorization: `Bearer ${process.env.SURFAIS_API_KEY}` };
      const orgId = process.env.SURFAIS_ORG_ID;

      async function main() {
        const res = await fetch(`${API}/orgs/${orgId}/brands/summary`, { headers });
        const body = await res.json();
        if (!res.ok) {
          throw new Error(`${res.status} ${body.error.code} (request ${body.request_id})`);
        }
        for (const brand of body.data) {
          const score = brand.latest ? brand.latest.score : null;
          console.log(brand.name, score, brand.delta, brand.delta_reason);
        }
      }

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

    ```json Response theme={null}
    {
      "data": [
        {
          "brand_id": "c2a4e8f0-6b1d-4c3a-8e5f-9d7b2a1c4e60",
          "name": "Hotel Aurora",
          "domain": "hotel-aurora.example",
          "external_ref": "PROP-01342",
          "latest": {
            "date": "2026-09-17",
            "score": 62.4,
            "score_version": "v2",
            "panel_version": "v3"
          },
          "previous": {
            "date": "2026-09-14",
            "score": 58.9,
            "score_version": "v2",
            "panel_version": "v3"
          },
          "delta": 3.5,
          "delta_reason": null,
          "avg_sentiment": 71.2,
          "sov": 41.7,
          "last_scan_at": "2026-09-17T06:42:10Z",
          "last_scan_run_id": "5e9d3b7a-1c4f-4a2e-b6d8-0f3a7c9e2b15",
          "changed_at": "2026-09-17T06:43:02Z"
        }
      ],
      "pagination": {
        "next_cursor": null,
        "has_more": false
      }
    }
    ```

    * `latest` and `previous` are the brand's two most recent score points. `score` is the [AIS Score](/metrics/ais-score) out of 100 on that `date`. `latest` is `null` until the brand has been scored, and `previous` is `null` until there is an earlier point. A brand with no scores yet reports `null`, never `0`.
    * `delta` is `latest.score` minus `previous.score`, to one decimal place. It is `null` when either score is missing. It is also `null` when the two scores are not comparable, because they were produced by different scoring models (`score_version`) or measured over different sets of AI models (`panel_version`). `delta_reason` then names the case. Do not subtract the scores yourself across a version change — see [Data model](/api/data-model).
    * `avg_sentiment` is the mean [sentiment](/metrics/sentiment) of the brand's mentions on the latest date, from 0 to 100. `sov` is the brand's share of voice on that date, as a percentage: its mentions divided by its own plus its tracked competitors' mentions. `sov` is `null` when nothing was mentioned.
    * `last_scan_at` is when the brand's latest scan was finalised. `changed_at` is a change token: it moves whenever something this endpoint reports can have changed — a scan being finalised or removed, the brand or one of its competitors being edited, or the organisation's scan schedule changing. Poll `changed_at`, not `last_scan_at`, to decide when to read again.

    There is no organisation-wide score. If you need one, aggregate the brands yourself.
  </Step>

  <Step title="Check your limits">
    Every successful response tells you where your key stands against its per-minute rate limit, and carries an id for the request.

    | Header                  | Meaning                                                                                                                |
    | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
    | `X-RateLimit-Limit`     | Requests your key may make per minute.                                                                                 |
    | `X-RateLimit-Remaining` | Requests left in the current minute.                                                                                   |
    | `X-RateLimit-Reset`     | When the current minute window ends, as Unix time in seconds.                                                          |
    | `X-Request-Id`          | The id of this request. On an error it equals `request_id` in the body. Log it, and quote it when you contact support. |

    Print them for your key:

    <CodeGroup>
      ```bash cURL theme={null}
      curl -sS -D - -o /dev/null https://api.surfais.com/v1/orgs \
        -H "Authorization: Bearer $SURFAIS_API_KEY"
      ```

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

      import requests

      resp = requests.get(
          "https://api.surfais.com/v1/orgs",
          headers={"Authorization": f"Bearer {os.environ['SURFAIS_API_KEY']}"},
          timeout=30,
      )
      print("status", resp.status_code)
      for name in ("X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", "X-Request-Id"):
          print(name, resp.headers.get(name))
      ```

      ```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}` },
        });
        console.log("status", res.status);
        for (const name of ["X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", "X-Request-Id"]) {
          console.log(name, res.headers.get(name));
        }
      }

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

    Limits are set per key, so read them from these headers rather than hard-coding a number. Over the limit, the API answers `429 rate_limited` with a `Retry-After` header: wait that many seconds, then retry. A key can also carry a monthly quota, which answers `429 quota_exceeded` once it is used up. See [Rate limits](/api/rate-limits).

    Rate limits count requests. Your plan's tracking caps — active prompts, own brands and countries — are separate: read them, with current usage, from `usage` on `GET /orgs/{orgId}`.
  </Step>
</Steps>

## If a call fails

Every error uses one envelope. Switch on `error.code`, never on `message`.

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

| Response                      | What it means                                                                                                 | What to do                                                                                     |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `401 invalid_api_key`         | The key is missing, mistyped, revoked or expired.                                                             | Check the `Authorization` header and the environment variable. Do not retry until it is fixed. |
| `403 tier_not_entitled`       | The organisation's plan does not include API access.                                                          | See [Who can use it](/api/introduction#who-can-use-it).                                        |
| `404 not_found`               | The `orgId` is not an organisation your key can reach. The API never confirms whether an organisation exists. | Use an `id` returned by `GET /orgs`.                                                           |
| `503 api_unavailable`         | The API is switched off. Every request gets this answer, with or without a key.                               | If it persists, [contact support](/api/support) and quote the request id.                      |
| Any `429`, or any other `503` | You are over a limit, or the API cannot serve the request right now.                                          | Wait the number of seconds in `Retry-After`, then retry.                                       |

[Errors](/api/errors) lists every code.

## Next steps

* [Authentication](/api/authentication) — organisation and partner keys, scopes, and the write rule.
* [Pagination](/api/pagination) — walk a list to the end, and how date windows work.
* [Provision a property](/api/guides/provision-a-property) — create an own brand with its competitors. Needs write access.
* [Sync a brand's prompts](/api/guides/sync-prompts) — send the prompt set you want and let the API work out the changes. Needs write access.
* [Request a scan](/api/guides/run-a-scan) — start an on-demand scan and wait for the results. Needs write access.
* [Export results](/api/guides/export-results) — pull results and mentions into your own store.
* [Webhooks](/api/webhooks/overview) — for platform partners: signed events when a scan completes.
