# auth.md, authenticating with DataSpring

DataSpring is a managed data core. Every surface — MCP, the CLI and the REST
API — runs the same handlers against the same tenant, so whichever one you
authenticate on, you get the same permissions and the same answers.

**Access is by invitation.** There is no self-serve signup. An agent cannot
create an account on a user's behalf; an owner or admin at the customer adds
the human in their Forspring account first, and the agent then authenticates
as that human. If you have no
credentials, stop and tell the user to write to hello@dataspring.app.

## What is readable without any credentials

These need no token and are safe for an agent to fetch while deciding whether
DataSpring is relevant:

| URL | What it is |
| --- | --- |
| `/llms.txt` | Short index of the public surfaces |
| `/llms-full.txt` | All public documentation in one file |
| `/openapi.json` | OpenAPI description of the REST API |
| `/.well-known/api-catalog` | RFC 9727 catalog of the APIs above |
| `/.well-known/mcp/server-card.json` | MCP server card |
| `/.well-known/agent-skills/index.json` | Index of the published skills |
| `/api/v1/skill` | The skills as markdown, for reading |
| `/api/skills` | The skills as trees, for installing |
| `/health` | Liveness |

Everything under `/api/` that touches a tenant's data needs a token, including
`/api/dispatch`.

## MCP

Endpoint: `https://dataspring.app/api/mcp/` (streamable HTTP).

Authentication is OAuth 2.1 with PKCE at the Forspring account
(`https://account.forspring.ai`), the one login for every Forspring product.
The protected-resource metadata names it as the only authorization server:

