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

# Quickstart

> Go from an API key to a retrieve response that carries sources, cognition and an honest degradation flag.

<Note>
  The API is in private preview and keys are issued to design partners. Everything on this page is the committed v1 contract, so the calls below are the ones that will work the moment your key is live.
</Note>

Five calls, in order. Copy each block, run it, and read the response before moving on. The base URL is `https://api.get-exo.com` everywhere on this site.

<Steps>
  <Step title="Get an API key" icon="key">
    Your first key comes from [the Exo dashboard](https://get-exo.com): sign in, open **Settings**, and generate a key. The plaintext is displayed once and stored only as a SHA-256 hash, so copy it before you close the panel. Once you hold a key with the `admin` scope you can mint further keys from the API itself with [`POST /v1/keys`](/authentication).

    Keys look like `exo_<env>_<token>`. A production key is minted `exo_prod_...`; a test-mode key from the sandbox family is minted `exo_sandbox_...` so it is recognisable on sight. Put yours in the environment rather than in source.

    <CodeGroup>
      ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      export EXO_KEY="exo_prod_your_key_here"
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      import os

      EXO_KEY = os.environ["EXO_KEY"]
      BASE = "https://api.get-exo.com"
      HEADERS = {"X-Exo-API-Key": EXO_KEY, "Content-Type": "application/json"}
      ```

      ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      const EXO_KEY = process.env.EXO_KEY!;
      const BASE = "https://api.get-exo.com";
      const HEADERS = {
        "X-Exo-API-Key": EXO_KEY,
        "Content-Type": "application/json",
      };
      ```
    </CodeGroup>
  </Step>

  <Step title="Confirm the key with GET /v1/me" icon="badge-check">
    `GET /v1/me` needs no data in the graph, so it is the cheapest proof that your key, your org and your scopes are what you think they are.

    <CodeGroup>
      ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      curl https://api.get-exo.com/v1/me \
        -H "X-Exo-API-Key: $EXO_KEY"
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      import requests

      me = requests.get(f"{BASE}/v1/me", headers=HEADERS)
      me.raise_for_status()
      print(me.json())
      ```

      ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      const me = await fetch(`${BASE}/v1/me`, { headers: HEADERS });
      if (!me.ok) throw new Error(`${me.status} ${await me.text()}`);
      console.log(await me.json());
      ```
    </CodeGroup>

    ```json 200 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    {
      "org": {
        "id": "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30",
        "displayName": "Acme",
        "slug": "acme"
      },
      "user": { "id": "4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93" },
      "scopes": ["read", "write"],
      "subjectDefault": "4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93",
      "plan": "default"
    }
    ```

    `scopes` is the list your key actually carries, and it is the first thing to check when a later call returns 403.

    `subjectDefault` is the identity your calls act as when you send no subject selector. It is always the key owner. It is never the resolved value of an `X-Exo-Subject` header, because internal subject ids are an isolation detail that does not leave the API.
  </Step>

  <Step title="Ingest a piece of content" icon="upload">
    `POST /v1/ingest` accepts the content and returns immediately with a job id. A background worker chunks it, embeds it and links it into the knowledge graph. Send an `Idempotency-Key` header so a retry returns the original job id instead of enqueuing a second copy.

    `metadata` is yours to shape, and two keys are read by the ingest worker: `title` becomes the node's title, and `source` is recorded as the file name it arrived under. Set `title` and you will see it again in step 5. Leave it out and the node is titled with the first line of your content.

    <CodeGroup>
      ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      curl -X POST https://api.get-exo.com/v1/ingest \
        -H "X-Exo-API-Key: $EXO_KEY" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: 4f7b2a10-quickstart" \
        -d '{
              "content": "We are dropping the freemium tier. The support load is not worth the top-of-funnel.",
              "metadata": {"title": "Pricing decision, June", "source": "quickstart"}
            }'
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      ingest = requests.post(
          f"{BASE}/v1/ingest",
          headers={**HEADERS, "Idempotency-Key": "4f7b2a10-quickstart"},
          json={
              "content": (
                  "We are dropping the freemium tier. "
                  "The support load is not worth the top-of-funnel."
              ),
              "metadata": {"title": "Pricing decision, June", "source": "quickstart"},
          },
      )
      ingest.raise_for_status()
      job_id = ingest.json()["jobId"]
      print(job_id)
      ```

      ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      const ingest = await fetch(`${BASE}/v1/ingest`, {
        method: "POST",
        headers: { ...HEADERS, "Idempotency-Key": "4f7b2a10-quickstart" },
        body: JSON.stringify({
          content:
            "We are dropping the freemium tier. The support load is not worth the top-of-funnel.",
          metadata: { title: "Pricing decision, June", source: "quickstart" },
        }),
      });
      if (!ingest.ok) throw new Error(`${ingest.status} ${await ingest.text()}`);
      const { jobId } = await ingest.json();
      console.log(jobId);
      ```
    </CodeGroup>

    ```json 202 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    {
      "jobId": "b1f0c6d2-8a44-4b21-9a1e-6d1f0c6d28a4",
      "status": "pending"
    }
    ```

    The status is `202`, not `200`. Nothing is in the graph yet.
  </Step>

  <Step title="Poll the job until it stops moving" icon="loader">
    `GET /v1/ingest/{job_id}/status` reports the job's phase, the phases already completed and a percentage estimate. A generic ingest carries no pipeline block, so `phase` stays `null` and `completedSteps` stays empty. Watch `status`.

    <CodeGroup>
      ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      curl "https://api.get-exo.com/v1/ingest/$JOB_ID/status" \
        -H "X-Exo-API-Key: $EXO_KEY"
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      import time

      while True:
          s = requests.get(f"{BASE}/v1/ingest/{job_id}/status", headers=HEADERS)
          s.raise_for_status()
          body = s.json()
          if body["status"] not in ("pending", "running"):
              break
          time.sleep(2)
      print(body)
      ```

      ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      let body: any;
      for (;;) {
        const s = await fetch(`${BASE}/v1/ingest/${jobId}/status`, {
          headers: HEADERS,
        });
        if (!s.ok) throw new Error(`${s.status} ${await s.text()}`);
        body = await s.json();
        if (body.status !== "pending" && body.status !== "running") break;
        await new Promise((res) => setTimeout(res, 2000));
      }
      console.log(body);
      ```
    </CodeGroup>

    ```json 200 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    {
      "jobId": "b1f0c6d2-8a44-4b21-9a1e-6d1f0c6d28a4",
      "status": "done",
      "phase": null,
      "percent": 100,
      "completedSteps": [],
      "errorMessage": null
    }
    ```

    <Warning>
      Poll for a terminal state, not for one spelling of success. This route reports the worker's own status, where the finished value is `done`. The cross-family view at `GET /v1/jobs/{job_id}` reports the same job as `succeeded`, which is the rename the jobs family documents. Break your loop when the status is neither `pending` nor `running` and you are correct on both routes.
    </Warning>
  </Step>

  <Step title="Retrieve against it" icon="search">
    `POST /v1/retrieve` is the flagship. It returns ranked sources, any contradictions it found, a reasoning block when the org's trained brain served the call, and a usage block.

    <CodeGroup>
      ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      curl -X POST https://api.get-exo.com/v1/retrieve \
        -H "X-Exo-API-Key: $EXO_KEY" \
        -H "Content-Type: application/json" \
        -d '{"query": "What did we decide about the freemium tier?", "topK": 5}'
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      r = requests.post(
          f"{BASE}/v1/retrieve",
          headers=HEADERS,
          json={"query": "What did we decide about the freemium tier?", "topK": 5},
      )
      r.raise_for_status()
      print(r.json())
      ```

      ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      const r = await fetch(`${BASE}/v1/retrieve`, {
        method: "POST",
        headers: HEADERS,
        body: JSON.stringify({
          query: "What did we decide about the freemium tier?",
          topK: 5,
        }),
      });
      if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
      console.log(await r.json());
      ```
    </CodeGroup>

    ```json 200 application/json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    {
      "query": "What did we decide about the freemium tier?",
      "sources": [
        {
          "chunkId": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2:0",
          "nodeId": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2",
          "title": "Pricing decision, June",
          "snippet": "We are dropping the freemium tier. The support load is not worth the top-of-funnel.",
          "score": 0.033333,
          "cosineInSeed": false
        }
      ],
      "contradictions": [],
      "cognition": null,
      "path": "hybrid",
      "degraded": true,
      "degradationReason": "no_brain",
      "latencyMs": 208,
      "traceId": "3f6c1d7a90b24e5aa1c8e2740b93df15",
      "usage": { "readUnits": 1, "embedTokens": 13, "brainInference": false }
    }
    ```

    The title is the one you sent. Node ids are opaque strings whose shape depends on how the content arrived, so read them, store them and pass them back, but never parse them. This one is content-hashed, which is why re-ingesting identical content updates the same node instead of duplicating it.
  </Step>
