# StellarFirm API endpoints

> Every public StellarFirm API route with its method, what it needs, and what it answers. The same list is in the OpenAPI 3.1 file at /openapi.json.

The routes below are the whole public StellarFirm API. Each one is also in the [OpenAPI file](https://stellarfirm.ai/openapi.json), with its parameters and answer schemas. A route answers `405` with an `Allow` header for any method not listed here.

## At a glance

| Method and path | Sign-in | What it does |
| --- | --- | --- |
| `GET /api/health` | None | Says the web app is up. |
| `GET /api/site-banner` | None | Reads the notice at the top of the site. |
| `GET /api/entitlement` | Session | Says whether the account may run. |
| `GET /api/credits/balance` | Session | Reads the account's credits. |
| `POST /api/email/unsubscribe` | Signed token | Stops the later onboarding emails. |
| `GET /api/try/status` | None | Reads the free trial's status for the visitor. |
| `POST /api/try/prompt` | None | Sends one free prompt and streams the assistant's work. |

## GET /api/health

Answers `200` with `ok: true` while the web app is running. Use it for an uptime check.

```json
{ "ok": true, "service": "...", "surface": "ui", "agent": "not-hosted-here" }
```

## GET /api/site-banner

The notice shown at the top of the site, or `null` when there is none. It is the same for everyone and may be cached for half a minute.

| Field | Type | Meaning |
| --- | --- | --- |
| `banner` | object or null | The notice, or `null`. |
| `banner.kind` | `promo` or `warning` | A `promo` shows on the public site only. A `warning` shows everywhere. |
| `banner.message` | string | The notice text. |

## GET /api/entitlement

Says whether the signed-in account may run: an active trial, a licence, or free Hobby after the trial. Only a `200` with `ok: true` means the account may run. It is never cached.

| Status | `status` field | Meaning |
| --- | --- | --- |
| 200 | `licensed`, `trial` or `hobby` | The account may run. Ask again within `recheckAfterSeconds`. |
| 401 | `signed-out` | No signed-in session. |
| 403 | `invalid` | The account may not run. |
| 503 | `unconfigured` or `unavailable` | The account could not be checked. Try again shortly. |

A `200` answer for an account that has the premium models also carries `premiumModels: true` and `codingSubscription: true`. The premium models are open to an account on its trial, with a licence, in a university, or on a paid plan. An account on the free plan after its trial gets neither field, and a client reads a missing field as no. The free plan keeps running on the open models.

A `200` answer also carries `assistantLimit`, how many named assistants the account's plan lets it hire, and `teamLimit`, how many teams of assistants it lets it have. A missing `teamLimit` means the plan puts no limit on teams, so a client must never read it as zero. Neither limit removes anything: an account that already has more keeps all of it and can still rename, move and delete, and only a new assistant or team is held back.

Every answer except `200` also carries the standard `error` object. See [Errors](/docs/api/errors).

## GET /api/credits/balance

The account's balance as a number of StellarFirm credits, never money. Answers `{ "credits": 120 }` for example. It answers `503` with the code `balance_unavailable` when the balance cannot be read.

## POST /api/email/unsubscribe

One-click unsubscribe from the later onboarding emails. Send the `token` from the link in the email as the query parameter. It answers `200` with a plain text line, `400` with `invalid_token` for a token that is not valid, and `502` with `upstream_failed` when the later emails are still scheduled and you should try again.

## GET /api/try/status

The free trial on the landing page gives a visitor with no account a few free prompts with three assistants: the Coder, Marketing and the Product owner. This route says whether the trial is open and how many prompts the visitor has left. The visitor is told apart by network address, with no sign-in and no cookie. It counts nothing and is never cached. It answers `404` with the code `not_found` while the trial is switched off.

```json
{ "open": true, "promptsLeft": 3, "promptsPerVisitor": 3 }
```

| Field | Type | Meaning |
| --- | --- | --- |
| `open` | boolean | False once this month's allowance for the trial is used up. |
| `promptsLeft` | integer | Free prompts this visitor has left. |
| `promptsPerVisitor` | integer | Free prompts each visitor gets. |
| `reopensOn` | date | The day the trial opens again (UTC, the first day of the next month). Present only while it is closed. |

## POST /api/try/prompt

Sends one free prompt. The body is JSON, and the request must come from a person in a browser on the site: a script is refused with `try_refused`.

```json
{ "assistant": "coder", "content": "Make a one page site for a bakery." }
```

| Field | Type | Meaning |
| --- | --- | --- |
| `assistant` | `coder`, `marketing` or `product-owner` | Which assistant gets the prompt. |
| `content` | string | The prompt, 1 to 2000 characters once trimmed. |

A visitor may run one prompt at a time. The assistants work on their own computers, so a prompt can take several minutes, and it stops after about twelve. A step that would need an Approve never runs: the answer stops there and says so.

Send `Accept: text/event-stream` to read the work as it happens. Each event is one JSON object on a `data:` line, and a comment line now and then keeps the connection open.

| Event `type` | Fields | Meaning |
| --- | --- | --- |
| `started` | `assistant`, `promptsLeft` | The prompt was accepted and counted. |
| `activity` | `text` | A short line saying what the assistant is doing. |
| `text` | `delta` | More of the answer's words. |
| `artifact` | `artifact` | A file the assistant published: `id`, `kind`, `title`, and `content` for a kind the page draws (`html`, `svg`, `markdown`, `csv`, `json`) when it is small enough. Other kinds are listed with no content. |
| `done` | `answer`, `promptsLeft`, `held` | The whole answer as Markdown. `held` is true when it stopped at a step that needs an Approve. |
| `error` | `code`, `promptsLeft` | The prompt could not be answered. The code is `try_failed`; `promptsLeft` says how many are left. |

Without that header, the final `done` event comes back as JSON, with the `artifacts` in one list.

A prompt that is refused before it starts answers the [shared error shape](/docs/api/errors) with a status and code: `429` `try_used_up` when the visitor used every free prompt, `429` `try_busy` while a prompt is running or too many run at once, `503` `try_ended` when this month's trial is used up (with `reopensOn`), `403` `try_refused` when the check that a person sent it fails, and `404` `not_found` while the trial is switched off.

## Unknown routes

Any other address under `/api` answers `404` with the error code `not_found`, in JSON, never an HTML page.

## Related

- [Authentication](/docs/api/authentication)
- [Example requests](/docs/api/examples)
- [Errors](/docs/api/errors)

---

Source: https://stellarfirm.ai/docs/api/endpoints