- `/.well-known/oauth-protected-resource/api/mcp` (RFC 9728)
- `/.well-known/oauth-protected-resource/api/mcp/o/<org_id>` (one organization's address)
- the account's own `https://account.forspring.ai/.well-known/oauth-authorization-server` (RFC 8414)

A client registers at the account (a client ID metadata document, or dynamic
registration), the person signs in and approves on the account's consent page,
which names DataSpring, the client and the organization, and the client gets
the account's access token (`fat_...`). DataSpring asks the account about it on
every request, so revoking it at `https://account.forspring.ai/o/<org>/agents`
disconnects the client on its next call.

The plain address names no organization: the account's consent asks for
exactly one, and the connection acts there. `https://dataspring.app/api/mcp/o/<org_id>`
is one organization's address, its own OAuth resource; a credential for
another organization is refused with `org_mismatch`. The older
`https://dataspring.app/api/mcp/?org=<org_id>` is still read. There is no tool
that switches organization.

Connections made before 2026-10-20 through DataSpring's own authorization
server (`/api/mcp/authorize`, consent on DataSpring's page) keep working until
they are removed.

Claude Code: `claude mcp add --transport http --scope user dataspring
https://dataspring.app/api/mcp/`, then `/mcp` → *Authenticate* (or `claude mcp
login dataspring` on a machine with no browser).

Automation with no person present (CI, a headless agent) sends a key to the
same endpoint instead: a key made in the Forspring account's Agents page
(`fpk_...` personal, `fsa_...` service account, at
`https://account.forspring.ai/o/<org>/agents`), or a DataSpring API key
(`ds_...`) made in the console's connect page under "Automation and CI", as
`Authorization: Bearer <key>`. A key belongs to one organization; the plain
address is the key's organization, and an address that names another one is
refused with `org_mismatch`. An unknown, revoked or expired key is HTTP 401.

Refusals lead with a stable reason and end with what fixes it, e.g.
`org_required: This connection names no organization, and you are in
several: ... Fix: Remove this connection's sign-in in your MCP client and
connect again: DataSpring asks which organization when you approve. ...`. The reasons are `not_signed_in`, `org_required`,
`not_a_member`, `product_off`, `org_suspended`, `role_too_low`,
`org_mismatch` and `account_unavailable` (the Forspring account did not
answer; retry, nothing changed).

## Agent registration

The audience for this document is an agent acting for a human who already has
a DataSpring account. There are two different things called registration here,
and only one of them is open:

- **OAuth client registration is open.** An MCP host registers itself at the
  Forspring account, the authorization server the discovery documents above
  name (a client ID metadata document, or dynamic registration at
  `https://account.forspring.ai/oauth/register`). No pre-shared client id, no human in the loop, no
  credential to ask us for.
- **Account registration is closed.** No endpoint lets a caller create a
  DataSpring organization or user, and no agent can provision one: an
  organization exists when the customer switches DataSpring on in their
  Forspring account, and a person when that account gives them a role in it. A human at
  the customer is added in their Forspring account first (people and roles
  live there, not in DataSpring); the agent then authenticates as that human
  and inherits exactly their permissions.

So an agent with no user behind it can register as a client and still get
nowhere, because there is no identity for it to act as. That is deliberate.
The right move at that point is to stop and tell the user to write to
hello@dataspring.app, not to retry the flow.

Credentials obtained through either the MCP flow or `dataspring auth login`
act for one person in one organization. They are not shared between
organizations, and nothing you can do with them exceeds what that person can
do in the dashboard.

## CLI

```bash
curl -fsSL https://dataspring.app/install.sh | sh   # Windows: irm https://dataspring.app/install.ps1 | iex
dataspring auth login   # prints a code and a link: approve on any device signed in to Forspring
dataspring whoami       # confirms the person and organization
```

The installer puts `uv` on the machine if it is missing and installs the CLI as
a uv tool (`uv tool install dataspring-cli`; `pip install dataspring-cli`
works too).

`dataspring auth login` shows a code and a URL; the person approves it at
their Forspring account (`https://account.forspring.ai/device`, on any device;
the machine running the CLI needs no browser) and ticks the organizations the
CLI may act in. The CLI keeps one sign-in per organization, for 30 days, in
`~/.forspring/credentials.json`, shared with the other Forspring CLIs (the
tokens in the OS keychain where there is one), and sends its access token as
the bearer. `ds_…` keys an older CLI stored in `~/.dataspring/credentials.json`
keep working. In CI, set `DATASPRING_API_KEY` to a key made in the console's
connect page ("Automation and CI") or an account `fpk_…`/`fsa_…` key, which
always wins, or store one with `dataspring auth login --key <key>`.
`dataspring auth logout` signs out of the organization named (or the only
one; `--all` for every one): it revokes before it forgets, says a credential
was revoked only when that was confirmed, and otherwise names where to revoke
it by hand and exits non-zero.

Every command names its organization, in this order: `--org <org_id>`,
`DATASPRING_ORG` then `FORSPRING_ORG`, a `.forspring.json` (`{"org":
"<org_id>"}`) in the working directory or a parent, the only organization
signed in to. `DATASPRING_API_KEY`, when set, always wins and acts in its own
organization. Signed in to several and none named is refused with
`org_required` (exit 2), never a prompt; `DATASPRING_API_KEY` for another
organization than the one named is `org_mismatch` (exit 4). `dataspring
whoami --json` reports `org_from`, where the organization came from, and
`credential`, what the command acts with. Exit codes, the same in every
Forspring product: 0 ok, 1 failure (and `account_unavailable`), 2 invalid
input (and `org_required`), 3 not found (`not_a_member`), 4 forbidden
(`not_signed_in`, `product_off`, `org_suspended`, `role_too_low`,
`org_mismatch`, `access_denied`), 5 invalid state (`expired_token`), 7 rate
limited (`slow_down`). `dataspring whoami --json` answers a refusal as
`{"ok": false, "reason", "note", "fix"}`. The CLI is a thin HTTP client over
the same REST routes; it holds no warehouse credentials of its own.

## REST

Send an API key as a bearer token:

```
Authorization: Bearer ds_…
```

A key belongs to one organization and acts with the lesser of its own role
and its creator's current role there; it is worth nothing once its creator
loses access.

against `https://dataspring.app/api/...`. `/openapi.json` describes the routes.

Every request works in one organization, and nothing remembers it between
requests. A key names its own; you may also send `X-DataSpring-Org: <org_id>`,
and a key for another organization than that header is refused
`org_mismatch`. A browser session says only who is signed in: the web console
lives at `https://dataspring.app/o/<org_id>/` and names that organization in
`X-DataSpring-Org` on every request (one with several organizations and no
header is refused `org_required`). Sign-in has no DataSpring page: a
signed-out visit goes straight to the Forspring account and comes back to the
same URL. Sessions end after 7 days unused or 30 days after sign-in.

### Refusals

A refusal is JSON with a stable reason, what happened (`note`), what fixes
it (`fix`), and the organization the request named (`org`), the same shape
in every Forspring product:

```json
{"ok": false, "reason": "not_a_member", "note": "You are not a member of organization acme, or it does not exist.", "fix": "Your organizations: noon. An owner or admin of an organization adds people at https://account.forspring.ai.", "org": "acme"}
```

`product_off` and `org_suspended` add `contacts` (up to three people who can
fix it) and `fix_url` (where, at the Forspring account). Servers before
2026-10-15 called `note` `detail`; read either. The MCP server's 401s carry
the same body (`not_signed_in`) beside the `WWW-Authenticate` challenge an
MCP client starts its sign-in from.

| `reason` | Status | Means |
| --- | --- | --- |
| `not_signed_in` | 401 | No session, or an unknown or revoked key |
| `org_required` | 400 | Several organizations, none named |
| `not_a_member` | 404 | Not in that organization, or it does not exist (the same answer); also a key whose creator left it |
| `product_off` | 403 | In the organization, with DataSpring switched off for you |
| `org_suspended` | 403 | Billing stopped DataSpring for the organization |
| `role_too_low` | 403 | Your role cannot do this |
| `org_mismatch` | 403 | The key is for another organization than `X-DataSpring-Org` |
| `account_unavailable` | 503 | The Forspring account did not answer; retry, nothing changed |

### Versioning and deprecation

Tenant-facing REST routes carry their version as the segment straight after
`/api/`, so today's routes read `v1`. A version, once published, keeps its
shape: we add fields and routes within a
version, and we do not remove or repurpose what is already there.

A breaking change means a new version prefix alongside the old one. When a
version is on its way out, responses from it carry the `Deprecation` and
`Sunset` headers of RFC 9745 and RFC 8594, and the `Sunset` date is at least
six months out. An agent that wants to be safe should read those two headers
and surface them rather than waiting to be broken.

## Read this before querying

`/api/v1/skill/consume/instructions` is the skill that explains how to query
DataSpring correctly — which surface to use, how metrics and dimensions relate,
and the mistakes that produce confident wrong numbers. Fetch it before your
first query. It needs no credentials.
