Tangled API #
Twisted reads public Tangled data from Bobbin, Tangled's read-only XRPC AppView. The hosted service is:
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:
@atcute/tangledlexicons define request parameters and response types. The package was at version2.0.13and atcute commit5474d2dduring this review.- Bobbin's router determines which lexicons the service implements.
- Tangled's Bobbin documentation explains runtime behavior, warm-up, and upstream failures. The announcement 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:
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
/// <reference types="@atcute/tangled" /> 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:
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:
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:
interface RecordView<T> {
uri: string
cid?: string
value: T
}
Batch lookups return { items: RecordView<T>[] } and accept at most 50 AT-URIs.
List methods return { items: RecordView<T>[]; 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:
{ "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:
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.