Documentation 40

Keys & rate limits · 5 min read

One key. Every provider's own auth slot.

No host here ever requires credentials — a SandboxAPIs key only raises your rate limit. It travels in the auth header your client already sends for that provider, so adoption stays a base-URL swap.

01Get a free key
02Send it in your provider's auth slot
03Read the budget in provider headers

01 / How keys work

Any token is accepted. A recognized one is lifted.

Every sandbox host accepts any credential — that is what makes the drop-in work with the auth code you already have. Recognition is by lookup: when the token in the provider's auth slot is a SandboxAPIs key, the request is metered against that key's tier. Anything else — including your real provider tokens — simply stays anonymous; nothing is ever rejected for bad auth.

Get a free key →

02 / Where the key rides

25 hosts, each in its own dialect.

The key goes exactly where that provider's real credential goes — same header, same scheme — so your client library's own auth option is the only thing you configure. Open the category you're integrating with; each section lists every host in it.

git host4 hosts
ProviderThe key rides ascurl
GitHubAuthorization: token <key> (Bearer works too)-H "Authorization: token $KEY"
GitLabPRIVATE-TOKEN: <key> (Bearer works too)-H "PRIVATE-TOKEN: $KEY"
BitbucketHTTP Basic — key as the app-password half-u "you:$KEY"
Azure DevOpsHTTP Basic — key as the PAT half (Bearer works too)-u ":$KEY"
issue tracker2 hosts
ProviderThe key rides ascurl
JiraHTTP Basic — key as the API-token half-u "you@example.com:$KEY"
LinearAuthorization: <key> — bare, no scheme-H "Authorization: $KEY"
AI telemetry6 hosts
ProviderThe key rides ascurl
Anthropicx-api-key: <key>-H "x-api-key: $KEY"
CursorHTTP Basic — key as the USERNAME, empty password-u "$KEY:"
DevinAuthorization: Bearer <key>-H "Authorization: Bearer $KEY"
OpenAIAuthorization: Bearer <key>-H "Authorization: Bearer $KEY"
Codex AnalyticsAuthorization: Bearer <key>-H "Authorization: Bearer $KEY"
Google Antigravityx-goog-api-key-H "x-goog-api-key: $KEY"
messaging2 hosts
ProviderThe key rides ascurl
SlackAuthorization: Bearer <key> (where the xoxb-/xoxp- token rides)-H "Authorization: Bearer $KEY"
Microsoft TeamsAuthorization: Bearer <key> (where the Entra JWT rides — the header must be present)-H "Authorization: Bearer $KEY"
CI/CD2 hosts
ProviderThe key rides ascurl
CircleCICircle-Token: <key> (Bearer works too)-H "Circle-Token: $KEY"
BuildkiteBearer token-H "Authorization: Bearer $KEY"
observability6 hosts
ProviderThe key rides ascurl
SentryAuthorization: Bearer <key> (what sentry-cli and sentry-mcp send)-H "Authorization: Bearer $KEY"
PagerDutyAuthorization: Token token=<key>-H "Authorization: Token token=$KEY"
StatuspageOAuth token-H "Authorization: OAuth $KEY"
PrometheusAuthorization: Bearer <key> (what a Prometheus behind an authenticating proxy expects)-H "Authorization: Bearer $KEY"
GrafanaAuthorization: Bearer <key> (the slot a Grafana service-account token rides in)-H "Authorization: Bearer $KEY"
DatadogDD-API-KEY-H "DD-API-KEY: $KEY"
CRM/support3 hosts
ProviderThe key rides ascurl
ZendeskHTTP Basic — key as the API-token half-u "you@example.com/token:$KEY"
SalesforceAuthorization: Bearer <key> (where the OAuth access token rides)-H "Authorization: Bearer $KEY"
HubSpotAuthorization: Bearer <key> (where the private-app token rides)-H "Authorization: Bearer $KEY"
request.sh
# GitHub's slot, for example — /rate_limit shows the tier the key resolved to
export KEY=your-sandboxapis-key   # claimed at sandboxapis.dev/login

