Sandbox & Testing
The sandbox is a real test-mode data space. A sk_test_ key operates on it; a sk_live_ key does not. Seed it with a deterministic fixture, read that fixture back through the same endpoints your production integration uses, exercise account scoping and content writes, then reset to a clean slate — no OAuth, no real channel, no waiting.
Picking a key
Newly issued keys carry the environment in their prefix: sk_test_ is a test key that operates on the sandbox, sk_live_ is a production key that operates on live data. The sandbox endpoints (/v1/sandbox/seed, /v1/sandbox/reset) require a test key — a live key gets 403 forbidden. Legacy bare-hex keys still work — they default to live. See Authentication → Legacy keys.
How test keys behave
- Operate on the sandbox, not live data. A
sk_test_key reads and writes the sandbox data space. Sandbox writes never touch a real OTA. Ask_live_key operates on your production data and is rejected from the sandbox seed/reset endpoints with403 forbidden. - Work before you verify your email. A test key is usable immediately after signup. A live key held by an unverified owner is rejected with
email_verification_requireduntil you confirm your email. - Lower rate limit while unverified. Unverified test traffic is capped at a reduced hourly limit so you can integrate safely. Verify your email to lift it. See Rate Limits.
Empty until you seed
data array — that is expected, not an error. Call POST /v1/sandbox/seed to populate deterministic fixtures you can read back.Quickstart
The full loop, top to bottom: grab a sk_test_ key from your dashboard, seed the fixture, list it, fetch one by id, scope a read to a single connected account, round-trip a content update, then reset.
BASE=https://api.repull.dev
KEY="Authorization: Bearer sk_test_YOUR_KEY"
# 1. Seed deterministic fixtures (idempotent)
curl -sX POST "$BASE/v1/sandbox/seed" -H "$KEY"
# 2. List the seeded listings — returns 900000101, 900000102, 900000103
curl -s "$BASE/v1/listings" -H "$KEY"
# 3. Fetch one by id, with content + details expanded
curl -s "$BASE/v1/listings/900000101?include=content,details" -H "$KEY"
# 4. Scope reads to the Airbnb connected account (Ocean View + Cabin only)
curl -s "$BASE/v1/listings" -H "$KEY" -H "X-Account-Id: 960000101"
# 5. Round-trip a content update on a sandbox listing
curl -sX PUT "$BASE/v1/listings/900000101/content" -H "$KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Sandbox Ocean View Loft — updated"}'
# 6. Reset the sandbox back to empty
curl -sX POST "$BASE/v1/sandbox/reset" -H "$KEY"Seed the fixture
POST /v1/sandbox/seed populates your sandbox with a fixed set of fixtures. It is idempotent — call it as many times as you like; it will not stack duplicates. Every fixture has a stable id (all sandbox ids are ≥ 900000000), so you can hard-code them in your tests. The response returns the ids it created.
curl -X POST https://api.repull.dev/v1/sandbox/seed \
-H "Authorization: Bearer sk_test_YOUR_KEY"
# → 200
{
"data": {
"listings": [900000101, 900000102, 900000103],
"reservations": [930000101, 930000102, 930000103, 930000104, 930000105],
"accounts": [960000101, 960000102]
}
}3 listings
| Id | Title | Connected channels |
|---|---|---|
900000101 | Sandbox Ocean View Loft | Airbnb and Booking.com |
900000102 | Sandbox Pine Ridge Cabin | Airbnb only |
900000103 | Sandbox Downtown Studio | Booking.com only |
5 reservations
Reservations 930000101 through 930000105 span the lifecycle so you can drive status-handling logic against them:
| Id | Status |
|---|---|
930000101 | created |
930000102 | confirmed |
930000103 | cancelled |
930000104 | modified |
930000105 | pending |
Every reservation carries an updatedAt timestamp. Amended reservations (the modified and date-changed ones) have an updatedAt that is strictly after their createdAt, so you can build reconciliation logic — “what changed since my last sync?” — against the fixture.
2 connected accounts
The seeder creates two fake connected accounts, both with status active: 960000101 (Airbnb) and 960000102 (Booking.com). There is no OAuth and no real OTA behind them — they exist so you can test connected-account resolution and scoping. Read them back with GET /v1/connect.
Reading fixtures
The sandbox serves the seeded fixtures through the same read endpoints your production integration uses — both the collection form and the by-id form. An unknown fixture id returns 404 not_found.
GET /v1/listings·GET /v1/listings/{id}— supports?include=content,detailsGET /v1/reservations·GET /v1/reservations/{id}GET /v1/connect— the two seeded connected accountsGET /v1/channels/airbnb/listings·GET /v1/channels/airbnb/listings/{id}GET /v1/availability/{propertyId}?from=YYYY-MM-DD&to=YYYY-MM-DD— a deterministic, all-available calendar at the fixture's nightly price
Availability needs a valid range
GET /v1/availability/{propertyId} requires both from and to as YYYY-MM-DD dates. A missing or invalid range returns 422 invalid_params. The calendar it returns is deterministic — every night is available at the listing's nightly price — so you can assert on it in CI.# A listing, with content + details expanded curl "https://api.repull.dev/v1/listings/900000101?include=content,details" \ -H "Authorization: Bearer sk_test_YOUR_KEY" # A deterministic availability window curl "https://api.repull.dev/v1/availability/900000101?from=2026-08-01&to=2026-08-07" \ -H "Authorization: Bearer sk_test_YOUR_KEY"
Account scoping
Account scoping works end-to-end in the sandbox, so you can prove your multi-tenant PMS isolates tenants correctly. Pass the X-Account-Id header to pin reads to a single connected account:
X-Account-Id: 960000101(Airbnb) scopes to the two Airbnb-linked listings — Ocean View Loft and Pine Ridge Cabin — and their reservations.X-Account-Id: 960000102(Booking.com) scopes to the two Booking-linked listings — Ocean View Loft and Downtown Studio — and their reservations.- No header scopes to the full workspace: all three listings.
A by-id read for a fixture whose channel doesn't match the pinned account returns 404 not_found — e.g. fetching the Booking-only Downtown Studio while pinned to the Airbnb account. See API Keys & Account Scope for how the header behaves against live data.
# Airbnb account → Ocean View + Cabin curl "https://api.repull.dev/v1/listings" \ -H "Authorization: Bearer sk_test_YOUR_KEY" \ -H "X-Account-Id: 960000101" # Pinned to Airbnb, the Booking-only Studio is out of scope → 404 curl "https://api.repull.dev/v1/listings/900000103" \ -H "Authorization: Bearer sk_test_YOUR_KEY" \ -H "X-Account-Id: 960000101"
Content writes round-trip
PUT /v1/listings/{id}/content on a sandbox listing updates the fixture — title, description, amenities, photos, occupancy, policies — and returns { id, changed: [...], deferred: [...] }. The change is readable on the next GET and bumps the listing's updatedAt, so you can test your write-then-read flow without a live channel.
curl -X PUT https://api.repull.dev/v1/listings/900000101/content \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Sandbox Ocean View Loft — updated","maxOccupancy":6}'
# → 200
{ "data": { "id": 900000101, "changed": ["title", "maxOccupancy"], "deferred": [] } }
# Read it back — the change is live and updatedAt has moved
curl "https://api.repull.dev/v1/listings/900000101" \
-H "Authorization: Bearer sk_test_YOUR_KEY"Reset
POST /v1/sandbox/reset clears all sandbox fixtures for your workspace so your next run starts from zero — the pattern for isolating one test case from the next.
curl -X POST https://api.repull.dev/v1/sandbox/reset \
-H "Authorization: Bearer sk_test_YOUR_KEY"
# → 200
{ "data": { "cleared": true } }Contract testing
Seed + reset turn the sandbox into a fixture you can drive from CI. The loop is: reset to a clean slate, seed the known fixture, run your integration against it, and assert on the stable ids and statuses your app depends on. Because test keys work the moment you sign up — no email verification, no live provider — you can wire this into a pull-request check.
# A contract-test run, top to bottom
BASE=https://api.repull.dev
KEY="Authorization: Bearer sk_test_YOUR_KEY"
# 1. Clean slate, then load the known fixture
curl -sX POST "$BASE/v1/sandbox/reset" -H "$KEY" > /dev/null
curl -sX POST "$BASE/v1/sandbox/seed" -H "$KEY" > /dev/null
# 2. Assert the deterministic listing ids are present
ids=$(curl -s "$BASE/v1/listings" -H "$KEY" | jq -c '[.data[].id] | sort')
test "$ids" = "[900000101,900000102,900000103]" \
|| { echo "contract drift: got $ids"; exit 1; }
# 3. Assert account scoping isolates tenants
airbnb=$(curl -s "$BASE/v1/listings" -H "$KEY" -H "X-Account-Id: 960000101" \
| jq '.data | length')
test "$airbnb" = "2" || { echo "scoping drift: expected 2, got $airbnb"; exit 1; }Assert on the stable fixture — the seeded ids, reservation statuses, and the presence of a pagination object — and pair it with the error-envelope checks in Sandbox Test Scenarios so a single CI run verifies both the happy path and your failure handling.
Not yet available
To keep expectations honest, here is what the sandbox does not model today:
- Magic failure ids. Ids that deterministically trigger a specific failure (Stripe-test-card style — force a
409, a504, or a rate-limit response on demand) are not available yet. They are planned. - Channel write flows beyond content. Content updates round-trip, but sandbox-aware channel writes past content — publish, messaging echo, and similar — are not modeled yet.
- Guests, reviews, conversations. The fixture is listings, reservations, and connected accounts. There are no seeded guest, review, or conversation fixtures.
Watch the changelog