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

# Browser API

> The public thesis API on id.cabalspy.xyz for client-side apps: public reads, writes with the user's access token (scope thesis) and a public live websocket per token.

The thesis widget runs on a small public API on id.cabalspy.xyz. Client-side apps can use it directly, without a backend and without an API key:

* **Reading** is public and allows any origin (CORS).
* **Writing** uses the signed-in user's CabalSpy ID access token as `Authorization: Bearer <token>`. The token must carry the scope `thesis`, so request it at sign-in, for example with `data-scope="openid profile thesis"` on the [login button](/cabalspy-id/login-button).

For server-to-server writing with your API key, use the [Data API](/cabalspy-id/theses-post) instead.

## Base URL

```text theme={null}
https://id.cabalspy.xyz/thesis/v1
```

## Endpoints

Path parameters: `{chain}` is one of solana, bnb, base, eth, rh, `{mint}` the token address, `{id}` a thesis or reply id.

| Method | Path | Auth | Description |
| - | - | - | - |
| GET | `/tokens/{chain}/{mint}?sort=new\|top&stance=&limit=&cursor=` | – | Theses of a token. The response also carries `token` and `summary`. |
| GET | `/tokens/{chain}/{mint}/summary` | – | Counts and token info. |
| GET | `/tokens/{chain}/{mint}/me` | Bearer | Eligibility of the signed-in user plus their `user` (csid\_id, username, name, image\_url) and `notices` (unread, latest, url). |
| POST | `/tokens/{chain}/{mint}` | Bearer | Post a thesis: `{"body": "...", "stance": "bull"}`. Holders only. |
| GET | `/items/{id}` | – | One thesis or reply. |
| GET | `/items/{id}/replies?limit=&cursor=` | – | Replies, oldest first. |
| POST | `/items/{id}/replies` | Bearer | Reply: `{"body": "..."}`. |
| POST | `/items/{id}/reactions` | Bearer | React: `{"emoji": "like", "on": true}`. |
| POST | `/items/{id}/report` | Bearer | Report: `{"reason": "spam", "note": "..."}`. |
| DELETE | `/items/{id}` | Bearer | Delete your own post. |
| GET | `/users/{csid_id}?limit=&cursor=` | – | Theses of one user. |
| GET | `/me/notifications` | Bearer | The user's moderation notifications, see [Moderation](/cabalspy-id/theses-moderation#browser-api). |
| POST | `/me/notifications/read` | Bearer | Mark all notifications as read. |
| POST | `/me/notifications/{id}/appeal` | Bearer | Appeal a decision: `{"message": "..."}`. |

The theses list, the summary and the websocket show theses of all platforms. Add `scope=instance&client_id=csid_...` to show only the theses written on that instance, see [Reach](/cabalspy-id/theses#reach).

With a Bearer token on read requests, `my_reactions` and `is_mine` are filled for the signed-in user. Thesis objects are the same as in the [Data API](/cabalspy-id/theses-list#response).

```javascript theme={null}
const API = "https://id.cabalspy.xyz/thesis/v1";

// read, no token needed
const list = await (await fetch(`${API}/tokens/solana/${mint}?sort=top`)).json();
console.log(list.summary, list.data);

// write with the access token from the login button (scope thesis)
const res = await fetch(`${API}/tokens/solana/${mint}`, {
  method: "POST",
  headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
  body: JSON.stringify({ body: "Strong community, still early.", stance: "bull" }),
});
const out = await res.json();
if (!out.success) alert(out.error.message);
```

## Responses

Success: `{"success": true, "data": ...}`, plus `pagination` (limit, has\_more, next\_cursor) on lists. Errors: `{"success": false, "error": {"code": "...", "message": "..."}}`. The messages are written for end users.

Access tokens live 15 minutes. When a write returns `invalid_token`, refresh the token or let the user sign in again.

| Status | Code | Meaning |
| - | - | - |
| 401 | sign\_in\_required | No Bearer token was sent. |
| 401 | invalid\_token | Token invalid or expired, access revoked, or account not active. |
| 403 | scope\_missing | The token does not carry the scope thesis. |
| 403 | plan\_user\_limit | The user signed in beyond the app's plan limit (anonymous sub). |
| 403 | sandbox\_user | Sandbox test users cannot write. |
| 403 | not\_a\_holder | Only holders can post a thesis. |
| 403 | posting\_paused | The user is paused from writing. |
| 400 | invalid\_body, duplicate, invalid\_parameter | See [Errors](/cabalspy-id/errors#thesis-errors). |
| 404 | not\_found | The post does not exist, was hidden or deleted. |
| 429 | rate\_limited | Too many requests from the user or from the IP address. |

Requests are also limited per IP address: 240 reads, 30 writes and 120 reactions per minute.

## Live websocket

A public websocket per token pushes new theses, replies, reactions and removals. No key or token is needed.

```text theme={null}
wss://id.cabalspy.xyz/thesis/v1/ws/{chain}/{mint}
wss://id.cabalspy.xyz/thesis/v1/ws/{chain}/{mint}?scope=instance&client_id=csid_...
```

```javascript theme={null}
const ws = new WebSocket(`wss://id.cabalspy.xyz/thesis/v1/ws/solana/${mint}`);
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === "thesis") prependCard(msg.data);
  if (msg.type === "reply") addReply(msg.data);
  if (msg.type === "reaction") updateCounts(msg.data.id, msg.data.reactions);
  if (msg.type === "removed") removeCard(msg.data.id);
};
```

| type | data |
| - | - |
| `thesis` | The new thesis. |
| `reply` | The new reply. |
| `reaction` | `{ id, reactions }` with the new counts. |
| `removed` | `{ id }` of a deleted or hidden post. |
| `heartbeat` | Sent every 30 seconds, no data. |

Every message also carries `chain` and `mint`. An invalid chain or token address closes the connection with code 1008, and each IP address can hold at most 20 connections.


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