Skip to content

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, 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, 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:
sh
export INFRANEST_TOKEN='paste the token here'
powershell
$env:INFRANEST_TOKEN = 'paste the token here'
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
// 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.

sh
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
$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
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
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 ​

sh
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
$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
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
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.

sh
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
$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
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
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.

sh
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
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
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
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.

sh
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
$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
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
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.

Where to go next ​

  • Limits — the rate limits and other numbers a script should stay inside.
  • Recipes — complete scripts: certificates through DNS, dynamic DNS, maintenance from a pipeline, a monitor per deploy.
  • AI assistants — let Claude, ChatGPT, Cursor or VS Code read your account, or have a coding agent write against the API.
  • Errors and Troubleshooting — every code, and what to do about it.
  • Tokens and scopes — before you make a live token.