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

# 5. Call Priors from your own backend

> Mint an API key, create one agent per customer, name the person on each call, and list one person's conversations. Each step shows the success response and what to do if it fails.

This walkthrough puts the whole integration in one place: one agent per customer, one thread per conversation, and the header that says which of your customers a request acts for. The other walkthroughs each cover one call in more depth.

You need an API key. See [Authentication](/docs/authentication).

The table below is the plan.

| Step | Call | What you receive | Then |
| - | - | - | - |
| 1 | **Settings → API keys** in the Priors application | The key string, shown once | Store it |
| 2 | `POST /v1/agents` with `visibility: external` | `202`, `initializing` | Poll |
| 3 | `POST .../threads` with `X-SP-End-User` | `201`, a `thread_id` | Continue |
| 4 | `POST .../observe-and-respond` | `200`, `response_context` | Return the answer |
| 5 | `GET /v1/agents/{agent_id}` with `X-SP-End-User` | `200`, that person's `threads` | Continue |

## The model

Two things identify a request.

* **The key** says *which company* is calling. It resolves to your organization, and your organization owns the agents, holds the credit, and is billed.
* **`X-SP-End-User`** says *which of your customers* you are calling for. It is your own identifier for that person.

The boundary between your customers is the **agent**, not the header. Give each customer their own agent. An agent has its own memory and its own storage, so nothing crosses between two of them. The header works inside one agent, to keep one person's conversations separate from another's.

## Step 1: mint a key

An administrator of your organization does this once, in the Priors application, under **Settings → API keys**. This is not an API call, and there is no endpoint for it. A key reaches your whole organization, so issuing one is an act of administration, and a key that could mint another key would turn one leak into an unbounded supply.

The page shows the new key once:

```
sp_7Kq2mN4pXr8wLz1c_a3f9…
└┬┘ └──────┬──────┘ └─┬─┘
 │         │          └─ the secret. Shown once, stored only as a hash, never recoverable.
 │         └──────────── the key id. Safe to log, and what you quote to support.
 └────────────────────── a fixed prefix, so a leaked key is easy to search for.
```

Put the whole string straight into your secret manager, and send it unchanged as the bearer token. It is not a fragment to join with the key id.

The page also shows an expiry date beside each key. Record it now: a key stops verifying on that date, nothing reaches your backend to say so, and an expired key returns the same `401` as an unknown one. So the first sign of a missed rotation is your own traffic failing.

You can hold more than one key at a time, which is what makes rotation uneventful: mint the new key, deploy it, then revoke the old one from the same page. Revocation takes effect on the very next request, with no cache to wait out.

Your organization can hold 5 live keys. If the page refuses a new one, revoke a key you no longer use.

## Step 2: create one agent for one customer

Do this once per customer.

```bash theme={null}
curl -X POST https://app.sapientpriors.com/api/v1/agents \
  -H "Authorization: Bearer $SP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_config": {
      "role": "Household finance assistant",
      "objective": "Answer questions about this household of accounts, payments and statements."
    },
    "visibility": "external"
  }'
```

```json theme={null}
{
  "agent_id": "agent_01JZ8X9K6AS3V8SGP0Q9A2P4FH",
  "status": "initializing",
  "visibility": "external",
  "access": "owner",
  "expires_at": null
}
```

The agent is owned by your organization rather than by any person, and it appears in no directory of ours. `external` is the only kind your key creates, so you can leave `visibility` out entirely and receive it.

Creation is asynchronous. Poll `GET /v1/agents/{agent_id}` until `status` is `ready` before the first turn.

Store the returned `agent_id` against your own customer record. That mapping is the one piece of state you keep. Priors never needs to know what a customer of yours is.

**If it fails**

* `400 invalid_request`: your request carried a `Sec-Fetch-Site` header, which means the key reached a browser.
* `409 agent_quota_exceeded`: your organization has reached its ceiling of 100 agents. One agent per customer means one hundred customers, and the 101st create is refused rather than queued. If you are heading past roughly eighty customers, contact us then rather than at the wall. Raising it is work on our side and it needs lead time.

## Step 3: open a conversation for one person

```bash theme={null}
curl -X POST https://app.sapientpriors.com/api/v1/agents/agent_01J.../threads \
  -H "Authorization: Bearer $SP_API_KEY" \
  -H "X-SP-End-User: customer-4471-person-7" \
  -H "Content-Type: application/json" \
  -d '{"title": "Mortgage questions", "metadata": {"kind": "mortgage"}}'
```

The thread is recorded against that person. Send the same `X-SP-End-User` value on every later call for them.

Threads carry a free-form `metadata` object, so you can tag one and match on it from the listing later.

**If it fails**

* `400 invalid_request`: the header was sent empty or as whitespace, or it is longer than 255 characters. An empty value is never read as "no header", because "no header" means your whole organization.
* `404 not_found`: the agent is not one this key reaches. Check that it is an `external` agent of your own organization.

