Your SaaS lets users create projects, and now you want them to register a custom domain without leaving your product. You're writing Go. You need a registrar API that speaks REST, ships an OpenAPI spec, and doesn't charge per API call. The name.com Core API (v1) checks all three boxes, and net/http is all you need to start making real calls against it.
You can also try generating a typed Go client from the name.com OpenAPI spec using oapi-codegen. Note that oapi-codegen currently targets OpenAPI 3.0, while the name.com spec uses OpenAPI 3.1. Depending on the spec features in use, you may need to down-convert the spec to 3.0 first or apply compatibility workarounds.
Why Go Is a Natural Fit for Domain API Integration
Go's standard library handles the entire HTTP Basic Auth handshake in a single method call (req.SetBasicAuth), which is exactly how the name.com API authenticates every request. You don't need any third-party HTTP client. The net/http package covers it completely. JSON marshaling and unmarshaling maps cleanly to Go structs via encoding/json, so you define your request and response types once and the compiler catches field mismatches at build time.
Goroutines make concurrent multi-TLD availability checks straightforward. If a user types "acme" into your domain search bar and you want to check .com, .io, .co, and .dev in parallel, you fire four goroutines and collect results from a channel. Sequentially, at 200ms per request, four TLDs take 800ms. Concurrently, they take around 200ms.
A function that incorporates the goroutine could look something like this:
func checkMultipleTLDs(client *NameClient, base string, tlds []string) map[string]bool {
type result struct {
tld string
available bool
}
ch := make(chan result, len(tlds))
for _, tld := range tlds {
tld := tld
go func() {
domain := base + "." + tld
res, err := client.CheckAvailability([]string{domain})
if err != nil || len(res.Results) == 0 {
ch <- result{tld: tld, available: false}
return
}
ch <- result{tld: tld, available: res.Results[0].Purchaseable}
}()
}
availability := make(map[string]bool)
for range tlds {
r := <-ch
availability[r.tld] = r.available
}
return availability
}
This function is just an example and won't do anything on its own. Once you have your project set up with all the other functions, you can drop this goroutine in to run multiple TLD checks concurrently.
The name.com Core API speaks pure REST and JSON, which maps directly to Go structs and calls cleanly with net/http. This guide focuses on consuming that API from a Go application.
Prerequisites and Environment Setup
Create a name.com account, then navigate to API > API Token Management to generate two tokens: one for production, one for the sandbox. If your account has two-factor authentication enabled, the API is blocked by default. Toggle "name.com API Access" to on under the same Security settings page before generating tokens.
The two base URLs are:
- Production:
https://api.name.com - Sandbox:
https://api.dev.name.com
The sandbox is API-only with no browser UI, and newly generated sandbox credentials can take up to 15 minutes to activate. For sandbox calls, append -test to your username (so yourcorp becomes yourcorp-test) and pair it with your sandbox token. The sandbox comes with preloaded testing credit. A domain must be registered in the sandbox environment before you can perform operations on it. A GET /core/v1/domains/example.com call returns Not Found unless example.com was first registered through the sandbox.
Three environment variables let you switch between environments by changing values rather than code:
# Sandbox
export NAMECOM_USERNAME="yourcorp-test"
export NAMECOM_TOKEN="your-sandbox-token"
export NAMECOM_BASE_URL="https://api.dev.name.com"
# Production (swap to these when ready)
# export NAMECOM_USERNAME="yourcorp"
# export NAMECOM_TOKEN="your-production-token"
# export NAMECOM_BASE_URL="https://api.name.com"
Initialize your module with go mod init github.com/yourcorp/domain-service. The examples in this guide require only net/http, encoding/json, bytes, fmt, io, os, and time, all from the Go standard library.
Authentication: Building a Reusable Go Client for the name.com API
The name.com API authenticates every request with HTTP Basic Auth. The credentials for SetBasicAuth are your API username and a dedicated API token you generate in Account Settings, separate from your login credentials. The NameClient struct below holds those credentials and the base URL, and the do helper attaches the auth header and Content-Type to every outbound request.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
)
type DomainResult struct {
DomainName string `json:"domainName"`
Purchasable bool `json:"purchasable"`
PurchasePrice float64 `json:"purchasePrice"`
}
type APIError struct {
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
func (e *APIError) Error() string {
if e.Details != "" {
return fmt.Sprintf("name.com API error: %s (%s)", e.Message, e.Details)
}
return fmt.Sprintf("name.com API error: %s", e.Message)
}
func parseAPIError(resp *http.Response) error {
var apiErr APIError
if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
return fmt.Errorf("name.com API: HTTP %d with unreadable body", resp.StatusCode)
}
return &apiErr
}
type NameClient struct {
BaseURL string
Username string
Token string
HTTPClient *http.Client
}
func NewNameClient(username, token, baseURL string) *NameClient {
return &NameClient{
BaseURL: baseURL,
Username: username,
Token: token,
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (c *NameClient) do(method, path string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest(method, c.BaseURL+path, body)
if err != nil {
return nil, err
}
req.SetBasicAuth(c.Username, c.Token)
req.Header.Set("Content-Type", "application/json")
return c.HTTPClient.Do(req)
}
With those structs and functions in place, your program is ready to make its first calls against the name.com API. APIError in particular gets covered more fully later in the guide.
You construct the client from the environment variables you set earlier. This same NewNameClient constructor carries through every example that follows:
client := NewNameClient(
os.Getenv("NAMECOM_USERNAME"),
os.Getenv("NAMECOM_TOKEN"),
os.Getenv("NAMECOM_BASE_URL"),
)
One gotcha documented in the name.com API reference: do not URL-encode the colon (:) character in endpoint paths. The availability check endpoint is literally /core/v1/domains:checkAvailability, and passing it through url.PathEscape will break the request. Build paths as raw strings.
Core Domain Operations with Code Examples
Checking Domain Availability
The availability check endpoint accepts a slice of domain names and returns purchase status and price for each. The purchaseable field tells you whether the domain is available to register.
First, add these supporting structs and functions:
// add the following type statement
type CheckAvailabilityRequest struct {
DomainNames []string `json:"domainNames"`
}
type CheckAvailabilityResponse struct {
Results []DomainResult `json:"results"`
}
// and this function
func (c *NameClient) CheckAvailability(domains []string) (*CheckAvailabilityResponse, error) {
payload, err := json.Marshal(CheckAvailabilityRequest{DomainNames: domains})
if err != nil {
return nil, err
}
// The colon in this path is intentional. Do not URL-encode it.
resp, err := c.do("POST", "/core/v1/domains:checkAvailability", bytes.NewReader(payload))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, parseAPIError(resp)
}
var result CheckAvailabilityResponse
return &result, json.NewDecoder(resp.Body).Decode(&result)
}
Now use this function in your main() entry point:
func main() {
client := NewNameClient(
os.Getenv("NAMECOM_USERNAME"),
os.Getenv("NAMECOM_TOKEN"),
os.Getenv("NAMECOM_BASE_URL"),
)
domains := []string{"amazecorp.com", "amazecorp.net", "amazecorp.io"}
// Call CheckAvailability
result, err := client.CheckAvailability(domains)
if err != nil {
log.Fatalf("Error checking availability: %v", err)
}
// Loop through and print the results
for _, domain := range result.Results {
if domain.Purchasable {
fmt.Printf("%s is available for $%.2f\n", domain.DomainName, domain.PurchasePrice)
} else {
fmt.Printf("%s is not available.\n", domain.DomainName)
}
}
}
Run your program:
go run yourprogram.go
You should see output similar to this:
amazecorp.com is available for $12.99
amazecorp.net is available for $19.99
amazecorp.io is available for $53.99
Those domains are available — in the sandbox environment, at least.
Listing Registered Domains
A typical use case beyond availability checking is listing domains you already own.
The /core/v1/domains endpoint returns a paginated response. These minimal structs capture the fields most applications need: domain name, expiration date, renewal price, and flags for auto-renew, privacy, and lock status.
// add these two structs
type Domain struct {
DomainName string `json:"domainName"`
ExpireDate string `json:"expireDate"`
AutorenewEnabled bool `json:"autorenewEnabled"`
Locked bool `json:"locked"`
PrivacyEnabled bool `json:"privacyEnabled"`
RenewalPrice float64 `json:"renewalPrice"`
}
type ListDomainsResponse struct {
Domains []Domain `json:"domains"`
NextPage int `json:"nextPage"`
LastPage int `json:"lastPage"`
TotalCount int `json:"totalCount"`
}
// add this function
func (c *NameClient) ListDomains() ([]Domain, error) {
var allDomains []Domain
page := 1
for {
resp, err := c.do("GET", fmt.Sprintf("/core/v1/domains?page=%d", page), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, parseAPIError(resp)
}
var result ListDomainsResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
allDomains = append(allDomains, result.Domains...)
if result.NextPage == 0 {
break // no more pages
}
page = result.NextPage
}
return allDomains, nil
}
To use this in main():
func main() {
client := NewNameClient(
os.Getenv("NAMECOM_USERNAME"),
os.Getenv("NAMECOM_TOKEN"),
os.Getenv("NAMECOM_BASE_URL"),
)
domains, err := client.ListDomains()
if err != nil {
log.Fatalf("Error listing domains: %v", err)
}
for _, domain := range domains {
fmt.Printf("%s — expires %s — autorenew: %v — privacyenabled: %v — locked: %v — renewalprice: %v\n", domain.DomainName, domain.ExpireDate, domain.AutorenewEnabled, domain.PrivacyEnabled, domain.Locked, domain.RenewalPrice)
}
}
If you have domains registered, the output looks like this:
myoldstartup.com — expires 2027-03-05T18:04:40Z — autorenew: true — privacyenabled: true — locked: true — renewalprice: 19.99
Registering a Domain
The registration request requires the domain name, the purchase price you expect to pay (the API validates this against the actual price to prevent unexpected charges), and contact information for at least one of the following roles: registrant, admin, tech, or billing.
// add these structs
type Contact struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
CompanyName string `json:"companyName,omitempty"`
Address1 string `json:"address1"`
Address2 string `json:"address2,omitempty"`
City string `json:"city"`
State string `json:"state"`
Zip string `json:"zip"`
Country string `json:"country"`
Phone string `json:"phone"`
Fax string `json:"fax,omitempty"`
Email string `json:"email"`
}
type Contacts struct {
Registrant Contact `json:"registrant"`
Admin Contact `json:"admin"`
Tech Contact `json:"tech"`
Billing Contact `json:"billing"`
}
type RegisterDomainRequest struct {
Domain DomainRegistration `json:"domain"`
PurchasePrice float64 `json:"purchasePrice,omitempty"`
PurchaseType string `json:"purchaseType,omitempty"`
Years int `json:"years,omitempty"`
PromoCode string `json:"promoCode,omitempty"`
}
type DomainRegistration struct {
DomainName string `json:"domainName"`
Contacts Contacts `json:"contacts,omitempty"`
}
type RegisterDomainResponse struct {
Domain Domain `json:"domain"`
Order int `json:"order"`
TotalPaid float64 `json:"totalPaid"`
}
// add this function
func (c *NameClient) RegisterDomain(domainName string, contacts Contacts) (*RegisterDomainResponse, error) {
// Step 1: Check availability and get the real purchase price
avail, err := c.CheckAvailability([]string{domainName})
if err != nil {
return nil, fmt.Errorf("availability check failed: %w", err)
}
if len(avail.Results) == 0 {
return nil, fmt.Errorf("no results returned for %s", domainName)
}
result := avail.Results[0]
if !result.Purchasable {
return nil, fmt.Errorf("%s is not available for purchase", domainName)
}
// Step 2: Register using the price returned by CheckAvailability
req := RegisterDomainRequest{
Domain: DomainRegistration{
DomainName: domainName,
Contacts: contacts,
},
PurchasePrice: result.PurchasePrice,
}
payload, err := json.Marshal(req)
if err != nil {
return nil, err
}
resp, err := c.do("POST", "/core/v1/domains", bytes.NewReader(payload))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, parseAPIError(resp)
}
var regResult RegisterDomainResponse
return ®Result, json.NewDecoder(resp.Body).Decode(®Result)
}
To use this, supply at least one contact and pass it to RegisterDomain:
func main() {
client := NewNameClient(
os.Getenv("NAMECOM_USERNAME"),
os.Getenv("NAMECOM_TOKEN"),
os.Getenv("NAMECOM_BASE_URL"),
)
contact := Contact{
FirstName: "Jane",
LastName: "Smith",
CompanyName: "Your Corp",
Address1: "123 Main Street",
Address2: "Suite 400",
City: "Austin",
State: "TX",
Zip: "78701",
Country: "US",
Phone: "+15551234567",
Email: "jane.smith@yourcorp.com",
}
contacts := Contacts{
Registrant: contact,
Admin: contact,
Tech: contact,
Billing: contact,
}
result, err := client.RegisterDomain("amazecorp.net", contacts)
if err != nil {
log.Fatalf("Error registering domain: %v", err)
}
fmt.Printf("Domain registered! Order #%d, total paid: $%.2f\n", result.Order, result.TotalPaid)
fmt.Printf("Expires: %s\n", result.Domain.ExpireDate)
}
A successful sandbox registration produces output like this:
Domain registered! Order #1502708, total paid: $53.99
Expires: 2027-04-23T05:03:13Z
When running against the sandbox, the domain registers in the test environment with no real transaction. Register the domain in the sandbox first, then make subsequent calls against it.
DNS Record Management with the name.com API
DNS record endpoints follow a predictable path structure: /core/v1/domains/{domainName}/records. The domainName is a path segment, so build the URL with fmt.Sprintf.
Creating a TXT record is one of the most common DNS operations in domain integrations, covering domain ownership verification for services like Google Search Console and SPF record setup. The answer field for a TXT record is the raw text string. The name.com API accepts a minimum TTL of 300 seconds. The priority field is required for MX and SRV records; omit it for A, CNAME, and TXT types.
// add the following structs
type DNSRecord struct {
Host string `json:"host"`
Type string `json:"type"`
Answer string `json:"answer"`
TTL int `json:"ttl"`
Priority int64 `json:"priority,omitempty"`
}
type CreatedRecord struct {
ID int `json:"id"`
DomainName string `json:"domainName"`
Host string `json:"host"`
FQDN string `json:"fqdn"`
Type string `json:"type"`
Answer string `json:"answer"`
TTL int `json:"ttl"`
Priority int64 `json:"priority,omitempty"`
}
// add this function
func (c *NameClient) CreateRecord(domainName string, record DNSRecord) (*CreatedRecord, error) {
payload, err := json.Marshal(record)
if err != nil {
return nil, err
}
path := fmt.Sprintf("/core/v1/domains/%s/records", domainName)
resp, err := c.do("POST", path, bytes.NewReader(payload))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, parseAPIError(resp)
}
var created CreatedRecord
return &created, json.NewDecoder(resp.Body).Decode(&created)
}
A common use case is creating an SPF TXT record for amazecorp.io:
func main() {
client := NewNameClient(
os.Getenv("NAMECOM_USERNAME"),
os.Getenv("NAMECOM_TOKEN"),
os.Getenv("NAMECOM_BASE_URL"),
)
txtRecord, err := client.CreateRecord("amazecorp.net", DNSRecord{
Host: "@",
Type: "TXT",
Answer: "v=spf1 include:yourmailprovider.com ~all",
TTL: 300,
})
if err != nil {
log.Fatalf("Error creating TXT record: %v", err)
}
fmt.Printf("Created TXT record: %s (id: %d)\n", txtRecord.FQDN, txtRecord.ID)
}
The output should look like this:
Created TXT record: amazecorp.net. (id: 13076719)
The ID field in the response is the only handle for referencing that record later, so store it if you plan to delete records programmatically.
Deleting a record follows the same pattern:
// add this function
func (c *NameClient) DeleteRecord(domainName string, recordID int) error {
path := fmt.Sprintf("/core/v1/domains/%s/records/%d", domainName, recordID)
resp, err := c.do("DELETE", path, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
return parseAPIError(resp)
}
return nil
}
With that function added, try deleting the record you just created:
func main() {
client := NewNameClient(
os.Getenv("NAMECOM_USERNAME"),
os.Getenv("NAMECOM_TOKEN"),
os.Getenv("NAMECOM_BASE_URL"),
)
// Use the ID returned when you created the record
err := client.DeleteRecord("amazecorp.net", 13076719)
if err != nil {
log.Fatalf("Error deleting record: %v", err)
}
fmt.Println("Record deleted successfully.")
}
A successful call returns:
Record deleted successfully.
Rate Limits, Error Handling, and Production Readiness
The name.com API enforces two rate limits: 20 requests per second and 3,000 requests per hour. Exceeding either returns 429 Too Many Requests. A retry loop with exponential backoff handles transient 429s without pulling in a third-party rate-limit library.

