Manage DNS with the Go SDK
Quick answer
Use Go 1.25+, install github.com/dnscaleou/dnscale-go, and set DNSCALE_API_KEY. Pass a context to zone and record operations, use iterators for pagination, and retain the record ID returned after each update. Safe reads have bounded retries; DNS changes are never automatically retried.
On this page
What you'll learn
- Install the Go SDK and configure a scoped API key
- List zones and records using pagination iterators
- Create a zone and exercise a test record lifecycle
- Control request deadlines and handle API and transport errors
- Use the generated client for the wider API
The DNScale Go SDK lets you manage DNS from Go services, command-line tools, and background jobs. This guide starts with a read-only inventory, then introduces zone creation, record changes, deadlines, and error handling.
The module is github.com/dnscaleou/dnscale-go. Find the source and issue tracker in dnscaleou/dnscale-go.
Install and authenticate
You need Go 1.25 or newer, a DNScale account, and an API key. For a new example project:
mkdir dnscale-go-example
cd dnscale-go-example
go mod init example.com/dnscale-go-example
go get github.com/dnscaleou/dnscale-goIn an existing Go module, run only the go get command. Commit go.mod and go.sum with your application to retain the selected dependency version.
Create a key in the dashboard and supply it through DNSCALE_API_KEY using your secret manager or local environment. dnscale.New(dnscale.Options{}) reads that variable automatically. You can also pass Options.APIKey from your application's secret store.
| Task | API scopes |
|---|---|
| List or read zones | zones:read |
| List or read records | zones:read, records:read |
| Create, update, or delete records | zones:read, records:write |
| Create, update, or delete zones | zones:read, zones:write |
| Read account usage | usage:read |
Zone and record-name restrictions still apply. Restrict existing-zone automation to the zones and names it needs. Creating a new zone needs appropriate zone-creation permissions. See API authentication and scopes.
The default endpoint is https://api.dnscale.eu/v1. If you set Options.BaseURL for another environment, include /v1. Keep credentials out of source code and logs.
List zones and records
Save this read-only program as main.go and run it with go run .. The key needs zones:read and records:read.
package main
import (
"context"
"fmt"
"log"
"time"
dnscale "github.com/dnscaleou/dnscale-go"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
dns, err := dnscale.New(dnscale.Options{})
if err != nil {
return err
}
defer dns.Close()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
for zone, err := range dns.Zones.Iter(ctx, 100) {
if err != nil {
return fmt.Errorf("list zones: %w", err)
}
fmt.Println("Zone:", zone.Id, zone.Name)
for record, err := range dns.Records.Iter(ctx, zone.Id.String(), 100) {
if err != nil {
return fmt.Errorf("list records for %s: %w", zone.Name, err)
}
fmt.Println(record.Id, record.Name, record.Type, record.Content)
}
}
return nil
}Iter fetches more pages as you consume them. Both iterators accept page sizes from 1 to 100; check the error before using each item. Breaking out of a loop stops additional requests. dns.Close() releases idle HTTP connections when run returns, including on error.
Zone IDs are UUID values in responses; pass zone.Id.String() to helpers that expect a zone ID. Record IDs are opaque strings. Generated fields use names such as Id and Ttl.
List returns just one page. Inside run, after creating dns and ctx, you can inspect a page like this:
page, err := dns.Zones.List(ctx, dnscale.PageOptions{Limit: 25, Offset: 0})
if err != nil {
return err
}
fmt.Println("Total zones:", page.Pagination.Total)
for _, zone := range page.Zones {
fmt.Println(zone.Id, zone.Name)
}Pagination is not an atomic snapshot. A listing can change while another process creates or removes resources, so do not treat a concurrent inventory as a consistent backup.
Create a zone
Skip this step if you already have a test zone. In the run function above, replace the inventory loop with the following block, keeping the final return nil. Replace example.com with a domain or test subdomain you control and do not already manage in DNScale.
zone, err := dns.Zones.Create(ctx, dnscale.CreateZoneRequest{
Name: "example.com",
})
if err != nil {
return err
}
fmt.Println("Zone ID:", zone.Id)
fmt.Println("Zone name:", zone.Name)This creates a real zone and is subject to your plan's limits. New zones default to the API's master type, meaning a primary zone. It does not change your registrar's nameserver delegation. See the zone API reference for region and other options.
Copy the zone's UUID from this output, the inventory, or the dashboard, and set it for the next example:
export DNSCALE_ZONE_ID="replace-with-your-test-zone-uuid"Create, update, and delete a test record
This program changes DNS. Use a test zone with an unused _sdk-go-demo TXT name and a key with zones:read, records:read, and records:write. The values below are demonstration data.
Replace main.go with this program and run go run .. It creates one record, updates it, reads it back, and deletes the updated record. It leaves the zone in place.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
dnscale "github.com/dnscaleou/dnscale-go"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
zoneID := os.Getenv("DNSCALE_ZONE_ID")
if zoneID == "" {
return fmt.Errorf("set DNSCALE_ZONE_ID to your test zone UUID")
}
dns, err := dnscale.New(dnscale.Options{})
if err != nil {
return err
}
defer dns.Close()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
created, err := dns.Records.Create(ctx, zoneID, dnscale.CreateRecordRequest{
Name: "_sdk-go-demo",
Type: "TXT",
Content: "sdk-demo-first",
Ttl: dnscale.Ptr(300),
})
if err != nil {
return fmt.Errorf("create record: %w", err)
}
fmt.Println("Created:", created.Id)
updated, err := dns.Records.Update(ctx, zoneID, created.Id, dnscale.CreateRecordRequest{
Name: created.Name,
Type: created.Type,
Content: "sdk-demo-second",
Ttl: dnscale.Ptr(300),
})
if err != nil {
return fmt.Errorf("update record: %w", err)
}
fmt.Println("Updated:", updated.Id)
current, err := dns.Records.Get(ctx, zoneID, updated.Id)
if err != nil {
return fmt.Errorf("read record: %w", err)
}
fmt.Println(current.Name, current.Content)
if err := dns.Records.Delete(ctx, zoneID, updated.Id); err != nil {
return fmt.Errorf("delete record: %w", err)
}
fmt.Println("Deleted the demo record")
return nil
}Keep the ID returned by the update. Record IDs are content-derived, so later reads and deletes must use updated.Id. The ID-based update sends a full record body, not a partial patch; include the intended name, type, content, and optional values.
If the program stops after a successful write, the demo record may remain. Inspect the test zone before rerunning and remove that specific value if needed. A timeout does not prove that the server rejected the change.
Records.UpdateByName and Records.DeleteByName are also available. For DeleteByName, a non-nil content pointer selects a value; nil deletes the entire RRset with that name and type. See the record API reference before using these helpers on shared names.
Optional values and record comments
Optional request fields often use pointers. dnscale.Ptr(300) supplies a TTL; dnscale.Ptr(false) sends an explicit false value instead of omitting the field.
Nullable fields such as Comment use github.com/oapi-codegen/nullable. Its zero value omits the field, nullable.NewNullableWithValue("note") sends a value, and nullable.NewNullNullable[string]() sends JSON null. To remove a record comment, send an empty string with nullable.NewNullableWithValue(""). See shared RRset comments for the API's comment behaviour.
Control deadlines and retries
Every operation accepts a context. Reuse your service's request context, or set a deadline for a complete job as the examples do. The client also has a default ten-second timeout per HTTP request, including retries; a shorter context deadline takes precedence.
To change the request timeout and disable automatic retries, replace the constructor in either complete example with:
dns, err := dnscale.New(dnscale.Options{
Timeout: 15 * time.Second,
MaxRetries: dnscale.Ptr(0),
})Keep the existing error check and defer dns.Close() after this constructor. An omitted MaxRetries allows up to two retries for transient failures on bodyless GET, HEAD, and OPTIONS requests. Retryable HTTP statuses are 408, 429, 500, 502, 503, and 504. Mutations are never automatically retried.
The SDK honours numeric and HTTP-date Retry-After values within a 30-second waiting budget. A longer requested wait surfaces the response instead of retrying too early. Context cancellation interrupts retry waits, and a request's timeout can end the operation sooner.
Handle API and transport errors
Use errors.As for *dnscale.APIError, *dnscale.RateLimitError, and *dnscale.ProtocolError. Rate-limit errors unwrap to API errors, so test for the more specific type first. errors.Is identifies context cancellation and deadline errors.
Add "errors" to the imports of either complete program, then replace its main function with this version:
func main() {
if err := run(); err != nil {
var rateLimit *dnscale.RateLimitError
var apiError *dnscale.APIError
var protocolError *dnscale.ProtocolError
switch {
case errors.As(err, &rateLimit):
log.Printf("rate limited: retry-after=%q request-id=%s",
rateLimit.RetryAfter, rateLimit.RequestID)
case errors.As(err, &apiError):
log.Printf("API error: status=%d code=%s message=%s request-id=%s",
apiError.StatusCode, apiError.Code, apiError.Message, apiError.RequestID)
case errors.Is(err, context.DeadlineExceeded):
log.Print("request deadline exceeded")
case errors.Is(err, context.Canceled):
log.Print("request cancelled")
case errors.As(err, &protocolError):
log.Printf("unexpected API response: %s", protocolError.Message)
default:
log.Printf("request or configuration error: %v", err)
}
log.Fatal("DNS operation did not complete; inspect state before repeating a write")
}
}RetryAfter is the original header string, not necessarily a number. API errors include a request ID when the server provides one. Network errors remain standard Go errors; missing credentials, invalid zone IDs, or invalid options can fail locally before a request is sent.
Access the wider API
dns.API exposes generated operations for the rest of the public contract, including DNSSEC, usage, billing, and users. Calls through this client share the SDK's authentication, timeout, retry, and API error handling.
For example, with usage:read permission, replace the inventory loop in the first program with this block, keeping its final return nil:
result, err := dns.API.GetCurrentUsageWithResponse(ctx)
if err != nil {
return err
}
if result.JSON200 == nil || result.JSON200.Status != "success" {
return fmt.Errorf("unexpected usage response: HTTP %d", result.StatusCode())
}
fmt.Printf("Usage: %+v\n", result.JSON200.Data)Check the typed response before dereferencing it. Models and generated request types live in github.com/dnscaleou/dnscale-go/api. Constructing api.NewClientWithResponses directly bypasses the SDK's configured authentication and transport policies; use dns.API when you want those shared behaviours.
Choose the right automation tool
Use the Go SDK for application-driven records, tenant onboarding, inventory tools, and procedural jobs. It does not provide desired-state reconciliation, dry-run plans, or automatic rollback.
- Terraform fits DNS managed alongside infrastructure state.
- DNSControl fits DNS configuration repositories.
- The Python SDK provides synchronous and asynchronous Python clients.
- DNS automation covers ownership and deployment patterns.
- The API reference documents endpoint permissions and request details.
Keep one routine owner for each record set so application code and infrastructure tools do not overwrite each other's changes.
Frequently asked questions
- What is the DNScale Go module path?
- Install github.com/dnscaleou/dnscale-go and import it as dnscale. Generated models and endpoint clients are in github.com/dnscaleou/dnscale-go/api.
- Which Go versions does the SDK support?
- The SDK requires Go 1.25 or newer.
- How do I paginate through zones and records?
- Use Zones.Iter(ctx, pageSize) or Records.Iter(ctx, zoneID, pageSize) in a for range loop. Both iterators accept page sizes from 1 to 100 and return a resource and an error. Check the error on every iteration.
- Does the SDK automatically retry DNS changes?
- No. Automatic retries are limited to safe reads. After a failed or timed-out write, inspect the current state before repeating the operation.
- Why did a record ID change after an update?
- DNScale record IDs are content-derived. Use the ID returned by the update for subsequent reads, updates, or deletion. Treat record IDs as opaque strings.
- Does creating a zone change my domain's nameservers?
- No. Creating a zone changes your DNScale account. Registrar delegation is a separate step, so you can test API operations without changing the nameservers of a live domain.
Put the guide into practice
Manage authoritative DNS records, review changes, and monitor your zones with DNScale.
Start free