# Tangled API Twisted reads public Tangled data from Bobbin, Tangled's read-only XRPC AppView. The hosted service is: ```text https://api.tangled.org ``` Every XRPC method lives at `/xrpc/{nsid}` and uses `GET`. Bobbin does not require authentication. The hosted service allows cross-origin `GET`, `HEAD`, and `OPTIONS` requests, so Twisted can call it from a browser or Capacitor WebView. This document reflects Bobbin and `@atcute/tangled` as checked on 2026-08-01. Bobbin is new and its contracts may change. Keep API calls behind one project module so an upstream change does not leak into views. ## Sources of truth Use these sources in this order: 1. [`@atcute/tangled` lexicons](https://github.com/mary-ext/atcute/tree/trunk/packages/definitions/tangled/lexicons/sh/tangled) define request parameters and response types. The package was at version `2.0.13` and atcute commit [`5474d2d`](https://github.com/mary-ext/atcute/commit/5474d2d9d502c4f52c8c13162fac8c2cd8613ec6) during this review. 2. [Bobbin's router](https://tangled.org/tangled.org/core/blob/master/bobbin/crates/xrpc/src/lib.rs) determines which lexicons the service implements. 3. [Tangled's Bobbin documentation](https://docs.tangled.org/bobbin) explains runtime behavior, warm-up, and upstream failures. The [announcement](https://blog.tangled.org/bobbin/) gives the architectural background. The definition package contains Tangled queries and procedures that Bobbin does not serve. A generated atcute type proves that a lexicon exists; it does not prove that `api.tangled.org` implements it. In particular, Twisted must not call the `sh.tangled.ci.*`, `sh.tangled.sync.*`, or `sh.tangled.git.temp.*` methods through Bobbin. ## Typed client Use `@atcute/client` for XRPC requests and register `@atcute/tangled` for its ambient query declarations: ```ts import { Client, simpleFetchHandler } from '@atcute/client' import type {} from '@atcute/microcosm' import type {} from '@atcute/tangled' export const tangled = new Client({ handler: simpleFetchHandler({ service: 'https://api.tangled.org' }) }) const response = await tangled.get('sh.tangled.search.query', { params: { q: 'tangled', limit: 20 } }) if (!response.ok) { throw new Error(response.data.message ?? response.data.error) } const { hits, cursor } = response.data ``` The equivalent project-wide registration is `/// ` in a declaration file. Do not copy generated interfaces into Twisted. Import a named record type from `@atcute/tangled` when a view or mapper needs one. Bobbin's record views embed `value` as `unknown`. Narrow it with the generated record schema before a view uses it: ```ts import { is } from '@atcute/lexicons' import { ShTangledRepo } from '@atcute/tangled' if (!is(ShTangledRepo.mainSchema, response.data.value)) { throw new Error('Bobbin returned an invalid repository record') } const repo: ShTangledRepo.Main = response.data.value ``` Twisted uses `Client.call` so atcute also validates query parameters and the published response envelope. Search is the one compatibility exception: Bobbin returns a floating-point `score`, while the generated AT Protocol `unknown` validator accepts object values. Twisted validates that envelope in its API boundary and still requires a generated record schema for every search hit before returning it to a feature. `@atcute/tangled` does not yet define `sh.tangled.bobbin.getCoverage`. Keep the following local exception beside the API client until upstream publishes that lexicon: ```ts export interface BobbinCoverage { ready: boolean eventsProcessed: number lastCursor: number } ``` Bobbin also exposes `com.bad-example.identity.resolveMiniDoc` for resolving a handle or DID before constructing a Tangled profile AT-URI. Register `@atcute/microcosm` to type this method; do not create another local identity type. ## Identifiers Bobbin uses three identifiers that should remain distinct in application code: - An actor DID, such as `did:plc:boltless`, identifies an account. - A repo DID identifies a git repository as minted by its knot. It is not the repository record's author DID. - A record AT-URI includes the author DID, collection, and record key, such as `at://did:plc:boltless/sh.tangled.repo/squid`. Single-record queries require a complete AT-URI. A handle or bare DID will not work in `actor.getProfile`, `repo.getRepo`, `repo.getIssue`, or `repo.getPull`. Git proxy calls also receive the repository record AT-URI from Twisted. Bobbin resolves the record and forwards the matching repository to its knot. ## Common response shapes Single-record methods return a record view: ```ts interface RecordView { uri: string cid?: string value: T } ``` Batch lookups return `{ items: RecordView[] }` and accept at most 50 AT-URIs. List methods return `{ items: RecordView[]; cursor?: string }`. Pass the returned opaque cursor unchanged to fetch the next page. Most lists accept: | Parameter | Meaning | | --------- | ----------------------------------------------------------- | | `subject` | Required DID, AT-URI, or service identifier described below | | `limit` | `1..1000`; defaults to `50` | | `order` | `asc` or `desc` by `createdAt`; defaults to `desc` | | `cursor` | Opaque cursor from the previous response | Count methods return `{ count: number; distinctAuthors: number }`. Counts and lists can be lower bounds while Bobbin warms its in-memory index. An endpoint ending in `By` reverses the lookup. For example, `listComments?subject={issue-uri}` lists comments on an issue, while `listCommentsBy?subject={actor-did}` lists comments written by an actor. ## Record lookups | NSID | Required parameter | Result | | ---------------------------------- | --------------------------------------- | ---------------- | | `sh.tangled.actor.getProfile` | `actor`: profile record AT-URI | One profile | | `sh.tangled.actor.getProfiles` | `actors`: up to 50 profile AT-URIs | Profiles | | `sh.tangled.repo.getRepo` | `repo`: repo record AT-URI | One repository | | `sh.tangled.repo.getRepos` | `repos`: up to 50 repo record AT-URIs | Repositories | | `sh.tangled.repo.getRepoByRepoDid` | `repoDid`: repo DID | One repository | | `sh.tangled.repo.getIssue` | `issue`: issue record AT-URI | One issue | | `sh.tangled.repo.getIssues` | `issues`: up to 50 issue record AT-URIs | Issues | | `sh.tangled.repo.getPull` | `pull`: pull record AT-URI | One pull request | | `sh.tangled.repo.getPulls` | `pulls`: up to 50 pull record AT-URIs | Pull requests | Issue and pull views include Bobbin's derived current state or status. Use those fields for list badges and detail headers rather than deriving the latest value from a separately fetched history. ## Indexed lists and counts Each row names a record family, the `subject` required by its non-`By` methods, and its implemented methods. Add `By` to each listed `list*` or `count*` method when an actor-DID reverse lookup is available. | Family | Non-`By` subject | Methods | Actor reverse lookup | | ----------------- | ----------------------------- | --------------------------------------------------- | -------------------- | | Repositories | owner DID | `repo.listRepos`, `repo.countRepos` | No | | Issues | repo DID | `repo.listIssues`, `repo.countIssues` | Yes | | Pull requests | repo DID | `repo.listPulls`, `repo.countPulls` | Yes | | Comments | issue, pull, or string AT-URI | `feed.listComments`, `feed.countComments` | Yes | | Reactions | target record AT-URI | `feed.listReactions`, `feed.countReactions` | Yes | | Stars | repo DID | `feed.listStars`, `feed.countStars` | Yes | | Ref updates | repo DID | `git.listRefUpdates`, `git.countRefUpdates` | Yes | | Followers | followed actor DID | `graph.listFollows`, `graph.countFollows` | Yes | | Vouches | vouched-for actor DID | `graph.listVouches`, `graph.countVouches` | Yes | | Collaborators | repo DID | `repo.listCollaborators`, `repo.countCollaborators` | Yes | | Issue states | issue AT-URI | `repo.issue.listStates`, `repo.issue.countStates` | Yes | | Pull statuses | pull AT-URI | `repo.pull.listStatuses`, `repo.pull.countStatuses` | Yes | | Artifacts | repo or release identifier | `repo.listArtifacts`, `repo.countArtifacts` | Yes | | Pipelines | repo or spindle identifier | `pipeline.listPipelines`, `pipeline.countPipelines` | Yes | | Pipeline statuses | pipeline AT-URI | `pipeline.listStatuses`, `pipeline.countStatuses` | Yes | | Knots | owner DID | `knot.listKnots`, `knot.countKnots` | No | | Knot members | knot identifier | `knot.listMembers`, `knot.countMembers` | Yes | | Spindles | owner DID | `spindle.listSpindles`, `spindle.countSpindles` | No | | Spindle members | spindle identifier | `spindle.listMembers`, `spindle.countMembers` | Yes | | Public keys | owner DID | `publicKey.listKeys`, `publicKey.countKeys` | No | | Label definitions | scope identifier | `label.listDefinitions`, `label.countDefinitions` | No | | Label operations | scope identifier | `label.listOps`, `label.countOps` | Yes | | Strings | scope identifier | `string.listStrings`, `string.countStrings` | No | `repo.listIssues` accepts optional `author` and `state` filters. `repo.listPulls` accepts optional `author` and `status` filters. Their `By` counterparts accept the state or status filter but already use the subject as the author. ## Search `sh.tangled.search.query` searches Bobbin's in-memory full-text index. | Parameter | Requirement | | ---------------- | ------------------------------------------------------------- | | `q` | Required non-empty search text | | `limit` | Optional `1..1000`, default `50` | | `cursor` | Optional opaque pagination cursor | | `nsid` | Optional record-collection filter | | `author` | Optional author DID | | `repo` | Optional repo DID | | `since`, `until` | Optional RFC 3339 timestamps; `since` must not exceed `until` | The response is `{ hits, cursor? }`. Each hit contains `uri`, optional `cid`, `nsid`, numeric relevance `score`, and the embedded `value`. Results are ordered by descending relevance. ## Git data proxied to knots Bobbin resolves the repository record, finds its knot, and streams these calls from that knot. It does not cache the response. For Twisted, `repo` is the repository record AT-URI. | NSID | Required parameters | Optional parameters | | ---------------------------------- | ---------------------- | ----------------------------------------- | | `sh.tangled.repo.getDefaultBranch` | `repo` | None | | `sh.tangled.repo.branch` | `repo`, `name` | None | | `sh.tangled.repo.branches` | `repo` | `cursor`, `limit` (`1..100`) | | `sh.tangled.repo.tag` | `repo`, `tag` | None | | `sh.tangled.repo.tags` | `repo` | `cursor`, `limit` (`1..100`) | | `sh.tangled.repo.tree` | `repo`, `ref` | `path` | | `sh.tangled.repo.blob` | `repo`, `ref`, `path` | `raw` | | `sh.tangled.repo.log` | `repo`, `ref` | `path`, `cursor`, `limit` (`1..100`) | | `sh.tangled.repo.diff` | `repo`, `ref` | None | | `sh.tangled.repo.compare` | `repo`, `rev1`, `rev2` | None | | `sh.tangled.repo.languages` | `repo` | `ref` (defaults to `HEAD`) | | `sh.tangled.repo.archive` | `repo`, `ref` | `format` (defaults to `tar.gz`), `prefix` | Several git endpoints declare `*/*` output. Call atcute with `as: 'blob'`, `as: 'bytes'`, or `as: 'stream'` instead of relying on JSON parsing. Bobbin forwards range and cache validators and preserves response metadata including `Content-Type`, `ETag`, `Last-Modified`, and `Content-Range`. The current knot responses use numeric offsets for branch and tag pages and a numeric page cursor for commit history. Twisted keeps those details inside its API boundary and exposes the next cursor as an opaque string to the views. Bobbin also proxies `sh.tangled.repo.listSecrets`, but Twisted has no user-facing need for secret configuration and must not call it. Bobbin has a compatibility route for `sh.tangled.repo.describeRepo`, but its Bobbin parameter differs from the current atcute lexicon. Twisted must use the typed record lookup methods instead. The service-level `sh.tangled.owner`, `sh.tangled.knot.version`, and `sh.tangled.knot.listKeys` methods require an extra `knot={host}` parameter when called through Bobbin. They are useful for diagnostics rather than the first client release. ## Coverage and stale data Call `sh.tangled.bobbin.getCoverage` without parameters: ```json { "ready": true, "eventsProcessed": 137863, "lastCursor": 162959 } ``` `ready: false` means Bobbin is still rebuilding its index. Single-record lookups can remain accurate because Bobbin asks Slingshot for them, but indexed lists, counts, and search may be incomplete. Twisted should show a non-blocking "data is still loading" notice and retain any returned results. Coverage only describes Hydrant ingestion. It does not report whether a knot is reachable or whether Bobbin's cached knot membership is current. ## Public smoke-test fixture The project owner has approved their public Tangled data as a smoke-test and development fixture: | Field | Value | | ------------------- | --------------------------------------------------------------------- | | Handle | `desertthunder.dev` | | Actor DID | `did:plc:xg2vq45muivyy3xwatcehspu` | | Profile AT-URI | `at://did:plc:xg2vq45muivyy3xwatcehspu/sh.tangled.actor.profile/self` | | Twisted repo AT-URI | `at://did:plc:xg2vq45muivyy3xwatcehspu/sh.tangled.repo/3mho6hukiei22` | | Twisted repo DID | `did:plc:4iw5fospv2asv3344au236ka` | Smoke tests may assert stable identity and record fields such as handle, DID, AT-URI, collection, and `$type`. Do not pin CIDs, result counts, descriptions, branch names, issue lists, or other mutable content. Run live tests in sequence with a short delay because the hosted edge applies a pre-authentication rate limit. ## Errors XRPC errors use this JSON shape: ```ts interface XrpcErrorBody { error: string message?: string } ``` Handle these classes in the client boundary: | Status | Typical meaning | UI behavior | | ----------- | ------------------------------------------------------------------------ | ------------------------------------------------------ | | `400` | Missing, malformed, or wrong-shaped identifier; invalid filter or cursor | Show a specific invalid-link or invalid-search message | | `404` | Record or route was not found | Show the resource not-found state | | `429` | Hosted edge rate limit | Honor `Retry-After` when present and offer retry | | `502` | Slingshot or knot failed, disappeared, or returned invalid data | Show an upstream-unavailable state with retry | | `503` | Bobbin shed the request under memory pressure | Offer retry; keep cached UI data | | Other `5xx` | Bobbin failure | Show a generic service error and preserve navigation | Treat aborts as canceled navigation, not user-visible failures. Never retry `400` or `404` automatically. Use bounded retries with backoff for `429`, `502`, and `503`; honor `Retry-After` when present.