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.
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
45 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 frozen 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 frozen 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-g11 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 →