curl -sD- -o /dev/null -H "Authorization: token $KEY" https://gh.sandboxapis.dev/rate_limit
# x-ratelimit-limit: 600   ← keyed: the free tier, in GitHub's own headers

03 / The tiers

6 rungs published. 4 the limiter enforces today.

The whole ladder is below, priced rungs included, because a limits page that lists only the metered tiers is a limits page you have to leave to find out what comes next. The Enforced column is the honest part: the 4 rungs marked yes are what packages/gateway meters against right now. Solo joined them on 2026-08-31: it is self-serve on pricing, and a subscription writes its rate onto every live key on the account within seconds of the payment. Scale has not — its daily window exists in the limiter, but no key carries that tier yet and those customers are onboarded by hand, so the row stays “not yet” until one is running in production. Pricing has the terms; this page has the mechanics.

TierLimitEnforcedBucket
Anonymous60 req/hourYesPer IP — one bucket shared across all provider hosts
Free key600 req/hourYesPer account, shared across all your keys and all provider hosts
Founding key6,000 req/hourYesPer account, shared across all your keys and all provider hosts
Solo — $5/month6,000 req/hourYesPer account, shared across all your keys and all provider hosts — founding's rate, bought rather than comped
Scale — $99/month250,000 req/dayNot yet50,000 req/hour burst. The daily window is built, but no key carries this tier yet — Scale is onboarded by hand
EnterpriseCustomNot yetScoped per agreement, including workloads that fit neither an hourly nor a daily window

At expiry the key falls back to the free limits of the day — it keeps working, and is never revoked. Anonymous access (60 requests/hour per IP across all sandbox hosts, no key) stays open to everyone, always — the waitlist gates the founding upgrade, not access.

This page is the technical statement of the limits — what they are, how they spend, and how each provider reports them. For what each rung costs, what it includes, and how to get on the list for one that isn't buyable yet, see pricing.

04 / Reading the budget

Provider-conventional headers, provider-shaped 429s.

Each host reports and enforces the budget in its provider's own dialect — GitHub's x-ratelimit-* trio and 403 over-limit body, GitLab's RateLimit-* 429, Linear's GraphQL RATELIMITED error, Anthropic's anthropic-ratelimit-requests-* trio — and providers whose real APIs send no rate-limit headers send none here either. Your client's existing throttling code just works; the quickstarts show each host's exact furniture. Reads also carry an ETag wherever the real provider serves one, and conditional requests with If-None-Match answer 304 Not Modified.

When the budget runs out, the refusal names the rung you are on and the next one up. The status, the body shape and the provider's own headers are unchanged — the sentence lives inside the message field the provider's envelope already has. On an anonymous caller's GitHub 403:

403 · anonymous
{
  "message": "API rate limit exceeded. Sign up for a free API key to raise limits to 600 requests/hour: https://sandboxapis.dev/upgrade?from=anonymous",
  "documentation_url": "https://sandboxapis.dev/upgrade?from=anonymous",
  "status": "403"
}

A key gets its own rung's sentence instead — a free key is told what Solo costs, a Solo key what Scale costs — and the same fact rides two headers so a client can read it without parsing prose: x-sandboxapis-tier names the rung that refused (anonymous, free, founding, solo, scale), and x-sandboxapis-upgrade carries the link, present only when there is a rung to offer. Both appear on over-limit refusals and nowhere else — never on a success, never on a coverage 404, never on a read-only refusal. Two dialects have no message field a real response would carry (Slack's {"ok":false,"error":"ratelimited"} and Teams'), so for those the sentence rides x-sandboxapis-reason, as it always has.

05 / Simulating failures

5 failures you can ask any host for, by name.

The base-URL swap gives you the success path in full. It gives you nothing at all on the path that actually pages someone — what your retry loop does when GitHub answers 403 with x-ratelimit-remaining: 0, what your scheduler does when Jira answers 429, whether your HTTP client really honours its own timeout when Slack goes slow. So you can ask for those, on the request, by name: send X-SandboxAPIs-Simulate and that host answers the failure instead of the data.

