# Test mode

Test keys create test domains that walk every state on a ten-second timer. No DNS record, no certificate authority, no bill.

## Test keys

A key is either live (`sk_live_…`) or test (`sk_test_…`). Test keys are free and need no card. Everything a test key creates is a test domain:

- It moves `pending_dns → verifying → issuing → active`, one step every 10 seconds, with `status_reason: "sandbox"`. No DNS record is needed and no certificate authority is contacted.
- It fires the same webhooks as a live domain, signed the same way, so your webhook handler is tested end to end.
- It is never served by the edge and never counts towards your plan.

## What a test key cannot do

Test keys are sandboxed so a leaked one cannot change production:

- It sees and changes test domains only; live domains are invisible to it (`404 not_found`).
- It can create test keys, never live ones (`403 forbidden`).
- It can set the app's origin and webhook URL only while the app has no live domains (`403 forbidden` after that).
- It cannot rotate the signing secret (`403 forbidden`).

## Run the quickstart with a test key

The [Quickstart](/docs/quickstart) uses a test key throughout. The domain you create reaches `active` about 30 seconds later and `domain.active` arrives at your webhook URL. The same six steps run every night against production.

```bash tab="curl"
# Wait for active (about 30 s):
curl https://api.apexly.dev/v1/events?domain_id=dom_8f3a&type=domain.active \
  -H "Authorization: Bearer sk_test_…"
```

```js tab="Node"
const res = await fetch(
  "https://api.apexly.dev/v1/events?domain_id=dom_8f3a&type=domain.active",
  { headers: { Authorization: "Bearer sk_test_…" } },
);
const { data } = await res.json(); // one event once the domain is active
```

```python tab="Python"
import requests

res = requests.get(
    "https://api.apexly.dev/v1/events",
    params={"domain_id": "dom_8f3a", "type": "domain.active"},
    headers={"Authorization": "Bearer sk_test_…"},
)
events = res.json()["data"]  # one event once the domain is active
```

## The sandbox origin

No app yet? Set your origin to `https://sandbox.apexly.dev`. It answers every request with what Apexly added to it: the host, the domain and app ids, the timestamp, and `signature_valid`, which says whether `X-Apexly-Signature` checks out against your signing secret. Point a live domain at it to see exactly what your app will receive before you write your own check.

```json
{
  "sandbox": true,
  "method": "GET",
  "path": "/",
  "host": "careers.acme.com",
  "apexly": {
    "domain_id": "dom_8f3a",
    "app_id": "app_hf_2k9x",
    "timestamp": "1760587971",
    "signed": true,
    "signature_valid": true
  }
}
```

## Going live

Create a live key in Settings (it needs a plan: Free promotional, a paid plan or a card on file; `402 live_keys_need_plan` on plain Free) and swap it in. Nothing else changes: the same requests, responses and webhooks, with real DNS checks and real certificates.

- [Quickstart](/docs/quickstart)
- [How it works](/docs/concepts/domain-states)
- [Limits and pricing](/docs/limits-and-pricing)