## Step 4: ask a question

```bash theme={null}
curl -X POST https://app.sapientpriors.com/api/v1/agents/agent_01J.../threads/thread_01J.../observe-and-respond \
  -H "Authorization: Bearer $SP_API_KEY" \
  -H "X-SP-End-User: customer-4471-person-7" \
  -H "Content-Type: application/json" \
  -d '{
    "observations": [
      {
        "content": [{"type": "text", "text": "When is my next payment due?"}],
        "timestamp": "2026-09-17T12:05:00Z",
        "speaker": "Amma",
        "tags": ["app"]
      }
    ],
    "detail": "brief"
  }'
```

`response_context` in the reply is the finished answer. Do not feed it to a model of your own. That asks a model to answer an answer. If you want material to answer from instead, call `observe-and-enrich`, whose request body is the same without `learn_reply`. `detail: brief` asks for a short answer that fits a phone screen; see [Answer length](/docs/concepts/answering#answer-length-detail).

Learned memory is shared across the agent whether or not you send the header. Only the thread list narrows.

**Send the header on every call for that person**, not only when you create the thread. Without it the key acts as the organization, and the per-subject rate window becomes one window shared by your whole backend.

**Say who is speaking.** Set `speaker` on each observation to the person's name (`"speaker": "Amma"`), not an id. The agent reads it when it reasons about the conversation and answers "who said..." questions from it. In a thread that several people share, leaving it out makes every message look like it came from the same person. Use `tags` for labels such as the channel (`"tags": ["app"]`); with `speaker` set, the agent keeps them with the message. Without `speaker`, the first tag is read as the speaker instead, which is the older behavior.

`speaker` and `X-SP-End-User` do different jobs. `speaker` goes into the memory: it says who said a message. `X-SP-End-User` never reaches the memory: it decides rate limits and which threads a person can open. Send both.

**If it fails**

* `402 payment_required`: your organization is out of credit. See [Credits](/docs/credits).
* `429 rate_limit_exceeded`: wait the number of seconds in `Retry-After`. With `X-SP-End-User`, the per-user window is counted against that person. Without it, every call your backend makes spends from one shared window.

## Step 5: list one person's conversations

There is no endpoint for this, and you do not need one.

```bash theme={null}
curl https://app.sapientpriors.com/api/v1/agents/agent_01J... \
  -H "Authorization: Bearer $SP_API_KEY" \
  -H "X-SP-End-User: customer-4471-person-7"
```

`GET /v1/agents/{agent_id}` returns the agent with a `threads` array, and that array is already filtered for whoever is asking. With the header, it is that person's conversations and nobody else's, each with `thread_id`, `title`, `metadata`, and timestamps. Pick one and post to it, or create a new one. Without the header you are the organization, so you receive every thread on the agent.

This also means you never lose a thread to a crash. If your backend dies after Priors creates a thread but before you commit the id, the thread is still recorded against that person and appears in the very next listing.

## Keep the key on your server

**The key belongs on your backend. It must never reach a browser or a mobile app.** A key in front-end code is in your bundle, and anyone who loads the page can read it.

Two things stop that happening by accident. Priors sends no CORS headers, so a browser blocks the call before it leaves the page. And a request that carries a key and a `Sec-Fetch-Site` header is refused with `400 invalid_request`, because only a browser sets that header.

Priors does not check `Origin` or `Referer`. If your own gateway or HTTP library adds either header, nothing here minds.

## Errors for a key

| Status | What happened |
| - | - |
| `400` | Besides the usual payload problems: `X-SP-End-User` without a key, or empty, or over 255 characters; a key on a request carrying `Sec-Fetch-Site`; `visibility: external` from anything that is not a key. |
| `401` | The key is missing, malformed, unknown, revoked, or expired. All five answer alike on purpose. The key id in your logs is how support tells them apart for you. |
| `402` | Your organization is out of credit. |
| `404` | The agent or thread is not one this credential reaches. It belongs to another organization, or it is not an `external` agent, or the thread was created by a different person. It is never a `403`, so a key cannot be used to discover which ids exist. |
| `429` | A rate window refused the request. Wait the number of seconds in `Retry-After`. |

Every error body carries `code`, `error`, and `request_id`. Branch on `code`, not on the message. Quote `request_id` when you report a problem.

## Generate a client

`https://www.sapientpriors.com/docs/openapi.yaml` is a complete OpenAPI 3.0 document. Any generator produces a typed client in your language, with both headers already wired up.

## What to read next

* [Authentication](/docs/authentication) for the two credentials side by side.
* [Limits](/docs/limits) for every rate window and ceiling.
* [Credits](/docs/credits) for what each call costs.
* [Integration notes](/docs/integration) for retries, timeouts, and versioning.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.