CustomDomain™ docs
Guides

Test mode

Run the widget, the API and your webhooks end to end on made up .test names, before a customer has a domain. Nothing is looked up, served, billed or counted.

Test mode lets you build and check your whole integration before a customer has a domain, and before you have added a card. A test mode application runs the real connect flow: the widget's screens, the API calls and the webhooks. The one thing it makes up is DNS. It connects names that end in .test, decides how each one ends, and counts for nothing. It does not use up your free domains or your plan's allowance, it is never billed, and nothing is looked up, written to a DNS provider or served.

Use it to:

  • see the widget in your product, with your look, from the first screen to Live;
  • write and check your webhook receiver against real, signed deliveries;
  • try each ending a customer can reach: live, a name another account already has, and DNS that never shows up;
  • run the same checks in CI or a preview environment with no real domain.

It does not try a DNS provider's sign in, real DNS, certificates or hosting. Those need a real domain in a live application.

Make a test mode application

In the console, each of these makes one, switches the console to it, and needs no card:

  • Try test mode in the application switcher.
  • Applications, then New application, then Test mode.
  • Try test mode, next to Try the widget with your own domain on the go-live list on Overview.
  • Try it in test mode, on the Preview tab of Embed.

With the API, send "sandbox": true when you create the application:

curl -X POST https://api.customdomain.ai/v1/applications \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Test mode","sandbox":true}'
# → 201 { "id": "app_…", "sandbox": true, "environment": "development", "client_secret": "…", … }
  • sandbox is fixed when the application is made and never changes. Leave environment out: a test mode application is always a development application.
  • A workspace holds up to 3 test mode applications, and each holds up to 100 test domains. Past that, the answer is 409 sandbox_limit.
  • It has its own client_secret, and POST /v1/applications/{id}/keys makes its API keys. Neither needs a card, because nothing a test mode application holds can reach anything that costs money or serves a site.
  • A key's prefix does not tell you the mode. A test mode application's keys start with cd_test_, and so does any key made for a staging or development environment, which works on real domains like a cd_live_ key does. The application's sandbox field is what decides: GET /v1/applications and GET /v1/applications/{id} carry "sandbox": true for a test mode application and no field for a live one.

Run the widget on it

Mint a widget token with the test application's client_secret, the way you do for any application (Widget tokens), and open the widget with it. The token carries a signed sandbox: true, and the widget:

  • shows a Test mode strip across the top of the modal;
  • asks for a .test name, with www.acme.test as the example, and says what to type when someone enters a real one;
  • goes straight to the manual setup, which says there is nothing to add: a simulated DNS finds the records;
  • leaves out the ways to hand the setup to someone else and hosted DNS, which need a real domain.

All of those words are translated into every language the widget speaks. The widget does not decide any of it. It reads the signed claim to know what to say, and the control plane enforces everything on every call.

In the console, the same widget opens from the Preview tab of Embed, and Connect a test domain on Domains connects a name without the widget.

The name picks the ending

There is nothing to switch. The first part of the name decides how the run ends, and the simulated DNS keeps time from when the domain was added or last re-checked.

You connectWhat happens
www.acme.test, or any name that does not start with conflict or timeoutAbout 15 seconds after it is added, the simulated DNS shows its records and it goes live. connection.live fires.
conflict.acme.testIt is pending at once with domain_already_connected, the way a name another account already has connected would be, and it never goes live.
timeout.acme.testIts records never appear. After 60 seconds it fails with propagation_timeout and connection.failed fires. Re-check starts a new run, which ends the same way.

The first part has to be exactly conflict or timeout: conflict-2.acme.test goes live. A name that does not end in .test is refused with 400 sandbox_domain_required in a test mode application, and a .test name in a live application with 400 sandbox_domain_reserved, so the two never mix. .test is reserved by the internet standards for testing and can never be a real domain, so a test domain cannot collide with a customer's.

The widget keeps showing the records as not found for a timeout name. The failure is on the connection, which you read with GET /v1/connections/{id} or in the connection.failed event, the same way you would for a real domain that never resolved.

