API docs
The Loophound API for studios.
For indie-studio engineers wiring Loophound into CI: every endpoint below is one Loophound key away — sent as X-Loophound-Key: <api-key> or as a Bearer header. The page splits the public surface into pushes, sweeps, and tickets — each block carries method, path, required headers, payload, and a curl recipe to copy.
Authentication
One API key in a header. Studios wire it once.
For indie-studio engineers wiring Loophound into CI, every endpoint below expects a Loophound key on the request — present it once per runner, then forget about auth for the rest of the build pipeline. The canonical future surface is X-Loophound-Key: <api-key> (or Authorization: Bearer <api-key>). Today the live router also accepts the signed-in studio's better-auth session as Authorization: Bearer <session-token>, so a CI runner following these docs keeps working without waiting for the API-key program to ship.
Every request is sent against https://app.loophound.gg. Treat the host as the only base URL — there is no per-region endpoint and no per-environment override.
Auth at a glance
curl https://app.loophound.gg/api/builds \
-H "Authorization: Bearer <api-key>" \
-H "X-Loophound-Key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"buildUrl": "https://cdn.example.com/frostmarch-pc.zip",
"platform": "pc"
}'Pushes
Push a build, list the ones you have already sent.
Pushes are the only write path into Loophound — register a new build for monitoring and read back the catalog your studio has pushed so far. The list endpoint pairs with GET /api/sweeps/[id] to fetch the checks and tickets for any given build.
/api/buildsRegister a new build with Loophound for monitoring. The server records the row against your studio and returns the freshly minted BuildItem plus the deterministic sweep id you can poll for results.
Bearer <api-key> · X-Loophound-Key: <api-key> · requireAuth()Required headers
- Authorizationrequired
- Bearer <token>Canonical future form is X-Loophound-Key: <api-key>; today the live router also accepts the signed-in studio’s better-auth session as a Bearer token, so a CI runner following these docs keeps using Authorization: Bearer <session-token> until the API-key program ships.
- Content-Typerequired
- application/jsonRequired whenever the request carries a JSON body.
Request body
- buildUrlrequired
- string (URL)Publicly hosted URL to the build artifact (https://…). Max 2048 chars.
- platformrequired
- 'pc' | 'mobile' | 'web'Target platform for this build.
- notes
- stringFree-form context for the monitoring run. Max 2000 chars.
Response body
- itemrequired
- BuildItemThe persisted build record.
- idrequired
- stringCUID for the build row.
- buildUrlrequired
- string (URL)Echoed from the request.
- platformrequired
- 'pc' | 'mobile' | 'web'Echoed from the request.
- notes
- stringEchoed from the request when present.
- queuedAtrequired
- ISO 8601 datetimeWhen the server persisted the build.
- sweepIdrequired
- stringIdentifier for the monitoring sweep that picked up this build.
curl
curl -X POST https://app.loophound.gg/api/builds \
-H "Authorization: Bearer <api-key>" \
-H "X-Loophound-Key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"buildUrl": "https://cdn.example.com/frostmarch-pc.zip",
"platform": "pc",
"notes": "Frostmarch chapter test nightly"
}'Request body
{
"buildUrl": "https://cdn.example.com/frostmarch-pc.zip",
"platform": "pc",
"notes": "Frostmarch chapter test nightly"
}Response
{
"item": {
"id": "clx1abc2de3fghij4klm5nop6",
"buildUrl": "https://cdn.example.com/frostmarch-pc.zip",
"platform": "pc",
"notes": "Frostmarch chapter test nightly",
"queuedAt": "2026-08-09T08:15:42.000Z"
},
"sweepId": "smp-cluster-1"
}/api/buildsList every build the signed-in studio has pushed, newest first. Pair each entry with GET /api/sweeps/[id] to read the checks and tickets the agents surfaced for that build.
Bearer <api-key> · X-Loophound-Key: <api-key> · requireAuth()Required headers
- Authorizationrequired
- Bearer <token>Canonical future form is X-Loophound-Key: <api-key>; today the live router also accepts the signed-in studio’s better-auth session as a Bearer token, so a CI runner following these docs keeps using Authorization: Bearer <session-token> until the API-key program ships.
Response body
- itemsrequired
- BuildItem[]Every build the signed-in studio has pushed, newest first.
- idrequired
- stringCUID for the build row.
- buildUrlrequired
- string (URL)Publicly hosted URL to the build artifact.
- platformrequired
- 'pc' | 'mobile' | 'web'Target platform; matches the value POSTed.
- notes
- stringFree-form context for the monitoring run; omitted when not set on POST.
- queuedAtrequired
- ISO 8601 datetimeWhen the server persisted the build.
curl
curl https://app.loophound.gg/api/builds \
-H "Authorization: Bearer <api-key>"Response
{
"items": [
{
"id": "clx1abc2de3fghij4klm5nop6",
"buildUrl": "https://cdn.example.com/frostmarch-pc.zip",
"platform": "pc",
"notes": "Frostmarch chapter test nightly",
"queuedAt": "2026-08-09T08:15:42.000Z"
},
{
"id": "clx7yz8w9v0qrstu1opq2rst",
"buildUrl": "https://cdn.example.com/frostmarch-mobile.zip",
"platform": "mobile",
"queuedAt": "2026-08-08T11:42:01.000Z"
}
]
}Sweeps
Poll a sweep by id — sweep ↔ build is 1:1, so that is the list-by-build reference too.
The agents run a sweep against every pushed build. Each sweep id covers exactly one build, so reading a sweep by id doubles as reading the full state of the build that drove it — including the ticket set the agents surfaced from that build.
/api/sweeps/[id]Poll a sweep’s results — keep calling until overall resolves from "running" into pass / fail / partial, then read checks and the ticket refs for the build that drove the sweep.
Bearer <api-key> · X-Loophound-Key: <api-key> · requireAuth()Required headers
- Authorizationrequired
- Bearer <token>Canonical future form is X-Loophound-Key: <api-key>; today the live router also accepts the signed-in studio’s better-auth session as a Bearer token, so a CI runner following these docs keeps using Authorization: Bearer <session-token> until the API-key program ships.
Response body
- idrequired
- stringSweep id (also returned by POST /api/builds as sweepId).
- buildIdrequired
- stringBuild row this sweep monitored.
- buildNamerequired
- stringHuman-readable label set by the build-intake form.
- buildUrl
- string (URL)Public URL to the build artifact. Omitted for sample-fixture sweeps that have no recorded build.
- platformrequired
- stringHuman-readable label (e.g. "PC", "Web").
- platformValuerequired
- 'pc' | 'mobile' | 'web'Zod-enum raw value — preferred for routing and analytics.
- engine
- 'unity' | 'unreal' | 'godot' | 'other'Engine declared on the running sweep. Omitted when not set on ingest.
- startedAtrequired
- ISO 8601 datetime
- finishedAtrequired
- ISO 8601 datetimeWhen monitoring reached its terminal state.
- durationSecondsrequired
- integer (>=0)Wall-clock time from startedAt to finishedAt.
- overallrequired
- 'pass' | 'fail' | 'partial'Terminal sweep outcome — keep polling until this is no longer "running".
- checksrequired
- SweepCheck[]Individual probes the agents completed for this sweep.
- idrequired
- stringStable check id within the sweep.
- namerequired
- stringHuman-readable probe name.
- grouprequired
- stringProbe group (lifecycle, multiplayer, gameplay, persistence, perf, input…).
- statusrequired
- 'pass' | 'warn' | 'fail'Outcome of this probe.
- detailrequired
- stringOne-line summary of the run.
- ticketsrequired
- SweepTicketRef[]Tickets the agents surfaced from this sweep. Sweep ↔ build is 1:1, so this array is the list-by-build reference — resolve each id via GET /api/flagged-tickets/[id] for full repro steps.
- idrequired
- stringTicket id — case-insensitive on GET.
- titlerequired
- stringShort human summary.
- severityrequired
- 'low' | 'medium' | 'high' | 'critical'
curl
curl https://app.loophound.gg/api/sweeps/smp-cluster-1 \
-H "Authorization: Bearer <api-key>"Response
{
"id": "smp-cluster-1",
"buildId": "bld_8f2a",
"buildName": "Combat tutorial — storm weather",
"buildUrl": "https://cdn.example.com/frostmarch-pc.zip",
"platform": "PC",
"platformValue": "pc",
"engine": "unity",
"startedAt": "2026-08-09T15:00:00.000Z",
"finishedAt": "2026-08-09T15:14:32.000Z",
"durationSeconds": 872,
"overall": "fail",
"checks": [
{
"id": "c1",
"name": "Boot in under 8s",
"group": "lifecycle",
"status": "pass",
"detail": "6.4s median"
},
{
"id": "c2",
"name": "Lobby join",
"group": "multiplayer",
"status": "pass",
"detail": "0 retries across 30 sessions"
},
{
"id": "c3",
"name": "Match start",
"group": "multiplayer",
"status": "fail",
"detail": "WebSocket drops at t+12s on EastCoast shard"
},
{
"id": "c4",
"name": "Respawn flow",
"group": "gameplay",
"status": "fail",
"detail": "NullReferenceException in Player.Respawn"
}
],
"tickets": [
{
"id": "lh-00422",
"title": "WebSocket drops three seconds after joining EastCoast lobby",
"severity": "critical"
},
{
"id": "lh-00428",
"title": "Inventory item vanishes after fast-travel during a storm",
"severity": "high"
}
]
}Tickets
Open a ticket, fetch its full payload, mark progress on it.
Tickets are the unit of work Loophound hands back to your team. Open one off a sweep, fetch the full reproduction details on demand, and ride the status-update path to track the thread your team is working through.
/api/sweeps/[id]/ticketsOPEN — open a new flagged ticket off a sweep. The path id selects the sweep; the server derives title, severity, signature, environment, and steps from the sweep’s persisted checks. Returns the new ticket id so the caller can navigate to the full payload in one round-trip.
Bearer <api-key> · X-Loophound-Key: <api-key> · requireAuth()Required headers
- Authorizationrequired
- Bearer <token>Canonical future form is X-Loophound-Key: <api-key>; today the live router also accepts the signed-in studio’s better-auth session as a Bearer token, so a CI runner following these docs keeps using Authorization: Bearer <session-token> until the API-key program ships.
Request body
- (no body)
- —No request body — the path id selects the sweep and the server derives title, severity, signature, environment, and steps from the sweep’s persisted checks.
Response body
- ticketIdrequired
- stringThe new flagged ticket id. Fetch via GET /api/flagged-tickets/{ticketId} for the full payload.
curl
curl -X POST https://app.loophound.gg/api/sweeps/smp-cluster-1/tickets \
-H "Authorization: Bearer <api-key>" \
-H "X-Loophound-Key: <api-key>"Response
{
"ticketId": "lh-00501"
}/api/flagged-tickets/[id]GET — fetch one ticket’s full payload. Same shape as a single item in any list endpoint — useful when ingesting via webhook so you can cache the full reproduction steps without a second round-trip.
Bearer <api-key> · X-Loophound-Key: <api-key> · requireAuth()Required headers
- Authorizationrequired
- Bearer <token>Canonical future form is X-Loophound-Key: <api-key>; today the live router also accepts the signed-in studio’s better-auth session as a Bearer token, so a CI runner following these docs keeps using Authorization: Bearer <session-token> until the API-key program ships.
Response body
- idrequired
- stringTicket id. The path segment is case-insensitive.
- titlerequired
- stringShort human summary.
- severityrequired
- 'low' | 'medium' | 'high' | 'critical'Agent-confidence triage.
- signaturerequired
- stringCrash signature or error class — the join key for cluster detection.
- environmentrequired
- EnvironmentThe platform / build combination the ticket was reproduced under.
- platformrequired
- 'steam-deck' | 'windows' | 'macos' | 'switch' | 'linux'
- buildHashrequired
- stringBuild revision.
- buildChannelrequired
- 'internal' | 'playtest' | 'release-candidate'
- stepsrequired
- ReproStep[]Ordered reproduction steps. Same shape as in any list endpoint — referenced by webhook integrations.
- orderrequired
- integer (>0)
- textrequired
- stringStep instruction.
- reportedAtrequired
- ISO 8601 datetime
- statusrequired
- 'open' | 'triaged' | 'in_progress' | 'resolved' | 'closed'Current ticket state.
- updatedAtrequired
- ISO 8601 datetimeWhen status last changed.
curl
curl https://app.loophound.gg/api/flagged-tickets/lh-00428 \
-H "Authorization: Bearer <api-key>"Response
{
"id": "lh-00428",
"title": "Inventory item vanishes after fast-travel during a storm",
"severity": "high",
"signature": "NullReferenceException on Player.Respawn",
"environment": {
"platform": "steam-deck",
"buildHash": "a3f12c0",
"buildChannel": "internal"
},
"steps": [
{
"order": 1,
"text": "Start a new session and load the post-storm save at the Frostmarch waystone."
},
{
"order": 2,
"text": "Pick up the Frostglass Amulet from the cartographer NPC and stash it in the top inventory slot."
},
{
"order": 3,
"text": "Open the map and fast-travel to the eastern ridge while a Lightning Storm weather event is active."
},
{
"order": 4,
"text": "Open the inventory at the destination. The Frostglass Amulet is missing from the slot — no log, no salvage prompt."
}
],
"reportedAt": "2026-08-08T18:22:11.000Z",
"status": "open",
"updatedAt": "2026-08-08T18:22:11.000Z",
"sweepId": null
}/api/flagged-tickets/[id]COMMENT — drive a ticket’s status through the lifecycle. Today the comment surface rides this status-update path, so a team marks progress (in_progress → triaged → resolved, or back to closed) on a ticket the same way they would leave a comment thread in their tracker.
Bearer <api-key> · X-Loophound-Key: <api-key> · requireAuth()Required headers
- Authorizationrequired
- Bearer <token>Canonical future form is X-Loophound-Key: <api-key>; today the live router also accepts the signed-in studio’s better-auth session as a Bearer token, so a CI runner following these docs keeps using Authorization: Bearer <session-token> until the API-key program ships.
- Content-Typerequired
- application/jsonRequired whenever the request carries a JSON body.
Request body
- statusrequired
- 'open' | 'triaged' | 'in_progress' | 'resolved' | 'closed'New status for the ticket. Today the comment surface rides this status-update path — your team marks progress on a thread by transitioning in_progress → triaged → resolved (or absorbing it back to closed).
Response body
- idrequired
- stringTicket id. The path segment is case-insensitive.
- titlerequired
- stringShort human summary.
- severityrequired
- 'low' | 'medium' | 'high' | 'critical'Agent-confidence triage.
- signaturerequired
- stringCrash signature or error class — the join key for cluster detection.
- environmentrequired
- EnvironmentThe platform / build combination the ticket was reproduced under.
- platformrequired
- 'steam-deck' | 'windows' | 'macos' | 'switch' | 'linux'
- buildHashrequired
- stringBuild revision.
- buildChannelrequired
- 'internal' | 'playtest' | 'release-candidate'
- stepsrequired
- ReproStep[]Ordered reproduction steps. Same shape as in any list endpoint — referenced by webhook integrations.
- orderrequired
- integer (>0)
- textrequired
- stringStep instruction.
- reportedAtrequired
- ISO 8601 datetime
- statusrequired
- 'open' | 'triaged' | 'in_progress' | 'resolved' | 'closed'Current ticket state.
- updatedAtrequired
- ISO 8601 datetimeWhen status last changed.
curl
curl -X PATCH https://app.loophound.gg/api/flagged-tickets/lh-00428 \
-H "Authorization: Bearer <api-key>" \
-H "X-Loophound-Key: <api-key>" \
-H "Content-Type: application/json" \
-d '{
"status": "in_progress"
}'Request body
{
"status": "in_progress"
}Response
{
"id": "lh-00428",
"title": "Inventory item vanishes after fast-travel during a storm",
"severity": "high",
"signature": "NullReferenceException on Player.Respawn",
"environment": {
"platform": "steam-deck",
"buildHash": "a3f12c0",
"buildChannel": "internal"
},
"steps": [
{
"order": 1,
"text": "Start a new session and load the post-storm save at the Frostmarch waystone."
},
{
"order": 2,
"text": "Pick up the Frostglass Amulet from the cartographer NPC and stash it in the top inventory slot."
},
{
"order": 3,
"text": "Open the map and fast-travel to the eastern ridge while a Lightning Storm weather event is active."
},
{
"order": 4,
"text": "Open the inventory at the destination. The Frostglass Amulet is missing from the slot — no log, no salvage prompt."
}
],
"reportedAt": "2026-08-08T18:22:11.000Z",
"status": "in_progress",
"updatedAt": "2026-08-09T20:11:03.000Z",
"sweepId": null
}End-to-end example
Push a build, read the sweep, mark progress on the ticket.
Three calls in sequence: register a build, poll the sweep the agents ran against it, then transition the picked ticket through in_progress so your team can see who is on it. Together they cover the whole monitoring loop.
- 1
Push the build
POST /api/builds with the artifact URL and the platform it is targeting. The response carries the new BuildItem id and the sweepId you will poll next.
Step 1 — curl
curl -X POST https://app.loophound.gg/api/builds \ -H "Authorization: Bearer <api-key>" \ -H "X-Loophound-Key: <api-key>" \ -H "Content-Type: application/json" \ -d '{ "buildUrl": "https://cdn.example.com/frostmarch-pc.zip", "platform": "pc", "notes": "Frostmarch chapter test nightly" }' - 2
Poll the sweep
GET /api/sweeps/{sweepId} with the id from step 1. Read checks, then read the tickets array — each entry is the same set CI consumers see when they treat the sweep as the list-by-build reference.
Step 2 — curl
curl https://app.loophound.gg/api/sweeps/smp-cluster-1 \ -H "Authorization: Bearer <api-key>" - 3
Mark the ticket in progress
PATCH /api/flagged-tickets/{id} with status=in_progress so the thread's progress is visible from CI in place of dropping a tracker comment. The id is case-insensitive.
Step 3 — curl
curl -X PATCH https://app.loophound.gg/api/flagged-tickets/lh-00428 \ -H "Authorization: Bearer <api-key>" \ -H "X-Loophound-Key: <api-key>" \ -H "Content-Type: application/json" \ -d '{ "status": "in_progress" }'
Not on Loophound yet
Not onboarded yet? Join the waitlist.
Drop your studio name, contact email, engine, and shipped platforms — we reach out as soon as monitoring for your stack opens.