func (c *NameClient) doWithRetry(method, path string, body []byte) (*http.Response, error) {
const maxRetries = 3
for attempt := 0; attempt < maxRetries; attempt++ {
var bodyReader io.Reader
if body != nil {
bodyReader = bytes.NewReader(body)
}
resp, err := c.do(method, path, bodyReader)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusTooManyRequests {
return resp, nil
}
resp.Body.Close()
waitTime := time.Duration(1<<uint(attempt)) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if secs, err := strconv.Atoi(retryAfter); err == nil {
waitTime = time.Duration(secs) * time.Second
}
}
jitter := time.Duration(rand.Intn(500)) * time.Millisecond
time.Sleep(waitTime + jitter)
}
return nil, fmt.Errorf("name.com API: rate limit exceeded after %d attempts", maxRetries)
}
Double check that you have these extra imports declared to be able to use this function:
import {
"math/rand"
"strconv"
}
In your other functions, replace c.do with c.doWithRetry to pick up the retry behavior.
The name.com API includes both message and details fields in every error response body, alongside the HTTP status code. You've already seen these structs in the earlier examples. The APIError struct decodes both layers so callers get the full error context:
type APIError struct {
Message string `json:"message"`
Details string `json:"details"`
}
func (e *APIError) Error() string {
return fmt.Sprintf("name.com API error: %s (%s)", e.Message, e.Details)
}
func parseAPIError(resp *http.Response) error {
var apiErr APIError
if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
return fmt.Errorf("name.com API: HTTP %d with unreadable body", resp.StatusCode)
}
return &apiErr
}
With APIError implementing the error interface, callers can use errors.As to inspect the structured error while keeping standard Go error handling patterns intact.
Final Thoughts
The name.com API is free to access with no subscription cost. You pay only for domains you actually register. Generate your tokens from the name.com API Settings page, and start building your application for free at name.com.
If you've built domain registration into a Go service, the sandbox credential delay and the unencoded colon in the availability endpoint are the two gotchas most likely to catch you off guard. Both tripped me up the first time.
Comments
Loading comments…