Sandbox
Build and test your integration without spending credits.
What sandbox is for
Wiring up an API takes a few passes. You get the auth header wrong, then the request body, then you discover your JSON parser chokes on a field you did not expect. None of that is worth spending credits on, and none of it needs real data.
A sandbox key returns canned results from the same endpoints, in the same shape, at no cost. When your integration works, you swap the key for a live one and nothing else changes.
Getting a sandbox key
Create one from your API Keys page in the dashboard: give it a name and turn on Sandbox key. Organizations created from now on also get one automatically, labelled Sandbox, sitting alongside the live key.
Sandbox keys are prefixed sk_test_, live keys sk_live_:
Authorization: Bearer sk_test_YOUR_SANDBOX_KEYSandbox results are synthetic
This matters more than anything else on this page.
The text is lorem ipsum, the figures are generated, and every DOI sits inside the reserved 10.0000/ test namespace. Nothing you get back is a real record, and none of it should be stored as though it were.
Every result carries two markers so your own code can tell:
metadata.syntheticistrueidis prefixedsandbox-
If your pipeline writes search results to a database, assert on one of those before the write. A fabricated DOI that reaches your production store is very hard to find again.
Making a sandbox request
Identical to a live request. Only the key changes.
cURL
curl -X POST "https://api.redpine.ai/api/v1/search/query" \
-H "Authorization: Bearer sk_test_YOUR_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{"collection": "my-collection", "query": "your search query"}'Python
import requests
response = requests.post(
"https://api.redpine.ai/api/v1/search/query",
headers={"Authorization": "Bearer sk_test_YOUR_SANDBOX_KEY"},
json={"collection": "my-collection", "query": "your search query"},
)
data = response.json()
assert response.headers["X-Billing-Mode"] == "sandbox"
for result in data["results"]:
assert result["metadata"]["synthetic"] is True
print(result["id"], result["text"][:60])TypeScript
const response = await fetch("https://api.redpine.ai/api/v1/search/query", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_YOUR_SANDBOX_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
collection: "my-collection",
query: "your search query",
}),
});
console.log(response.headers.get("X-Billing-Mode")); // "sandbox"
const data = await response.json();
for (const result of data.results) {
console.log(result.id, result.metadata.synthetic);
}Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"collection": "my-collection",
"query": "your search query",
})
req, _ := http.NewRequest("POST",
"https://api.redpine.ai/api/v1/search/query", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer sk_test_YOUR_SANDBOX_KEY")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Header.Get("X-Billing-Mode")) // "sandbox"
}Telling sandbox and live apart
Two signals, and it is worth asserting on at least one in your test suite:
- The key prefix --
sk_test_againstsk_live_. - The
X-Billing-Moderesponse header --sandboxagainstlive.
If you are ever unsure which mode a request ran in, read the header. It is set on every response.
What sandbox covers
| Endpoint | Sandbox behaviour |
|---|---|
POST /api/v1/search/query | Returns fixture results |
POST /api/v1/search/{collection} | Returns fixture results |
POST /api/v1/search/assisted | Returns fixture results |
MCP search, preview, confirm | Return fixture results |
MCP call-tool | Refused -- tools reach live third-party services |
| Everything else | 403 SANDBOX_UNSUPPORTED_ENDPOINT |
Anything without sandbox behaviour fails loudly rather than quietly doing the real thing. An endpoint that silently behaved normally under a sandbox key would leave you unable to tell a fixture from a real result, which defeats the point.
{
"error": {
"code": "SANDBOX_UNSUPPORTED_ENDPOINT",
"message": "This endpoint has no sandbox behaviour. Use a live API key, or one of the supported sandbox endpoints.",
"request_id": "req_a1b2c3d4"
}
}What still applies
- Rate limits. Sandbox requests count against your quota, so you can exercise your
429handling. - Collection access. A sandbox key has a scope like any other. Requesting a collection it cannot reach returns
403, exactly as a live key would. - Errors. Malformed requests return the same
422they would in production.
The point is that you test against the real error surface, not a happy path.
Results you can rely on
The same query always returns the same results. That makes sandbox usable in a test suite rather than only in a demo -- you can assert on specific ids and counts without your assertions flapping between runs.
Different queries return different fixtures, covering the shapes you will meet in production: results with figures, with tables, with math, multi-result pages, and an empty result set. Send a few different queries while building, so you exercise more than one.
Going live
Replace the key. That is the whole migration -- same base URL, same endpoints, same request and response shapes.
Keep the sandbox key afterwards. It is the cheapest way to run your integration tests in CI without spending credits every time your build runs.
What sandbox does not tell you
Whether our results are any good for your use case. Fixtures are synthetic, so they say nothing about retrieval quality on your actual questions. Evaluating that needs a live key against a real collection -- talk to us if you want help sizing that.