# Test & live modes

> Each API key is bound to one project and one mode (test or live). Test and live data are fully isolated, and only live first attempts count toward the dispatch SLO.

Every API key is bound to exactly one **project** and one **mode** — `test` or `live`.
The mode is baked into the key prefix:

- `sk_test_…` — a **test-mode** key
- `sk_live_…` — a **live-mode** key

You pass the key as a bearer token, and the mode comes along with it. There is no
`mode` parameter on any request — the key decides.

```bash
# A test-mode request: builds, schedules, and delivers against test data only.
curl https://api.schedstack.com/v1/schedules \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "https://example.com/hooks/orders",
    "method": "POST",
    "body": "{\"order_id\":\"ord_123\"}",
    "delay": "30s"
  }'
```

The resources you create echo their mode back so you always know which side you're on:

```json
{
  "id": "sch_…",
  "object": "schedule",
  "mode": "test",
  "endpoint": "https://example.com/hooks/orders",
  "method": "POST",
  "state": "active"
}
```

## Data is fully isolated per mode

Test and live are **separate datasets within the same project**. A `sk_test_…` key
cannot read, list, or mutate live data, and a `sk_live_…` key cannot touch test data.
Isolation is enforced at the query layer — every read and write is scoped by
`(project, mode)`.

This applies to everything keyed off a request:

- **Schedules, endpoints, and deliveries** are mode-scoped. `GET /v1/schedules`,
  `GET /v1/deliveries/{id}`, and attempt history return only rows for the calling key's
  mode.
- **[Idempotency keys](/docs/concepts/idempotency/) are mode-scoped too.** The same
  `idempotency_key` used by a `sk_test_…` request and a `sk_live_…` request will **not**
  collide — they resolve to independent schedules. A test request never replays a live
  response, or vice versa.

There is no cross-mode read. If you create a schedule with a test key and then call the
API with a live key, you'll get a `404` for that schedule — it doesn't exist in live data.
Switching modes means switching keys, and you start from an empty, independent dataset.

## Only live first attempts count toward the SLO

SchedStack publishes a **100–300 ms dispatch SLO** (`fired_at − scheduled_for`, p99,
where `fired_at` is the first byte out). The SLO measures **dispatch initiation** — when
we start the send — not your receiver's response time.

The published number is computed over a deliberately narrow population:

- **`live` mode only** — test-mode dispatches are excluded entirely.
- **First attempts only** (the initial fire). Retries, reclaim/recovery fires,
  rate-limit or breaker deferrals, catch-up/coalesced fires, and DST-shifted fires are
  all excluded.

Test mode is for correctness, not timing. Use it to verify that your endpoint receives,
[verifies the signature on](/docs/guides/verify-signatures/), and
[idempotently handles](/docs/guides/idempotency-keys/) a delivery — but don't read
anything into test-mode dispatch latency.

## Develop safely in test mode

1. **Build against a `sk_test_…` key.** Point your integration at the same base URL and
   paths — only the key changes. Schedule short delays, watch deliveries land, and inspect
   [retries and the dead-letter view](/docs/concepts/retries-and-dead-letter/) without
   touching live data.

2. **Verify the full contract.** Confirm your endpoint
   [verifies the `Sched-Signature`](/docs/guides/verify-signatures/) and dedupes on the
   delivery ID — at-least-once delivery means you own deduplication. Test mode is the place
   to prove this before real traffic.

3. **Swap to `sk_live_…` to go live.** Same code, live key. Because data is isolated,
   nothing you created in test leaks into live — you start clean. Keep the test key wired
   into CI and local dev.

Treat the two keys like any other environment secret: `sk_test_…` in development and CI,
`sk_live_…` only in production. Keys are shown **once** at creation and stored hashed —
if you lose one, mint a new one rather than trying to recover it.

## Where to go next

- **[Idempotency](/docs/concepts/idempotency/)** — how idempotency keys are scoped and how
  to dedupe at-least-once delivery.
- **[Retries & dead-letter](/docs/concepts/retries-and-dead-letter/)** — what happens when a
  delivery doesn't land on the first attempt.
- **[Quickstart](/docs/quickstart/)** — schedule your first durable delivery and watch it
  land.
