Redpine Connect

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_KEY

Sandbox 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.synthetic is true
  • id is prefixed sandbox-

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_ against sk_live_.
  • The X-Billing-Mode response header -- sandbox against live.

If you are ever unsure which mode a request ran in, read the header. It is set on every response.

What sandbox covers

EndpointSandbox behaviour
POST /api/v1/search/queryReturns fixture results
POST /api/v1/search/{collection}Returns fixture results
POST /api/v1/search/assistedReturns fixture results
MCP search, preview, confirmReturn fixture results
MCP call-toolRefused -- tools reach live third-party services
Everything else403 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 429 handling.
  • 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 422 they 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.

Was this page helpful?

On this page