a love letter to tangled (android, iOS, and a search API)
1[
2 {
3 "page": "search",
4 "route": "/search",
5 "method": "GET",
6 "summary": "Unified search across all indexed Tangled documents.",
7 "details": "Delegates to keyword search. The mode parameter is accepted but only \"keyword\" is supported.",
8 "queryParams": [
9 {
10 "name": "q",
11 "type": "string",
12 "required": true,
13 "description": "Search query. Supports boolean operators, phrases, and prefix matching."
14 },
15 {
16 "name": "mode",
17 "type": "string",
18 "required": false,
19 "description": "Search mode. Only \"keyword\" is supported."
20 },
21 { "name": "limit", "type": "int", "required": false, "description": "Results per page (1–100). Default 20." },
22 { "name": "offset", "type": "int", "required": false, "description": "Pagination offset. Default 0." },
23 {
24 "name": "collection",
25 "type": "string",
26 "required": false,
27 "description": "Filter by ATProto collection NSID."
28 },
29 {
30 "name": "type",
31 "type": "string",
32 "required": false,
33 "description": "Filter by record type: repo, issue, pull, profile, string."
34 },
35 { "name": "author", "type": "string", "required": false, "description": "Filter by author handle or DID." },
36 { "name": "repo", "type": "string", "required": false, "description": "Filter by repository name." },
37 { "name": "language", "type": "string", "required": false, "description": "Filter by programming language." },
38 { "name": "state", "type": "string", "required": false, "description": "Filter by state: open, closed, merged." },
39 { "name": "from", "type": "string", "required": false, "description": "Created after (ISO 8601)." },
40 { "name": "to", "type": "string", "required": false, "description": "Created before (ISO 8601)." }
41 ]
42 },
43 {
44 "page": "search",
45 "route": "/search/keyword",
46 "method": "GET",
47 "summary": "PostgreSQL full-text keyword search over indexed Tangled documents.",
48 "details": "Same parameters and response shape as GET /search. Use this route to bypass mode negotiation.",
49 "queryParams": [
50 {
51 "name": "q",
52 "type": "string",
53 "required": true,
54 "description": "Search query. Supports boolean operators, phrases, and prefix matching."
55 },
56 { "name": "limit", "type": "int", "required": false, "description": "Results per page (1–100). Default 20." },
57 { "name": "offset", "type": "int", "required": false, "description": "Pagination offset. Default 0." },
58 {
59 "name": "collection",
60 "type": "string",
61 "required": false,
62 "description": "Filter by ATProto collection NSID."
63 },
64 {
65 "name": "type",
66 "type": "string",
67 "required": false,
68 "description": "Filter by record type: repo, issue, pull, profile, string."
69 },
70 { "name": "author", "type": "string", "required": false, "description": "Filter by author handle or DID." },
71 { "name": "repo", "type": "string", "required": false, "description": "Filter by repository name." },
72 { "name": "language", "type": "string", "required": false, "description": "Filter by programming language." },
73 { "name": "state", "type": "string", "required": false, "description": "Filter by state: open, closed, merged." },
74 { "name": "from", "type": "string", "required": false, "description": "Created after (ISO 8601)." },
75 { "name": "to", "type": "string", "required": false, "description": "Created before (ISO 8601)." }
76 ]
77 },
78 {
79 "page": "documents",
80 "route": "/documents/{id}",
81 "method": "GET",
82 "summary": "Fetch a single indexed document by its stable ID.",
83 "details": "The document ID has the format did|collection|rkey. Deleted documents return 404.",
84 "pathParams": [{ "name": "id", "type": "string", "description": "Stable document ID (did|collection|rkey)." }]
85 },
86 {
87 "page": "actors",
88 "route": "/actors/{handle}",
89 "method": "GET",
90 "summary": "Tangled actor profile with optional Bluesky social data.",
91 "details": "Resolves handle to DID, fetches sh.tangled.actor.profile from the actor's PDS. If the profile record has bluesky:true, Bluesky display name and avatar are also returned.",
92 "pathParams": [{ "name": "handle", "type": "string", "description": "Tangled handle or DID." }]
93 },
94 {
95 "page": "actors",
96 "route": "/actors/{handle}/repos",
97 "method": "GET",
98 "summary": "List all sh.tangled.repo records for an actor.",
99 "pathParams": [{ "name": "handle", "type": "string", "description": "Tangled handle or DID." }]
100 },
101 {
102 "page": "actors",
103 "route": "/actors/{handle}/repos/{repo}",
104 "method": "GET",
105 "summary": "Repo record with resolved knot host and AT URI.",
106 "pathParams": [
107 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
108 { "name": "repo", "type": "string", "description": "Repository name." }
109 ]
110 },
111 {
112 "page": "actors",
113 "route": "/actors/{handle}/repos/{repo}/tree",
114 "method": "GET",
115 "summary": "Directory tree for a ref, proxied from the actor's knot.",
116 "pathParams": [
117 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
118 { "name": "repo", "type": "string", "description": "Repository name." }
119 ],
120 "queryParams": [
121 {
122 "name": "ref",
123 "type": "string",
124 "required": false,
125 "description": "Branch, tag, or commit SHA. Defaults to default branch."
126 },
127 { "name": "path", "type": "string", "required": false, "description": "Subdirectory path within the tree." }
128 ]
129 },
130 {
131 "page": "actors",
132 "route": "/actors/{handle}/repos/{repo}/blob",
133 "method": "GET",
134 "summary": "File contents at a path and ref, proxied from the actor's knot.",
135 "pathParams": [
136 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
137 { "name": "repo", "type": "string", "description": "Repository name." }
138 ],
139 "queryParams": [
140 { "name": "ref", "type": "string", "required": false, "description": "Branch, tag, or commit SHA." },
141 { "name": "path", "type": "string", "required": true, "description": "File path within the repository." }
142 ]
143 },
144 {
145 "page": "actors",
146 "route": "/actors/{handle}/repos/{repo}/log",
147 "method": "GET",
148 "summary": "Commit log for a ref, proxied from the actor's knot (raw bytes).",
149 "pathParams": [
150 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
151 { "name": "repo", "type": "string", "description": "Repository name." }
152 ],
153 "queryParams": [
154 { "name": "ref", "type": "string", "required": false, "description": "Branch, tag, or commit SHA." },
155 {
156 "name": "path",
157 "type": "string",
158 "required": false,
159 "description": "Limit log to commits touching this path."
160 },
161 { "name": "limit", "type": "int", "required": false, "description": "Max commits to return." },
162 {
163 "name": "cursor",
164 "type": "string",
165 "required": false,
166 "description": "Pagination cursor from a previous response."
167 }
168 ]
169 },
170 {
171 "page": "actors",
172 "route": "/actors/{handle}/repos/{repo}/branches",
173 "method": "GET",
174 "summary": "Branch list for a repo, proxied from the actor's knot (raw bytes).",
175 "pathParams": [
176 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
177 { "name": "repo", "type": "string", "description": "Repository name." }
178 ],
179 "queryParams": [
180 { "name": "limit", "type": "int", "required": false, "description": "Max branches to return." },
181 { "name": "cursor", "type": "string", "required": false, "description": "Pagination cursor." }
182 ]
183 },
184 {
185 "page": "actors",
186 "route": "/actors/{handle}/repos/{repo}/default-branch",
187 "method": "GET",
188 "summary": "Default branch name for a repo, proxied from the actor's knot.",
189 "pathParams": [
190 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
191 { "name": "repo", "type": "string", "description": "Repository name." }
192 ]
193 },
194 {
195 "page": "actors",
196 "route": "/actors/{handle}/repos/{repo}/languages",
197 "method": "GET",
198 "summary": "Language breakdown for a repo at a ref, proxied from the actor's knot.",
199 "pathParams": [
200 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
201 { "name": "repo", "type": "string", "description": "Repository name." }
202 ],
203 "queryParams": [
204 { "name": "ref", "type": "string", "required": false, "description": "Branch, tag, or commit SHA." }
205 ]
206 },
207 {
208 "page": "actors",
209 "route": "/actors/{handle}/repos/{repo}/tags",
210 "method": "GET",
211 "summary": "Tag list for a repo, proxied from the actor's knot (raw bytes).",
212 "pathParams": [
213 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
214 { "name": "repo", "type": "string", "description": "Repository name." }
215 ]
216 },
217 {
218 "page": "actors",
219 "route": "/actors/{handle}/repos/{repo}/diff",
220 "method": "GET",
221 "summary": "Diff for a ref, proxied from the actor's knot (raw bytes).",
222 "pathParams": [
223 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
224 { "name": "repo", "type": "string", "description": "Repository name." }
225 ],
226 "queryParams": [
227 { "name": "ref", "type": "string", "required": false, "description": "Commit SHA or branch to diff." }
228 ]
229 },
230 {
231 "page": "actors",
232 "route": "/actors/{handle}/repos/{repo}/compare",
233 "method": "GET",
234 "summary": "Comparison between two refs, proxied from the actor's knot (raw bytes).",
235 "pathParams": [
236 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
237 { "name": "repo", "type": "string", "description": "Repository name." }
238 ],
239 "queryParams": [
240 { "name": "from", "type": "string", "required": true, "description": "Base ref (branch, tag, or SHA)." },
241 { "name": "to", "type": "string", "required": true, "description": "Head ref (branch, tag, or SHA)." }
242 ]
243 },
244 {
245 "page": "actors",
246 "route": "/actors/{handle}/repos/{repo}/issues",
247 "method": "GET",
248 "summary": "Issues scoped to a repo, pre-joined with state (open/closed).",
249 "pathParams": [
250 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
251 { "name": "repo", "type": "string", "description": "Repository name." }
252 ]
253 },
254 {
255 "page": "actors",
256 "route": "/actors/{handle}/repos/{repo}/pulls",
257 "method": "GET",
258 "summary": "Pull requests targeting a repo, pre-joined with status (open/closed/merged).",
259 "pathParams": [
260 { "name": "handle", "type": "string", "description": "Tangled handle or DID." },
261 { "name": "repo", "type": "string", "description": "Repository name." }
262 ]
263 },
264 {
265 "page": "actors",
266 "route": "/actors/{handle}/issues",
267 "method": "GET",
268 "summary": "All issues authored by an actor, pre-joined with state.",
269 "pathParams": [{ "name": "handle", "type": "string", "description": "Tangled handle or DID." }]
270 },
271 {
272 "page": "actors",
273 "route": "/actors/{handle}/pulls",
274 "method": "GET",
275 "summary": "All pull requests authored by an actor, pre-joined with status.",
276 "pathParams": [{ "name": "handle", "type": "string", "description": "Tangled handle or DID." }]
277 },
278 {
279 "page": "actors",
280 "route": "/actors/{handle}/following",
281 "method": "GET",
282 "summary": "sh.tangled.graph.follow records for an actor.",
283 "pathParams": [{ "name": "handle", "type": "string", "description": "Tangled handle or DID." }]
284 },
285 {
286 "page": "actors",
287 "route": "/actors/{handle}/strings",
288 "method": "GET",
289 "summary": "sh.tangled.string records posted by an actor.",
290 "pathParams": [{ "name": "handle", "type": "string", "description": "Tangled handle or DID." }]
291 },
292 {
293 "page": "issues",
294 "route": "/issues/{handle}/{rkey}",
295 "method": "GET",
296 "summary": "Single issue with pre-joined state (open/closed).",
297 "details": "State is derived from sh.tangled.repo.issue.state records. If no state record exists the issue is considered open.",
298 "pathParams": [
299 { "name": "handle", "type": "string", "description": "Author's Tangled handle or DID." },
300 { "name": "rkey", "type": "string", "description": "Record key of the issue." }
301 ]
302 },
303 {
304 "page": "issues",
305 "route": "/issues/{handle}/{rkey}/comments",
306 "method": "GET",
307 "summary": "All comments for a specific issue.",
308 "details": "Filters sh.tangled.repo.issue.comment records from the actor's PDS by matching the issue AT URI.",
309 "pathParams": [
310 { "name": "handle", "type": "string", "description": "Author's Tangled handle or DID." },
311 { "name": "rkey", "type": "string", "description": "Record key of the parent issue." }
312 ]
313 },
314 {
315 "page": "pulls",
316 "route": "/pulls/{handle}/{rkey}",
317 "method": "GET",
318 "summary": "Single pull request with pre-joined status (open/closed/merged).",
319 "details": "Status is derived from sh.tangled.repo.pull.status records. If no status record exists the PR is considered open.",
320 "pathParams": [
321 { "name": "handle", "type": "string", "description": "Author's Tangled handle or DID." },
322 { "name": "rkey", "type": "string", "description": "Record key of the pull request." }
323 ]
324 },
325 {
326 "page": "pulls",
327 "route": "/pulls/{handle}/{rkey}/comments",
328 "method": "GET",
329 "summary": "All comments for a specific pull request.",
330 "details": "Filters sh.tangled.repo.pull.comment records from the actor's PDS by matching the pull AT URI.",
331 "pathParams": [
332 { "name": "handle", "type": "string", "description": "Author's Tangled handle or DID." },
333 { "name": "rkey", "type": "string", "description": "Record key of the parent pull request." }
334 ]
335 },
336 {
337 "page": "identity",
338 "route": "/identity/resolve",
339 "method": "GET",
340 "summary": "Resolve a handle to a DID via bsky.social.",
341 "details": "Proxies com.atproto.identity.resolveHandle. The response shape mirrors the ATProto lexicon.",
342 "queryParams": [
343 { "name": "handle", "type": "string", "required": true, "description": "Tangled or ATProto handle to resolve." }
344 ]
345 },
346 {
347 "page": "identity",
348 "route": "/identity/did/{did}",
349 "method": "GET",
350 "summary": "Fetch a DID document for did:plc or did:web identifiers.",
351 "details": "did:plc resolves via plc.directory. did:web resolves via https://{host}/.well-known/did.json. Other DID methods return 400.",
352 "pathParams": [{ "name": "did", "type": "string", "description": "A did:plc:… or did:web:… identifier." }]
353 },
354 {
355 "page": "activity",
356 "route": "/activity",
357 "method": "GET",
358 "summary": "Paginated list of recent ATProto events from the Jetstream cache.",
359 "details": "Events are cached as the Jetstream firehose is consumed. Only Tangled-relevant collections are retained by default.",
360 "queryParams": [
361 { "name": "limit", "type": "int", "required": false, "description": "Results per page (1–200). Default 50." },
362 { "name": "offset", "type": "int", "required": false, "description": "Pagination offset. Default 0." },
363 {
364 "name": "collection",
365 "type": "string",
366 "required": false,
367 "description": "Filter by ATProto collection NSID."
368 },
369 {
370 "name": "operation",
371 "type": "string",
372 "required": false,
373 "description": "Filter by operation: create, update, delete."
374 },
375 { "name": "did", "type": "string", "required": false, "description": "Filter by actor DID." }
376 ]
377 },
378 {
379 "page": "activity",
380 "route": "/activity/stream",
381 "method": "GET",
382 "summary": "WebSocket proxy to the Bluesky Jetstream firehose.",
383 "details": "Accepts a WebSocket upgrade and forwards all query parameters to jetstream2.us-east.bsky.network. Standard Jetstream params: wantedCollections, wantedDids, cursor."
384 },
385 {
386 "page": "profiles",
387 "route": "/profiles/{did}/summary",
388 "method": "GET",
389 "summary": "Social summary for an actor: follower count via Constellation.",
390 "details": "Best-effort — if Constellation is unavailable follower_count is 0 rather than an error.",
391 "pathParams": [{ "name": "did", "type": "string", "description": "ATProto DID, e.g. did:plc:abc." }]
392 },
393 {
394 "page": "profiles",
395 "route": "/backlinks/count",
396 "method": "GET",
397 "summary": "Constellation backlinks count for any subject/source pair.",
398 "details": "Used by the mobile client for star counts (source: sh.tangled.graph.star) and follower counts (source: sh.tangled.graph.follow).",
399 "queryParams": [
400 { "name": "subject", "type": "string", "required": true, "description": "AT URI or DID being linked to." },
401 {
402 "name": "source",
403 "type": "string",
404 "required": true,
405 "description": "Collection NSID of the backlink records."
406 }
407 ]
408 },
409 {
410 "page": "proxy",
411 "route": "/xrpc/knot/{knotHost}/{nsid}",
412 "method": "GET",
413 "summary": "Proxy a GET request to a Tangled knot's XRPC endpoint.",
414 "details": "Forwards to https://{knotHost}/xrpc/{nsid} with all query parameters. knotHost must not contain slashes or whitespace.",
415 "pathParams": [
416 { "name": "knotHost", "type": "string", "description": "Knot hostname (no scheme), e.g. knot.example." },
417 { "name": "nsid", "type": "string", "description": "XRPC NSID to call on the knot." }
418 ]
419 },
420 {
421 "page": "proxy",
422 "route": "/xrpc/pds/{pds}/{nsid}",
423 "method": "GET",
424 "summary": "Proxy a GET request to an ATProto PDS XRPC endpoint.",
425 "details": "Forwards to https://{pds}/xrpc/{nsid} with all query parameters. pds must not contain slashes or whitespace.",
426 "pathParams": [
427 { "name": "pds", "type": "string", "description": "PDS hostname (no scheme), e.g. bsky.social." },
428 { "name": "nsid", "type": "string", "description": "XRPC NSID to call on the PDS." }
429 ]
430 },
431 {
432 "page": "proxy",
433 "route": "/xrpc/bsky/{nsid}",
434 "method": "GET",
435 "summary": "Proxy a GET request to the Bluesky public API.",
436 "details": "Forwards to https://public.api.bsky.app/xrpc/{nsid}. No authentication is forwarded; only public endpoints are accessible.",
437 "pathParams": [{ "name": "nsid", "type": "string", "description": "XRPC NSID to call on the Bluesky public API." }]
438 },
439 {
440 "page": "health",
441 "route": "/healthz",
442 "method": "GET",
443 "summary": "Liveness probe. Returns 200 if the process is responsive.",
444 "details": "No dependency checks. Safe to call at any frequency."
445 },
446 {
447 "page": "health",
448 "route": "/readyz",
449 "method": "GET",
450 "summary": "Readiness probe. Checks database reachability.",
451 "details": "Checks database reachability."
452 }
453]