The grammar is the fault, then optional parameters: X-SandboxAPIs-Simulate: <fault>[; <param>=<value>…]. Nothing is invented — a simulated refusal comes out of the same code that produces a real one, so the status, the body and the provider's own headers are what your client will really meet in production.

FaultParametersWhat the host answers
rate-limit—That host's real over-limit response, from the same code path a genuine refusal uses — GitHub's 403 with x-ratelimit-* at zero, Slack's 429 with Retry-After, Jira's ErrorCollection 429, Linear's GraphQL RATELIMITED.
server-errorstatus=500|502|503|504 (default 500)That provider's own 5xx envelope — the body its API really returns when it fails, not a generic one.
timeoutafter=<ms> (default 30s, max 60s), then=rate-limit|server-errorHolds the connection open for `after`, then answers normally — or, with `then`, answers that fault instead. This is how you find out whether your client's own deadline is really set.
latencyms=<n> (default 2s, max 10s)Delays the real response by `ms` and then serves it, unchanged.
read-onlyverb=POST|PUT|PATCH|DELETE (default PUT)That host's read-only write refusal, on your GET — so you can see the shape your code has to handle without sending a write.
terminal
export KEY=your-sandboxapis-key

# GitHub's real over-limit response — 403, x-ratelimit-* at zero, Retry-After
curl -sD- -o/dev/null -H "Authorization: token $KEY" \
  -H "X-SandboxAPIs-Simulate: rate-limit" https://gh.sandboxapis.dev/repos/olympus-labs/parthenon

# The same wall in Slack's dialect — 429 + Retry-After, {"ok":false,"error":"ratelimited"},
# and no rate-limit header family at all, because the real API sends none
curl -sD- -H "Authorization: Bearer $KEY" \
  -H "X-SandboxAPIs-Simulate: rate-limit" https://slack.sandboxapis.dev/api/conversations.list

# An outage, at the status you want to handle
curl -s -H "Authorization: token $KEY" \
  -H "X-SandboxAPIs-Simulate: server-error; status=503" https://gh.sandboxapis.dev/repos/olympus-labs/parthenon

# 10s late — does your client's timeout actually fire?
curl -s -H "Authorization: Bearer $KEY" \
  -H "X-SandboxAPIs-Simulate: latency; ms=10000" https://slack.sandboxapis.dev/api/conversations.list

# Hold the connection 30s, then refuse — the pair most retry loops get wrong
curl -s -H "Authorization: token $KEY" \
  -H "X-SandboxAPIs-Simulate: timeout; after=30000; then=rate-limit" https://gh.sandboxapis.dev/repos/olympus-labs/parthenon

# See the read-only refusal without sending a write
curl -s -H "Authorization: token $KEY" \
  -H "X-SandboxAPIs-Simulate: read-only" https://gh.sandboxapis.dev/repos/olympus-labs/parthenon

Every faulted response carries X-SandboxAPIs-Simulated with the fault you asked for, written out in full (timeout; after=30000; then=rate-limit), so your logs separate a failure you asked for from one you did not. A simulated refusal costs no budget and is recorded in no usage counter — it serves no data, so it is not usage. The two delay-only faults are the exception and for the obvious reason: latency and a bare timeout serve the real response, so they are metered like the request they are. Out-of-range parameters are refused rather than quietly clamped (ms is 1…10000, after is 1…60000), because being silently given 60s when you asked for 120s is a green test for a deadline nothing exercised. Pinned snapshot hosts accept the header too; a pin without it answers the same bytes it always has.

06 / Scratch writes

One key can write issues here. They last a day and nobody else sees them.

Everything else on these hosts is read-only, and stays read-only: the data set is one shared world and no request ever changes it for anybody. Scratch writes are the one exception, and they are bounded on purpose. Turn them on for a single key in your dashboard, and that key can create an issue, edit its title or body, close or reopen it, and comment on it — on the GitHub host, with your own client, with no code changes.