What the API does in test mode

The calls are the ones you already use, with the same bodies, errors and statuses:

# 1. Mint a token for the test application (your server, as always).
curl -X POST https://api.customdomain.ai/v1/tokens \
  -H "Content-Type: application/json" \
  -d '{"application_id":"<TEST_APP_ID>","client_secret":"<TEST_CLIENT_SECRET>","domain":"www.acme.test"}'

# 2. Connect a name, with that token or with a key of the test application.
curl -X POST https://api.customdomain.ai/v1/connections \
  -H "Authorization: Bearer <TEST_APP_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"domain":"www.acme.test"}'
# → 201 { "id": "…", "status": "pending", "sandbox": true, "records": [ … ], … }

# 3. About 15 seconds later.
curl https://api.customdomain.ai/v1/connections/<ID> -H "Authorization: Bearer <TEST_APP_KEY>"
# → { "status": "live", "sandbox": true, … }
  • POST /v1/domains:check answers with the manual rail only, "sandbox": true, and the reason test_mode on the rails that need a real provider. Nothing is detected.
  • A connection has "sandbox": true. It is copied from the application when the connection is made and never changes. application_url, if you send one, is accepted and ignored: there is nothing to serve.
  • The records are the ones a live domain would be shown, so your own screens that print them can be checked as they are. They are never looked up.
  • DELETE /v1/connections/{id} removes a test domain at once, with connection.disconnected. Deleting a test mode application removes its test domains with it.
  • GET /v1/connections:lookup?domain=www.acme.test finds a test domain by its exact name, with a key or a token of the test application, or with a workspace key.

What a test credential can reach

A key or widget token of a test mode application can run the connect flow and read its own webhooks, and nothing else: domains:check, connections (create, list, read, records, diagnose, recheck, remove, lookup), webhooks and webhook-deliveries, tokens, the catalogs the widget reads (plans, providers, templates, config), and GET /v1/applications, which lists only its own application. Every other route answers 403 sandbox_not_allowed before it does anything, including billing, members, the audit log, SSL, Power, sign in with a DNS provider, bulk creation, sharing, domain purchases and agents. A test credential never reaches a live domain of the same workspace, which answers as if it were someone else's.

That also means a test key left in a repository, a CI log or a screenshot is worth nothing.

The wall works the other way too. A test domain is served only on those same routes, whoever asks, so a workspace key cannot start SSL, Power or a provider sign in on a name that has no DNS. And an AI agent cannot be given a test mode application: consent and grants are for live applications.

Webhooks

A test mode application has its own endpoints, made with its own key or token, and its events reach only those. Every event about a test domain carries "sandbox": true; a live event carries no such field (Webhooks). They are signed and retried like any other, so your receiver can be checked end to end. A receiver that writes to a real customer's account should read the field and skip them.

GET /v1/webhooks with a workspace key leaves a test mode application's endpoints out unless you pass ?app_id= naming it, so a test endpoint is never mistaken for a live one.

What it does not count

A test domain is left out of everything that counts, bills or serves real domains:

  • your free domains, your trial, your plan's allowance and your bill, and usage metering;
  • the workspace's totals and every list that covers all of your applications. Domains and Activity show a test application's rows only while the console is switched to it, marked Test mode, and they are never in a total;
  • notifications and email: a test domain going live rings no bell, sends no push and sends no email;
  • the edge, certificates, monitoring and DNS provider accounts: none of them has anything to do for a made up name, and a test domain has no Edge, Certificate, History or Traffic view, only Overview and DNS;
  • the go-live checklist rows: a token minted for a test application does not count as your server minting a real one, and a test domain is not a customer.

Move to a real domain

A test mode application is a place to rehearse, so nothing carries over. When you are ready, switch the console to your live application (the application switcher), mint tokens with its client secret, and connect a real name. Your server code does not change apart from the credentials. If you read sandbox on webhook events, real events simply do not carry it.

On this page