# Quickstart Five minutes, on your sandbox, with a known answer at every step: find the sandbox, read its sample DNS zones, try a change as a dry run, make it, and undo it. Everything here happens in the [sandbox](sandbox.md), with a test token, so nothing reaches a DNS provider or the internet. If a step answers something other than what is shown, the problem is on our side or in the step before — and the error's `code` says which. These exact commands run against a real sandbox in the [examples repository's CI](https://github.com/InfraNest-Infrastructure-Organized/infranest-examples/tree/main/examples/quickstart), so they stay correct. ## 1. Create your sandbox and a test token 1. In the app, open **My account → Developer** and select **Create a sandbox**. InfraNest makes it, fills it with sample data and switches you to it. 2. Still in the sandbox, choose **New token**, name it `quickstart` and tick **DNS → Read** and **DNS → Write**. 3. Copy the token (it starts `…|in_test_`) and put it in your shell: ::: code-group ```sh [cURL] export INFRANEST_TOKEN='paste the token here' ``` ```powershell [PowerShell] $env:INFRANEST_TOKEN = 'paste the token here' ``` ```python [Python] # In the shell that runs Python: export INFRANEST_TOKEN='paste the token here' import os, requests API = "https://api.infranest.io/api" HEADERS = {"Authorization": f"Bearer {os.environ['INFRANEST_TOKEN']}", "Accept": "application/json"} ``` ```js [JavaScript] // In the shell that runs Node 18+: export INFRANEST_TOKEN='paste the token here' const API = 'https://api.infranest.io/api' const HEADERS = { Authorization: `Bearer ${process.env.INFRANEST_TOKEN}`, Accept: 'application/json' } ``` ::: ## 2. Find your sandbox Ask who the token belongs to. A test token is bound to its sandbox, so you get exactly one organization, marked `is_sandbox`. ::: code-group ```sh [cURL] curl -s https://api.infranest.io/api/auth/me \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" \ | jq '.user.organizations[] | {id, name, is_sandbox}' ``` ```powershell [PowerShell] $headers = @{ Authorization = "Bearer $env:INFRANEST_TOKEN"; Accept = 'application/json' } $me = Invoke-RestMethod -Uri 'https://api.infranest.io/api/auth/me' -Headers $headers $me.user.organizations | Select-Object id, name, is_sandbox ``` ```python [Python] me = requests.get(f"{API}/auth/me", headers=HEADERS).json() print([(o["id"], o["name"], o["is_sandbox"]) for o in me["user"]["organizations"]]) ``` ```js [JavaScript] const me = await (await fetch(`${API}/auth/me`, { headers: HEADERS })).json() console.log(me.user.organizations.map(({ id, name, is_sandbox }) => ({ id, name, is_sandbox }))) ``` ::: You see your sandbox's ID (yours will differ from `2`): ```json { "id": 2, "name": "Sandbox", "is_sandbox": true } ``` Keep the ID: `export ORG=2` (PowerShell: `$env:ORG = 2`). ## 3. Read the sample zones ::: code-group ```sh [cURL] curl -s "https://api.infranest.io/api/organizations/$ORG/dns-zones" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" \ | jq '.data[] | {id, name, records_count}' ``` ```powershell [PowerShell] $zones = Invoke-RestMethod -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-zones" -Headers $headers $zones.data | Select-Object id, name, records_count ``` ```python [Python] org = 2 # your sandbox's ID zones = requests.get(f"{API}/organizations/{org}/dns-zones", headers=HEADERS).json()["data"] print([(z["id"], z["name"], z["records_count"]) for z in zones]) ``` ```js [JavaScript] const org = 2 // your sandbox's ID const zones = (await (await fetch(`${API}/organizations/${org}/dns-zones`, { headers: HEADERS })).json()).data console.log(zones.map(({ id, name, records_count }) => ({ id, name, records_count }))) ``` ::: Two zones, named after your sandbox's ID: ```json { "id": 1, "name": "sandbox-2.example", "records_count": 7 } { "id": 2, "name": "shop-2.example", "records_count": 2 } ``` Keep the ID of `sandbox-….example`: `export ZONE=1` (PowerShell: `$env:ZONE = 1`). ## 4. Try a change as a dry run Add an `A` record named `quickstart`, with `?dry_run=1`: the API answers with the change it would make, and makes none. ::: code-group ```sh [cURL] curl -s -X POST "https://api.infranest.io/api/organizations/$ORG/dns-zones/$ZONE/dns-records?dry_run=1" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"type": "A", "name": "quickstart", "value": "192.0.2.50", "ttl": 3600}' ``` ```powershell [PowerShell] $record = @{ type = 'A'; name = 'quickstart'; value = '192.0.2.50'; ttl = 3600 } | ConvertTo-Json Invoke-RestMethod -Method Post -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-zones/$env:ZONE/dns-records?dry_run=1" ` -Headers $headers -ContentType 'application/json' -Body $record ``` ```python [Python] zone = 1 # the ID of sandbox-….example record = {"type": "A", "name": "quickstart", "value": "192.0.2.50", "ttl": 3600} dry = requests.post(f"{API}/organizations/{org}/dns-zones/{zone}/dns-records", params={"dry_run": 1}, headers=HEADERS, json=record) print(dry.json()) ``` ```js [JavaScript] const zone = 1 // the ID of sandbox-….example const record = { type: 'A', name: 'quickstart', value: '192.0.2.50', ttl: 3600 } const post = (path, body, extra = {}) => fetch(`${API}${path}`, { method: 'POST', headers: { ...HEADERS, 'Content-Type': 'application/json', ...extra }, body: JSON.stringify(body), }) console.log(await (await post(`/organizations/${org}/dns-zones/${zone}/dns-records?dry_run=1`, record)).json()) ``` ::: ```json { "dry_run": true, "zone": "sandbox-2.example", "provider_push": "none", "changes": [ { "action": "create", "before": null, "after": { "type": "A", "name": "quickstart", "value": "192.0.2.50", "ttl": 3600 } } ], "unchanged": 0, "skipped": [] } ``` `provider_push: none` because a sandbox zone is kept in InfraNest only. In a live organization it says how the change would reach your DNS provider. ## 5. Make the change The same request without `?dry_run=1`. The `Idempotency-Key` makes a retry of this request safe: send it again with the same key, and you get the same answer instead of a second record. ::: code-group ```sh [cURL] curl -s -X POST "https://api.infranest.io/api/organizations/$ORG/dns-zones/$ZONE/dns-records" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"type": "A", "name": "quickstart", "value": "192.0.2.50", "ttl": 3600}' ``` ```powershell [PowerShell] Invoke-RestMethod -Method Post -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-zones/$env:ZONE/dns-records" ` -Headers ($headers + @{ 'Idempotency-Key' = [guid]::NewGuid().ToString() }) -ContentType 'application/json' -Body $record ``` ```python [Python] import uuid created = requests.post(f"{API}/organizations/{org}/dns-zones/{zone}/dns-records", headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())}, json=record) print(created.status_code, created.json()) ``` ```js [JavaScript] const created = await post(`/organizations/${org}/dns-zones/${zone}/dns-records`, record, { 'Idempotency-Key': crypto.randomUUID() }) console.log(created.status, await created.json()) ``` ::: The API answers `201 Created` with the new record (shortened here; IDs and times will differ): ```json { "id": 10, "dns_zone_id": 1, "type": "A", "name": "quickstart", "value": "192.0.2.50", "ttl": 3600, "priority": null, "created_at": "2026-10-25T19:01:40.000000Z" } ``` ## 6. Undo it Every DNS change is recorded in the zone's history, in batches. Take the newest batch, and undo it. ::: code-group ```sh [cURL] BATCH=$(curl -s "https://api.infranest.io/api/organizations/$ORG/dns-zones/$ZONE/changes" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" | jq -r '.batches[0].batch_id') curl -s -X POST "https://api.infranest.io/api/organizations/$ORG/dns-changes/undo" \ -H "Authorization: Bearer $INFRANEST_TOKEN" -H "Accept: application/json" \ -H "Content-Type: application/json" -d "{\"batch_id\": \"$BATCH\"}" ``` ```powershell [PowerShell] $changes = Invoke-RestMethod -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-zones/$env:ZONE/changes" -Headers $headers $undo = @{ batch_id = $changes.batches[0].batch_id } | ConvertTo-Json Invoke-RestMethod -Method Post -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-changes/undo" ` -Headers $headers -ContentType 'application/json' -Body $undo ``` ```python [Python] batch = requests.get(f"{API}/organizations/{org}/dns-zones/{zone}/changes", headers=HEADERS).json()["batches"][0]["batch_id"] print(requests.post(f"{API}/organizations/{org}/dns-changes/undo", headers=HEADERS, json={"batch_id": batch}).json()) ``` ```js [JavaScript] const history = await (await fetch(`${API}/organizations/${org}/dns-zones/${zone}/changes`, { headers: HEADERS })).json() console.log(await (await post(`/organizations/${org}/dns-changes/undo`, { batch_id: history.batches[0].batch_id })).json()) ``` ::: ```json { "batch_id": "935a6fe3-c635-47ad-a2bd-3985a68e79a4", "results": [ { "change_id": 10, "record": { "type": "A", "name": "quickstart", "value": "192.0.2.50" }, "outcome": "undone" } ], "undone": 1, "skipped": 0, "failed": 0 } ``` The `quickstart` record is gone, and the undo is itself a batch in the history, so it can be undone too. To start the sandbox over completely, [reset it](sandbox.md#start-again). ## Where to go next - **[Limits](limits.md)** — the rate limits and other numbers a script should stay inside. - **[Recipes](recipes/acme-dns-01.md)** — complete scripts: certificates through DNS, dynamic DNS, maintenance from a pipeline, a monitor per deploy. - **[AI assistants](ai-assistants.md)** — let Claude, ChatGPT, Cursor or VS Code read your account, or have a coding agent write against the API. - **[Errors](errors.md)** and **[Troubleshooting](troubleshooting.md)** — every `code`, and what to do about it. - **[Tokens and scopes](tokens-and-scopes.md)** — before you make a live token.