Three endpoints, and GitHub's own answers. POST /repos/{owner}/{repo}/issues answers 201 with the issue object and a Location header; PATCH /repos/{owner}/{repo}/issues/{n} answers 200 with the updated issue — on an issue you created or on one that was already here; POST /repos/{owner}/{repo}/issues/{n}/comments answers 201 with the comment. Every other write on every other endpoint is refused exactly as it always was, and so are these three for a key you have not turned this on for.

Your reads then include them. The issues listing, the single issue, the comment thread, the repository's open-issue count and the GraphQL issues connection all show your writes back to you — merged before pagination, so totals and Link headers are right and a client walking to the last page sees what it should.

And now Jira comments and transitions. The same key can POST /rest/api/3/issue/{key}/comment, which answers 201 with Jira's own comment object, and POST /rest/api/3/issue/{key}/transitions, which answers 204 — on issues that already exist, with the transitions the workflow allows from where the issue is for you. Your comment then appears in the thread, in the inline comment field and in the comment total; your transition moves the issue's status, statusCategory, resolutiondate and updated, and GET …/transitions offers the next moves from the new state. The body is stored as its text, so formatting is not kept; a comment over 32,767 characters gets Jira's own 400, and the caps below answer Jira's 413. JQL search and the changelog do not include your writes yet and say so with X-SandboxAPIs-Scratch: unmerged. Jira scratch creates no issues.

And Slack messages. The same key can call chat.postMessage with a channel that already exists here, by id or by name, and get Slack's own answer: ok, the channel, the minted ts and the complete message object. Pass thread_ts to reply in a thread, to a message that was already here or to one you posted. Your message then appears in conversations.history, your reply in conversations.replies with the parent's reply_count, reply_users and latest_reply moved to include it, and both are merged before the cursor pager, so has_more and next_cursor stay right. Blocks are stored as sent and echoed back on the message. Text over 40,000 characters is Slack's own msg_too_long; the caps below answer Slack's 429 with a Retry-After that says when your oldest row expires. search.messages does not include your messages yet and says so with X-SandboxAPIs-Scratch: unmerged. Slack scratch creates no channels.

What the headers say. Every response to a scratch-enabled key carries X-SandboxAPIs-Scratch: applied; issues=2; comments=1 when your rows shaped the answer, none when you have nothing in scope, unmerged on the endpoints above, and limit when a cap refused a write. A response your writes changed is also marked private, no-store, so “create it, then list it” is never answered out of your own client's cache. A caller without a scratch key sees none of these headers at all.

The caps. A free key holds 50 rows at a time, a paid key 200 — a row being one issue, one edit, one comment, one transition or one message, and each one dropping off after its day. There is a limit of 50 per repository, per project or per channel, and a body over 65,536 characters is refused, which is GitHub's own limit. Over any of them you get the provider's own refusal — GitHub's 422 for the body and its secondary-rate-limit 403 for the rest, Jira's 413, Slack's 429 — so your client's existing error handling meets a shape it knows.

Pinned hosts too. Writes work on a pinned host, and they stay on that host: an issue you create against gh-2026-03.snap.sandboxapis.dev is not on gh.sandboxapis.dev. A pin called without a scratch key answers exactly the bytes it always has.

07 / MCP

One env var lifts the agent too.

The MCP server is a thin client over the same public hosts, so it shares the same tiers. Set SANDBOXAPIS_API_KEY and every request it makes carries your key — in each provider's own auth slot, automatically. Without it the server runs anonymously at 60 req/hour, and an over-limit refusal to that server says so in the one way its operator can act on: “This MCP server is running without a key (60 req/hour). Set SANDBOXAPIS_API_KEY to a free key for 600/hour and opt-in scratch writes” — the same anonymous rung, the same link, addressed to the reader who can fix it. Agents get the same fact as data: a refused tool result carries upgrade, and orient states the budget before the first call.

terminal
claude mcp add sandboxapis --env SANDBOXAPIS_API_KEY=your-key -- npx -y @sandboxapis/mcp