> ## 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.

# SocialFi SDK for JavaScript, React and Python

> Official CabalSpy ID SocialFi SDK for JavaScript/TypeScript, React and Python: login with OpenID Connect, Data API client, live streams, token theses and webhook verification.

CabalSpy publishes official, open-source SDKs for CabalSpy ID in TypeScript and Python. The `cabalspy-id` package covers everything: the OpenID Connect login with PKCE and ID token verification, a typed Data API client with pagination helpers and retries, the live streams, token theses, webhook verification and, in TypeScript, the login button and thesis widget for the browser and React.

| Language | Install | Import | Source |
| - | - | - | - |
| TypeScript / JavaScript | `npm install cabalspy-id` | `cabalspy-id`, plus the entry points below | [cabalspy-id-js](https://github.com/CabalSpy/cabalspy-id-js) |
| Python | `pip install cabalspy-id` | `cabalspy_id`, plus the modules below | [cabalspy-id-python](https://github.com/CabalSpy/cabalspy-id-python) |

<Note>
  Current version: `cabalspy-id` 0.5.1 on [npm](https://www.npmjs.com/package/cabalspy-id) and [PyPI](https://pypi.org/project/cabalspy-id/). New releases are published from GitHub.
</Note>

## Packages

Need only part of it? Three smaller packages re-export parts of `cabalspy-id`, with the same classes, behaviour and types. Their versions follow `cabalspy-id`.

| Package | npm | PyPI | Contents |
| - | - | - | - |
| [cabalspy-id](https://github.com/CabalSpy/cabalspy-id-js) | `cabalspy-id` | `cabalspy-id` | Everything. |
| [socialfi-api](https://github.com/CabalSpy/socialfi-api) | `socialfi-api` | `socialfi-api` | The SocialFi API: Data API, Login with CabalSpy ID (OpenID Connect), trade stream, webhooks. |
| [socialfi-thesis](https://github.com/CabalSpy/socialfi-thesis) | `socialfi-thesis` | `socialfi-thesis` | Token theses: Data API thesis methods, `ThesisClient`, `ThesesStream`; React `<CabalSpyTheses>` from `socialfi-thesis/react`. |
| [socialfi-widget](https://github.com/CabalSpy/socialfi-widget) | `socialfi-widget` | – | Browser widgets: login button and thesis widget mount helpers, and React components from `socialfi-widget/react`. |

```bash theme={null}
npm install socialfi-api      # or socialfi-thesis, socialfi-widget
pip install socialfi-api      # or socialfi-thesis
```

```ts theme={null}
import { CabalSpyID } from 'socialfi-api';                    // = cabalspy-id/api
import { CabalSpyTheses } from 'socialfi-thesis/react';        // = cabalspy-id/react
import { mountLoginButton, mountTheses } from 'socialfi-widget'; // = cabalspy-id/widget
```

```python theme={null}
from socialfi_api import CabalSpyID   # = cabalspy_id.api
```

## Entry points

Inside `cabalspy-id`, import only what you use. Classes are shared between entry points.

| TypeScript import | Contents |
| - | - |
| `cabalspy-id` | Everything. |
| `cabalspy-id/api` | `CabalSpyID`, `CabalSpyIDStream`, `CabalSpyIDAuth`, `verifyWebhook`, errors and Data API types. |
| `cabalspy-id/thesis` | `CabalSpyID` (thesis methods), `ThesisClient`, `ThesisLive`, `ThesesStream`, thesis types. |
| `cabalspy-id/widget` | Browser only: `mountLoginButton`, `mountTheses`, `setThesisAccessToken`, `startLogin`, `onWidgetEvents`, `loadCabalSpyIDWidget`. |
| `cabalspy-id/react` (= `cabalspy-id/widget/react`) | `<CabalSpyLogin />`, `<CabalSpyTheses />`, `useCabalSpyID()`. |

| Python module | Contents |
| - | - |
| `cabalspy_id` | Everything. |
| `cabalspy_id.api` | `CabalSpyID`, `AsyncCabalSpyID`, `CabalSpyIDStream`, `CabalSpyIDAuth`, `verify_webhook`, errors and Data API types. |
| `cabalspy_id.thesis` | `CabalSpyID` (thesis methods), `ThesisClient`, `AsyncThesisClient`, `ThesisLive`, `ThesesStream`, thesis types. |

## Overview

The TypeScript SDK has no runtime dependencies and runs on Node 18+, browsers, Deno, Bun and edge runtimes, as ESM and CommonJS. The Python SDK needs Python 3.10+, has sync and async clients, and types every response with `TypedDict`s. The live stream in Python needs one extra: `pip install "cabalspy-id[stream]"`.

| | TypeScript | Python |
| - | - | - |
| Login (OpenID Connect) | `CabalSpyIDAuth` | `CabalSpyIDAuth`, `AsyncCabalSpyIDAuth` |
| Data API and stream | `CabalSpyID` | `CabalSpyID`, `AsyncCabalSpyID` |
| Webhooks | `verifyWebhook`, `signWebhook` | `verify_webhook`, `sign_webhook` |
| React | `CabalSpyLogin`, `useCabalSpyID` from `cabalspy-id/react` | – |

Both read the Data API key from `CABALSPY_ID_API_KEY`, then `CABALSPY_API_KEY`. It is the same key as the CabalSpy API with **CabalSpy ID** enabled, see [Authentication](/cabalspy-id/data-api#authentication).

## Data API

<CodeGroup>
  ```ts TypeScript theme={null}
  import { CabalSpyID } from 'cabalspy-id';

  const csid = new CabalSpyID({ apiKey: process.env.CABALSPY_API_KEY });

  const me = await csid.me(); // instance, plan, rate limit, users with data

  // The person who just signed in: look them up by the sub of their ID token
  const profile = await csid.profile({ csid_id: claims.sub });
  const wallet = profile.wallets.find((w) => w.chains.includes('solana'))!;

  // Full trading profile for a period, same numbers as the CabalSpy API
  const t = await csid.tracker({ blockchain: 'solana', address: wallet.address, period: '7d' });
  console.log(t.period_stats.realized_pnl_usd);

  // Your users ranked by PnL
  const board = await csid.leaderboard({ blockchain: 'solana', period: '7d', sort: 'pnl_usd', limit: 25 });
  ```

  ```python Python theme={null}
  from cabalspy_id import CabalSpyID

  csid = CabalSpyID()  # reads CABALSPY_ID_API_KEY, then CABALSPY_API_KEY

  me = csid.me()  # instance, plan, rate limit, users with data

  # The person who just signed in: look them up by the sub of their ID token
  profile = csid.profile(csid_id=claims["sub"])
  wallet = next(w for w in profile["wallets"] if "solana" in w["chains"])

  # Full trading profile for a period, same numbers as the CabalSpy API
  t = csid.tracker(blockchain="solana", address=wallet["address"], period="7d")
  print(t["period_stats"]["realized_pnl_usd"])

  # Your users ranked by PnL
  board = csid.leaderboard(blockchain="solana", period="7d", sort="pnl_usd", limit=25)
  ```
</CodeGroup>

| Endpoint | TypeScript | Python |
| - | - | - |
| [GET /v1/health](/cabalspy-id/health) | `health()` | `health()` |
| [GET /v1/me](/cabalspy-id/me) | `me()` | `me()` |
| [GET /v1/users](/cabalspy-id/users) | `users({ limit, kol, cursor, scope })` | `users(limit=, kol=, cursor=, scope=)` |
| [GET /v1/profile](/cabalspy-id/profile) | `profile({ username } \| { csid_id } \| { address })` | `profile(username= \| csid_id= \| address=)` |
| [GET /v1/wallets/lookup](/cabalspy-id/wallet-lookup) | `walletLookup(address)` | `wallet_lookup(address)` |
| [GET /v1/wallets/tracker](/cabalspy-id/tracker) | `tracker({ blockchain, address, period })` | `tracker(blockchain=, address=, period=)` |
| [GET /v1/wallets/history](/cabalspy-id/history) | `history({ blockchain, address, limit, cursor })` | `history(blockchain=, address=, limit=, cursor=)` |
| [GET /v1/wallets/holdings](/cabalspy-id/holdings) | `holdings({ blockchain, address })` | `holdings(blockchain=, address=)` |
| [GET /v1/wallets/pnl\_calendar](/cabalspy-id/pnl-calendar) | `pnlCalendar({ blockchain, address })` | `pnl_calendar(blockchain=, address=)` |
| [GET /v1/wallets/leaderboard](/cabalspy-id/leaderboard) | `leaderboard({ blockchain, period, sort, limit, cursor, scope })` | `leaderboard(blockchain=, period=, sort=, limit=, cursor=, scope=)` |
| [GET /v1/transactions/latest](/cabalspy-id/latest) | `latestTransactions({ blockchain, limit })` | `latest_transactions(blockchain=, limit=)` |

If your account has several CabalSpy ID instances, pass `instance` (the client\_id) to the constructor. The Python async client works the same way with `await`:

```python Python async theme={null}
from cabalspy_id import AsyncCabalSpyID

async with AsyncCabalSpyID() as csid:
    users = await csid.users(kol=True)
```

### Pagination

Iterators follow `next_cursor` for you.

<CodeGroup>
  ```ts TypeScript theme={null}
  for await (const user of csid.iterUsers({ kol: true })) console.log(user.username);
  for await (const trade of csid.iterHistory({ blockchain: 'solana', address })) console.log(trade.tx_signature);
  for await (const entry of csid.iterLeaderboard({ blockchain: 'base', period: '30d' })) console.log(entry.rank);
  ```

  ```python Python theme={null}
  for user in csid.iter_users(kol=True):
      print(user["username"])
  for trade in csid.iter_history(blockchain="solana", address=address):
      print(trade["tx_signature"])
  for entry in csid.iter_leaderboard(blockchain="base", period="30d"):
      print(entry["rank"])
  ```
</CodeGroup>

### Errors

Every failure is a subclass of `CabalSpyIDError` with the error `code`, the HTTP status, the request id, a `docs` link to the [error reference](/cabalspy-id/errors) and, for validation errors, `parameter` and `allowed`. `429`, `5xx` and network errors are retried with exponential backoff (2 retries by default), and `Retry-After` is respected.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { NotFoundError, PlanUserLimitError, RateLimitError } from 'cabalspy-id';

  try {
    await csid.profile({ csid_id: claims.sub });
  } catch (err) {
    if (err instanceof PlanUserLimitError) return showUpgradeNotice(); // anon_ sub: no data until you upgrade
    if (err instanceof NotFoundError) return null;                     // not one of your users
    if (err instanceof RateLimitError) console.log(err.retryAfter);
    throw err;
  }
  ```

  ```python Python theme={null}
  from cabalspy_id import NotFoundError, PlanUserLimitError, RateLimitError

  try:
      csid.profile(csid_id=claims["sub"])
  except PlanUserLimitError:
      show_upgrade_notice()          # anon_ sub: no data until you upgrade
  except NotFoundError:
      pass                           # not one of your users
  except RateLimitError as err:
      print(err.retry_after)
  ```
</CodeGroup>

| Class | Status | Codes |
| - | - | - |
| `BadRequestError` | 400 | missing\_parameter, invalid\_parameter, instance\_required |
| `AuthenticationError` | 401 | missing\_api\_key |
| `PermissionError` | 403 | invalid\_api\_key, product\_not\_enabled, no\_instance, scope\_not\_allowed |
| `PlanUserLimitError` | 403 | plan\_user\_limit |
| `NotFoundError` | 404 | profile\_not\_found, wallet\_not\_found, instance\_not\_found |
| `RateLimitError` | 429 | rate\_limit\_exceeded |
| `ServerError` | 5xx | internal\_error, service\_unavailable |
| `ConnectionError` (TS), `APIConnectionError` (Python) | – | Network failure or timeout. |
| `OAuthError` | 4xx | OAuth errors of the login endpoints. |
| `TokenVerificationError` | – | An ID or access token failed verification. |
| `WebhookVerificationError` | – | A webhook signature or timestamp is invalid. |

## Live stream

Every trade of your users the moment it is indexed (Pro plan and up), see [Trades stream](/cabalspy-id/stream). The client reconnects with exponential backoff after a dropped connection or when heartbeats stop. Configuration errors (`invalid_api_key`, `plan_required`, `scope_not_allowed`) are fatal and stop reconnecting.

<CodeGroup>
  ```ts TypeScript theme={null}
  const stream = csid.stream({ blockchain: 'solana' }); // omit blockchain for every chain

  stream.on('trade', (t) => console.log(t.trader?.username, t.transaction_type, t.token_symbol, t.value_usd));
  stream.on('error', (e) => console.error(e.code, e.message));
  await stream.connect(); // resolves once the server confirmed the subscription

  // or
  for await (const trade of csid.stream()) console.log(trade);
  ```

  ```python Python theme={null}
  # async only, needs: pip install "cabalspy-id[stream]"
  from cabalspy_id import AsyncCabalSpyID

  async with AsyncCabalSpyID() as csid:
      stream = csid.stream(blockchain="solana")  # omit blockchain for every chain

      @stream.on("trade")
      def on_trade(t):
          print(t["trader"]["username"], t["transaction_type"], t["token_symbol"], t["value_usd"])

      await stream.connect()        # returns once the server confirmed the subscription
      await stream.run_forever()    # until stream.close() or a fatal error
  ```
</CodeGroup>

Events: `trade`, `subscribed`, `heartbeat`, `open`, `close`, `reconnecting`, `error`. Browsers, Deno, Bun and Node 22+ have a global `WebSocket`. On Node 18 to 21, install `ws` and pass it as `csid.stream({ WebSocketImpl: WebSocket })`.

## Server-side login

Authorization code flow with PKCE S256, state and nonce, see [OpenID Connect](/cabalspy-id/openid-connect). `handleCallback` checks the error, state and issuer, exchanges the code and verifies the ID token and its nonce.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { CabalSpyIDAuth, isLimited } from 'cabalspy-id';

  const auth = new CabalSpyIDAuth({
    clientId: process.env.CSID_CLIENT_ID!,         // csid_...
    clientSecret: process.env.CSID_CLIENT_SECRET,  // csid_sk_... (omit for public clients)
    redirectUri: 'https://your.app/callback',      // registered in the dashboard
  });

  // GET /login
  const login = await auth.createAuthorizationUrl({ scope: ['openid', 'profile', 'wallets', 'offline_access'] });
  // keep login.state, login.nonce and login.codeVerifier (session or signed http-only cookie)
  redirect(login.url);

  // GET /callback
  const { tokens, claims } = await auth.handleCallback(request.url, stored);

  if (isLimited(claims)) {
    // over your plan's user limit: anonymous sub "anon_...", no profile or wallet data
  } else {
    const user = await auth.userinfo(tokens.access_token); // x profile, wallets, onchain
  }
  ```

  ```python Python theme={null}
  import os
  from cabalspy_id import CabalSpyIDAuth, is_limited

  auth = CabalSpyIDAuth(
      "csid_...",                                      # your instance's client_id
      client_secret=os.environ["CSID_CLIENT_SECRET"],  # csid_sk_... (omit for public clients)
      redirect_uri="https://your.app/callback",        # registered in the dashboard
  )

  # GET /login
  req = auth.create_authorization_url(scope=["openid", "profile", "wallets", "offline_access"])
  session["csid"] = req.to_dict()   # state, nonce, code_verifier, redirect_uri
  return redirect(req.url)

  # GET /callback
  result = auth.handle_callback(request.url, **session.pop("csid"))

  if is_limited(result.claims):
      ...  # over your plan's user limit: anonymous sub "anon_...", no profile or wallet data
  else:
      user = auth.userinfo(result.tokens["access_token"])  # x profile, wallets, onchain
  ```
</CodeGroup>

| TypeScript | Python | |
| - | - | - |
| `discover()` | `discover()` | The discovery document (cached). |
| `createAuthorizationUrl({ scope, prompt, redirectUri, state, nonce })` | `create_authorization_url(scope=, prompt=, redirect_uri=, state=, nonce=)` | Authorization URL plus state, nonce and code verifier to store. |
| `handleCallback(url, { state, nonce, codeVerifier })` | `handle_callback(url, state=, nonce=, code_verifier=)` | Full callback handling, returns tokens and verified claims. |
| `exchangeCode({ code, codeVerifier, redirectUri })` | `exchange_code(code=, code_verifier=, redirect_uri=)` | Code exchange at the token endpoint. |
| `refresh(refreshToken)` | `refresh(refresh_token)` | Refresh. Tokens rotate, store the new one. |
| `revoke(token)` | `revoke(token)` | Revoke a refresh token and its chain. |
| `userinfo(accessToken)` | `userinfo(access_token)` | The user info claims. |
| `verifyIdToken(idToken, { nonce, maxAge })` | `verify_id_token(id_token, nonce=, max_age=)` | ES256 signature via JWKS, iss, aud, azp, exp, nonce. |
| `verifyAccessToken(accessToken)` | `verify_access_token(access_token)` | For access tokens your frontend sends to your API. |

`AsyncCabalSpyIDAuth` in Python has the same methods as coroutines.

<Tip>
  **Handling users without X.** Users who signed in with Telegram or Google have `x` set to null in the claims and in Data API profiles, `twitter` set to null, and a handle like `tg.<name>` or `id.<hex>`. Check for `null` before reading X fields and key your users on `sub`. See [Users without X](/cabalspy-id/openid-connect#users-without-x).
</Tip>

## Verify an ID token

Browser apps let the [login button](/cabalspy-id/login-button) run PKCE and receive an `idToken`. Send it to your backend and verify it there before you trust it.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { CabalSpyIDAuth, isLimited } from 'cabalspy-id';

  const auth = new CabalSpyIDAuth({ clientId: 'csid_...' });
  const claims = await auth.verifyIdToken(idToken); // ES256 via JWKS: iss, aud, azp, exp
  if (isLimited(claims)) {
    // anonymous sub, no profile data
  }
  ```

  ```python Python theme={null}
  from cabalspy_id import CabalSpyIDAuth, is_limited

  auth = CabalSpyIDAuth("csid_...")
  claims = auth.verify_id_token(id_token_from_the_browser)
  if is_limited(claims):
      ...  # anonymous sub, no profile data
  ```
</CodeGroup>

## React

`cabalspy-id/react` wraps the [login button](/cabalspy-id/login-button). `<CabalSpyLogin>` loads `widget.js` once and maps its props to the widget's `data-*` attributes.

```tsx React theme={null}
import { CabalSpyLogin, useCabalSpyID } from 'cabalspy-id/react';

export function Header() {
  const { user, limited } = useCabalSpyID({ clientId: 'csid_...' });
  return (
    <CabalSpyLogin
      clientId="csid_..."
      scope={['openid', 'profile', 'wallets']}
      theme="dark"
      shape="pill"
      onLogin={({ idToken }) => fetch('/api/session', { method: 'POST', body: JSON.stringify({ idToken }) })}
      onLogout={() => fetch('/api/session', { method: 'DELETE' })}
    />
  );
}
```

| Prop | Attribute | |
| - | - | - |
| `clientId` | `data-client-id` | Your Instance ID. Required. |
| `theme`, `color`, `textColor`, `shape`, `size`, `label`, `fullWidth`, `showLogo` | `data-theme` … `data-show-logo` | Override the design from the button builder. |
| `href` | `data-href` | Server apps: your login route. |
| `redirectUri`, `scope` | `data-redirect-uri`, `data-scope` | Browser apps: the widget runs PKCE itself. |
| `user`, `logoutHref` | `data-user`, `data-logout-href` | Server apps: show the signed-in person's avatar menu. |
| `onLogin`, `onLogout`, `onError` | `csid:login`, `csid:logout`, `csid:error` | Event callbacks. |

`useCabalSpyID({ clientId })` returns `{ user, idToken, accessToken, limited, ready, login(), signOut() }`.

## Theses

Both SDKs cover [token theses](/cabalspy-id/theses): the Data API endpoints, the [theses stream](/cabalspy-id/theses-stream) (`ThesesStream`, every plan, see the [chart markers cookbook](/thesis-markers-on-price-chart)), thesis events on the general stream, a client for the public browser API and, in TypeScript, the thesis widget. Writing needs the scope `thesis` in your login.

### Data API

Reading works with any key. Writes happen in the name of one of your users, passed as `csid_id`. Writes are retried only after the API's own `rate_limit_exceeded`, never after a server or network error, so a post cannot be duplicated.

<CodeGroup>
  ```ts TypeScript theme={null}
  const { can_post } = await csid.thesisEligibility({ blockchain: 'solana', mint, csid_id: claims.sub });
  if (can_post) {
    const t = await csid.postThesis({ blockchain: 'solana', mint, body: 'Accumulating here.', stance: 'bull', csid_id: claims.sub });
  }
  const list = await csid.listTheses({ blockchain: 'solana', mint, sort: 'new' });
  ```

  ```python Python theme={null}
  ok = csid.thesis_eligibility(blockchain="solana", mint=mint, csid_id=claims["sub"])
  if ok["can_post"]:
      t = csid.post_thesis(blockchain="solana", mint=mint, body="Accumulating here.", stance="bull", csid_id=claims["sub"])
  theses = csid.list_theses(blockchain="solana", mint=mint, sort="new")
  ```
</CodeGroup>

| Endpoint | TypeScript | Python |
| - | - | - |
| [GET /v1/theses](/cabalspy-id/theses-list) | `listTheses()`, `iterTheses()` | `list_theses()`, `iter_theses()` |
| [GET /v1/theses/summary](/cabalspy-id/theses-summary) | `thesisSummary()` | `thesis_summary()` |
| [GET /v1/theses/item](/cabalspy-id/theses-item) | `thesis(id)` | `thesis(id)` |
| [GET /v1/theses/replies](/cabalspy-id/theses-replies) | `thesisReplies()`, `iterThesisReplies()` | `thesis_replies()`, `iter_thesis_replies()` |
| [GET /v1/theses/latest](/cabalspy-id/theses-latest) | `latestTheses()` | `latest_theses()` |
| [GET /v1/theses/user](/cabalspy-id/theses-user) | `userTheses()` | `user_theses()` |
| [GET /v1/theses/eligibility](/cabalspy-id/theses-eligibility) | `thesisEligibility()` | `thesis_eligibility()` |
| [POST /v1/theses](/cabalspy-id/theses-post) | `postThesis()` | `post_thesis()` |
| [POST /v1/theses/replies](/cabalspy-id/theses-reply) | `replyToThesis()` | `reply_to_thesis()` |
| [POST /v1/theses/reactions](/cabalspy-id/theses-reactions) | `reactToThesis()` | `react_to_thesis()` |
| [POST /v1/theses/report](/cabalspy-id/theses-report) | `reportThesis()` | `report_thesis()` |
| [POST /v1/theses/delete](/cabalspy-id/theses-delete) | `deleteThesis()` | `delete_thesis()` |

### Stream

Pass `events` and optionally `mint` to receive `thesis`, `thesis.reply`, `thesis.reaction` and `thesis.removed`, or iterate `stream.theses()`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const stream = csid.stream({ events: ['theses'], blockchain: 'solana', mint });
  stream.on('thesis', (t) => console.log('new thesis', t.id));
  await stream.connect();
  ```

  ```python Python theme={null}
  stream = csid.stream(events=["theses"], blockchain="solana", mint=mint)  # AsyncCabalSpyID

  @stream.on("thesis")
  def on_thesis(t):
      print("new thesis", t["id"])

  await stream.connect()
  ```
</CodeGroup>

### Public browser API: ThesisClient

`ThesisClient` wraps the [browser API](/cabalspy-id/theses-browser-api) on id.cabalspy.xyz: no API key, open reads, writes with the user's access token (scope `thesis`), and `live()` for the public per-token websocket. Python also has `AsyncThesisClient`.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { ThesisClient } from 'cabalspy-id';

  const theses = new ThesisClient({ accessToken }); // from csid:login / useCabalSpyID()
  const { items, token, summary } = await theses.list('solana', mint, { sort: 'top' });
  const me = await theses.me('solana', mint);       // { can_post, can_reply, holder, paused, user }
  if (me.can_post) await theses.post('solana', mint, { body: 'Strong community, still early.', stance: 'bull' });

  const live = theses.live('solana', mint);
  live.on('thesis', prepend).on('reply', addReply).on('reaction', updateCounts).on('removed', ({ id }) => drop(id));
  await live.connect();
  ```

  ```python Python theme={null}
  from cabalspy_id import ThesisClient

  with ThesisClient(access_token=result.tokens["access_token"]) as theses:
      page = theses.list("solana", mint, sort="top")     # items, pagination, token, summary
      if theses.me("solana", mint)["can_post"]:
          theses.post("solana", mint, body="Strong community, still early.", stance="bull")
  ```
</CodeGroup>

Methods: `list`, `iterate`, `summary`, `get`, `replies`, `byUser` / `by_user`, `me`, `post`, `reply`, `react`, `report`, `delete`, `live`, `setAccessToken` / `set_access_token`.

### React widget

`<CabalSpyTheses>` from `cabalspy-id/react` renders the [thesis widget](/cabalspy-id/theses#path-1-the-widget). Pass the signed-in user's access token so they can write right away.

```tsx React theme={null}
import { CabalSpyTheses, useCabalSpyID } from 'cabalspy-id/react';

const { accessToken } = useCabalSpyID({ clientId: 'csid_...' });
<CabalSpyTheses chain="solana" mint={mint} theme="dark" height={520} accessToken={accessToken} />
```

Props: `chain`, `mint`, `theme`, `height`, `radius`, `accessToken`.

## Webhooks

Pass the raw request body exactly as received. Deliveries older than 5 minutes are rejected, any signature of a secret roll is accepted, and the parsed event is returned. Deduplicate on the event `id`. See [Webhooks](/cabalspy-id/webhooks).

<CodeGroup>
  ```ts TypeScript theme={null}
  import { verifyWebhook } from 'cabalspy-id';

  const event = await verifyWebhook({ payload: rawBody, headers: req.headers, secret: process.env.CSID_WEBHOOK_SECRET! });

  switch (event.type) {
    case 'wallet.verified': /* event.data.sub, event.data.address */ break;
    case 'consent.revoked': /* delete the user's data */ break;
  }
  ```

  ```python Python theme={null}
  import os
  from cabalspy_id import verify_webhook

  event = verify_webhook(request.body, request.headers, os.environ["CSID_WEBHOOK_SECRET"])

  if event["type"] == "consent.revoked":
      delete_user_data(event["data"]["sub"])
  ```
</CodeGroup>

`signWebhook()` / `sign_webhook()` build signed test requests for your receiver.

<Warning>
  Keep the API key and the client secret on your server. Never ship them to a browser.
</Warning>

## More

<CardGroup cols={2}>
  <Card title="TypeScript SDK on GitHub" icon="github" href="https://github.com/CabalSpy/cabalspy-id-js">
    Source, runnable examples for login, Data API, stream, webhooks and React.
  </Card>

  <Card title="Python SDK on GitHub" icon="github" href="https://github.com/CabalSpy/cabalspy-id-python">
    Source and runnable examples, sync and async.
  </Card>
</CardGroup>

For the CabalSpy API (KOL, Smart Money and Whale wallets) see the [CabalSpy SDKs](/sdks).


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