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
- 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.
- Still in the sandbox, choose New token, name it
quickstartand tick DNS → Read and DNS → Write. - Copy the token (it starts
…|in_test_) and put it in your shell:
export INFRANEST_TOKEN='paste the token here'$env:INFRANEST_TOKEN = 'paste the token here'# 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"}// 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.
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}'$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_sandboxme = requests.get(f"{API}/auth/me", headers=HEADERS).json()
print([(o["id"], o["name"], o["is_sandbox"]) for o in me["user"]["organizations"]])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):
{ "id": 2, "name": "Sandbox", "is_sandbox": true }Keep the ID: export ORG=2 (PowerShell: $env:ORG = 2).
3. Read the sample zones
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}'$zones = Invoke-RestMethod -Uri "https://api.infranest.io/api/organizations/$env:ORG/dns-zones" -Headers $headers
$zones.data | Select-Object id, name, records_countorg = 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])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:
{ "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.
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}'$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 $recordzone = 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())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()){
"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.
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}'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 $recordimport 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())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):
{
"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.
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\"}"$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 $undobatch = 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())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()){
"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.