Sandbox and Production
Every account has both environments from the moment it is created. They run the same code against the same schema, so anything that works in Sandbox works in Production — but they never share a single row of data. Sandbox is free and needs no plan, which means there is no reason to develop against live data and no cost to not doing so.
One base URL, two environments
There is no separate sandbox hostname and no ?test=true parameter. The key you send determines which environment you are in, and nothing else about the request changes.
The only difference
# Sandbox
curl https://countdownshare.com/api/v1/timers \
-H "Authorization: Bearer cs_test_..."
# Production — same URL, same path, different key
curl https://countdownshare.com/api/v1/timers \
-H "Authorization: Bearer cs_live_..."This is deliberate. A separate sandbox host is a second thing that can drift, and drift between test and live environments is how integrations pass every test and then fail on launch day. Here there is one deployment, one codebase, and one contract.
What actually differs
The behaviour of every endpoint is identical. What changes is the ceiling on volume and whether a timer can become public.
| Sandbox | Production | |
|---|---|---|
| Key prefix | cs_test_ | cs_live_ |
| Cost | Free with any account | Starter, Growth, or Custom plan |
| New timers per month | 100 | 1,000 – 10,000+ |
| Requests per minute | 30 | 300 |
| API keys | 1 | 3 – 10+ |
| Webhook destinations | 1 | 5 – 25+ |
| Webhook deliveries per month | 100 | Unlimited |
| Webhook rules | Unlimited | Unlimited |
| Publish public pages and embeds | Not available | Available |
| Destination URL scheme | http:// or https:// | https:// only |
How strict the isolation is
Complete. A Sandbox key presented with a Production timer ID does not return a permission error — it returns not_found, because from that key's perspective the timer does not exist. There is no query the API can be asked that crosses the boundary.
Separate rows
Timers, keys, destinations, rules, and deliveries all carry an environment. Nothing is shared between the two.
Separate meters
Sandbox usage never counts toward your Production allowance, and the two billing cycles are tracked independently.
Enforced below the code
The constraint lives in the database, so a bug in a route handler cannot leak across it. Application logic is the second line of defence, not the first.
A practical consequence: you cannot promote a Sandbox timer to Production. There is no copy operation, and there should not be — a timer that was created for a test should not quietly become one your customers see. Recreate it with a Production key.
Testing webhooks locally
Sandbox accepts http:// destination URLs specifically so you can point a destination at a local tunnel — ngrok, Cloudflare Tunnel, or your own — while you develop the handler. Production requires HTTPS.
Private, loopback, and link-local addresses are rejected in both environments, so http://localhost:3000 will not be accepted. Use the public tunnel URL the tool gives you rather than the address it forwards to.
POST /webhook-destinations/{destination_id}/test sends a real signed delivery on demand. Use it to confirm your signature verification works before any timer exists, which separates “my handler is wrong” from “my rule never matched”. See verify a delivery.Going live
The code change is one environment variable. Everything below is the part people forget.
Configuration
Boot-time guard
// Environment is a deployment concern, not a code branch.
// One variable changes everything; nothing else in your code differs.
const key = process.env.COUNTDOWNSHARE_API_KEY;
// A guard worth having: fail loudly at boot rather than
// quietly writing test data to a live account.
if (process.env.NODE_ENV === "production" && !key?.startsWith("cs_live_")) {
throw new Error("Production build is holding a non-production API key");
}Checklist
Swap the key
A cs_live_ key in your production secret store, and the Sandbox key removed from it.
Add publish: true
Production timers are still drafts by default. A timer nobody can see is the most common launch-day surprise.
Re-register destinations
Destinations do not cross environments. Create the Production one with its public HTTPS URL and store the new signing secret.
Re-create webhook rules
Rules belong to a timer, so Production timers need their own rules attached.
Recheck rate limits
Production allows ten times the Sandbox rate. If you built throttling around 30 rpm, you are now leaving throughput unused.
Confirm the monthly allowance
GET /usage reports the limit and what remains on the current plan. Check it matches the volume you expect.
Handle quota_exhausted
It is a 429, like the rate limit, but it does not clear on its own. Make sure your code distinguishes them.