A Buildkite REST v2-compatible host for the simulated org. Point go-buildkite, pybuildkite or the bk CLI at it with a base-URL swap and walk the pipeline: builds at real commit SHAs, the jobs each one ran, and each job's actual log output — the runner banner, the checkout, the compile, the failing diagnostic naming a real file at a real line, and the exit code. The commits those builds built are browsable on the git hosts, and the same runs are served by the Bitbucket and Azure DevOps hosts in their own dialects.
Both files list exactly what this host serves — generated from our own coverage manifest, not from Buildkite's published specification — and carry no vendor schema: paths, methods and what each one answers, with the base URL and auth header already set. One folder holds a write, so the read-only refusal is something you can press.
Try it — a real request to buildkite.sandboxapis.dev
No key needed. Press Enter to send.
01Point your client at buildkite.
02Walk a build to its jobs
03Read a job's log
01 / Base URL + auth
One origin, any credential.
Requests go to https://buildkite.sandboxapis.dev. Paths are Buildkite's own — /v2/…, exactly as on api.buildkite.com — so a client that already takes a base URL only changes the origin. Auth is Authorization: Bearer …; any value passes, and so does none, so there is no trial organization to create and no API token to mint.
request.sh
# the organizationcurl -H "Authorization: Bearer anything" "https://buildkite.sandboxapis.dev/v2/organizations"# no token works toocurl "https://buildkite.sandboxapis.dev/v2/organizations/olympus-labs/pipelines"
Using a key? Keys & rate limits → — a SandboxAPIs key rides the same Bearer slot and lifts the anonymous limit.
02 / How this shape is checked
Docs-derived — the weaker guarantee, stated.
03 / The walk
Organization → pipeline → build → job → log.
Every step follows a link the previous response handed you. One Buildkite pipeline is one repository CI workflow, so the org has a pipeline per repo per workflow, and each names the repository it builds.
walk.sh
ORG=olympus-labs# 1. the pipelinescurl "https://buildkite.sandboxapis.dev/v2/organizations/$ORG/pipelines" | jq -r '.[] | "\(.slug) \(.repository)"'# 2. a pipeline's builds, newest firstcurl "https://buildkite.sandboxapis.dev/v2/organizations/$ORG/pipelines/parthenon-ci/builds?per_page=5" \| jq -r '.[] | "#\(.number) \(.state) \(.commit[0:7]) \(.message)"'# 3. one build, with its jobscurl "https://buildkite.sandboxapis.dev/v2/organizations/$ORG/pipelines/parthenon-ci/builds/1" \| jq '{number, state, commit, jobs: [.jobs[] | {name, state, exit_status, log_url}]}'# 4. the log — JSON, with per-line timestampscurl "$LOG_URL" | jq -r .content# ... or raw, which is what raw_log_url points atcurl -H "Accept: text/plain" "$LOG_URL"# ... or just the size, without downloading itcurl -I -X HEAD "$LOG_URL"
04 / What is not here
47 of 78 read endpoints, and the rest say why.
Filters behave the same way. The documented build filters this canon can answer — branch, commit, state, creator, created_from, created_to, finished_from — really filter. The ones it cannot, like meta_data, answer a 400 that names the parameter, rather than quietly returning an unfiltered page that looks like an answer.
05 / Pin it for CI
A pinned universe, for tests that must not drift.
The live host rolls forward. For CI, point at the pinned host instead — same paths, same client, an origin that answers the same bytes forever.
The generation suffix on a pin name is the universe generation it was cut on, and for this host it is load-bearing rather than incidental. Job logs arrived with generation 8, so a -g8 pin is the oldest one on which the log endpoint answers at all; artifact CONTENT arrived with hello-15, so it answers on buildkite-v2-g12 and on no pin older than that — an earlier one serves the pipeline, build, job and log walk in full and answers the 404 that names the generation for the artifact rows, permanently, because a pin's bytes never move. How pinned snapshots work →