Founding keys are opena year free at 6,000 req/hour, for the first 1,000 developersClaim yours
Documentation 29

Buildkite quickstart

CI with real logs in it.

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.

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 organization
curl -H "Authorization: Bearer anything" "https://buildkite.sandboxapis.dev/v2/organizations"

# no token works too
curl "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 pipelines
curl "https://buildkite.sandboxapis.dev/v2/organizations/$ORG/pipelines" | jq -r '.[] | "\(.slug)  \(.repository)"'

# 2. a pipeline's builds, newest first
curl "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 jobs
curl "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 timestamps
curl "$LOG_URL" | jq -r .content

# ... or raw, which is what raw_log_url points at
curl -H "Accept: text/plain" "$LOG_URL"

# ... or just the size, without downloading it
curl -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.

ci.sh
curl "https://buildkite-v2-g11.snap.sandboxapis.dev/v2/organizations/olympus-labs/pipelines"

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 →