</Steps>

## What you saw

That last response is worth reading field by field, because four of its fields are the reason Exo is not a vector store with better marketing.

| Field                    | What it told you                                                                                                                                                                   |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sources`                | The ranked evidence. `snippet` is capped at 280 characters. Ask for `expand=sources.content` when you want the full stored text.                                                   |
| `sources[].score`        | On the hybrid path this is a reciprocal-rank-fusion score, so it is small by construction. Compare sources within one response; do not read it as a similarity or a probability.   |
| `sources[].cosineInSeed` | Whether the trained model held this node in its seed set. Always `false` on the hybrid path, which has no seed set.                                                                |
| `contradictions`         | Empty here, because one note cannot disagree with itself. Once the same subject records a second, conflicting position, this array carries the tension.                            |
| `cognition`              | `null`, and correctly so. A reasoning trace only exists on the brain path.                                                                                                         |
| `path`                   | `hybrid`. Your org has no trained ExoBrain yet, so the call was served by vector plus keyword search over your stored content.                                                     |
| `degraded`               | `true`, with `degradationReason: "no_brain"`. Exo says when it is running on the fallback rather than quietly returning worse results.                                             |
| `usage`                  | `readUnits` is the number of sources returned, the billable read unit. `embedTokens` is a documented estimate, not a metered figure. `brainInference` is false on the hybrid path. |

The other two values `degradationReason` can take are `brain_stale`, meaning the brain is serving a graph that has changed since it trained, and `cold_start`. Read [Brain path and hybrid path](/concepts/retrieval-paths) for what each one costs you.

<Note>
  The `sources` and `contradictions` arrays are open objects in the v1 contract, which is what lets the reasoning layer add fields without a breaking change. The keys shown above are the ones the route emits today. The canonical, listable contradiction object with a stable `id`, `a`, `b`, `tension` and `status` lives on [`GET /v1/contradictions`](/concepts/contradiction).
</Note>

## When it goes wrong

| Status | What happened                                                                                                                                                     |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 401    | The key is missing, malformed or revoked. Check the header name: it is `X-Exo-API-Key`, or `Authorization: Bearer` if you prefer that form.                       |
| 403    | The key authenticated but lacks the scope the route needs, or names a subject that was never provisioned. Code: [`permission_denied`](/errors/permission_denied). |
| 404    | The job id belongs to another subject or another org. Exo returns 404 rather than leaking that it exists. Code: [`job_not_found`](/errors/job_not_found).         |
| 422    | The body failed validation. The `errors[]` array names the offending field with a JSON pointer. Code: [`validation_error`](/errors/validation_error).             |
| 429    | You are over the rate limit. Wait for the interval in the `Retry-After` header. See [Rate limits](/platform/rate-limits).                                         |
| 503    | A dependency is unavailable. Safe to retry with backoff. Code: [`service_degraded`](/errors/service_degraded).                                                    |

Every error body is RFC 9457 `problem+json` and carries a `requestId`. Quote it if you need help. Full envelope on [Errors](/platform/errors).

<Note>
  `requestId` in the error body and the `X-Request-Id` response header are the id to quote in support. It is minted per request and looks like `req_<16 hex>`. The `traceId` in a successful retrieve response is a separate 32-character W3C trace id, propagated from an incoming `traceparent` header when you send one.
</Note>

## Next

<Columns cols={3}>
  <Card title="One memory per end user" icon="users" href="/guides/subjects">
    Provision a subject for each end user of your product, then select it with one header.
  </Card>

  <Card title="Condition your own agent" icon="message-square-quote" href="/guides/conditioning">
    Turn a subject's identity into a system prompt any LLM can take.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/overview">
    All 80 operations, with the traps that the generated schema cannot carry.
  </Card>
</Columns>
