Engineering

How to Query WHOIS Data in Go Using a REST API (2026 Guide)

September 11, 202610 min readGo · Golang · WHOIS · REST API

Introduction

Go is a natural fit for domain intelligence services: concurrent workers, predictable timeouts, and small static binaries that run in CI or Kubernetes. Security scanners, portfolio monitors and signup-risk services all need structured WHOIS data — registrar, dates, EPP status, nameservers — without maintaining hundreds of registry text parsers.

A REST WHOIS API keeps that complexity out of your Go code. You send a GET request with an API key and decode normalized JSON. This guide shows how to integrate the WhoisJSON WHOIS API into Go — from a reusable net/http client to HTTP 429 retries and a worker pool for bulk domain scans.

Companion guides already cover Python and Node.js. The endpoint and response schema are identical across languages.

Prerequisites

  • Go 1.21+ — examples use the standard library only ( net/http, encoding/json, context).
  • A free WhoisJSON API key sign up here for 1,000 free requests per month. No credit card required.
  • Familiarity with context.Context — used for timeouts and cancellation on every request.

Basic WHOIS Lookup in Go

Authenticate with Authorization: TOKEN=YOUR_API_KEY. The base URL is https://whoisjson.com/api/v1.

whois_lookup.goGo
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "time"
)

const baseURL = "https://whoisjson.com/api/v1"

// Full struct definitions are shown in the next section.
// For a minimal first run, decode into map[string]any instead.

type WhoisClient struct {
    APIKey string
    HTTP   *http.Client
}

func NewWhoisClient(apiKey string) *WhoisClient {
    return &WhoisClient{
        APIKey: apiKey,
        HTTP: &http.Client{
            Timeout: 15 * time.Second,
        },
    }
}

func (c *WhoisClient) Lookup(ctx context.Context, domain string) (map[string]any, error) {
    u, err := url.Parse(baseURL + "/whois")
    if err != nil {
        return nil, err
    }
    q := u.Query()
    q.Set("domain", domain)
    u.RawQuery = q.Encode()

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Authorization", "TOKEN="+c.APIKey)

    res, err := c.HTTP.Do(req)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    body, err := io.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }
    if res.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("whois %s: HTTP %d: %s", domain, res.StatusCode, string(body))
    }

    var data map[string]any
    if err := json.Unmarshal(body, &data); err != nil {
        return nil, err
    }
    return data, nil
}

func main() {
    client := NewWhoisClient(os.Getenv("WHOISJSON_API_KEY"))
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    data, err := client.Lookup(ctx, "example.com")
    if err != nil {
        panic(err)
    }
    fmt.Println(data["name"], data["expires"])
}
Remaining requests: every response includes a Remaining-Requests header. Read it with res.Header.Get("Remaining-Requests") to track quota in your services.

Decode Into Typed Structs

For production code, prefer structs over code map[string]any | . Optional RDAP enrichment fields ( code age | , code expiration | , code statusAnalysis | ) may be absent on some TLDs — use pointers or code omitempty | .

types.goGo
type Registrar struct {
    ID    string `json:"id,omitempty"`
    Name  string `json:"name,omitempty"`
    Email string `json:"email,omitempty"`
    URL   string `json:"url,omitempty"`
    Phone string `json:"phone,omitempty"`
}

type Age struct {
    Days              int  `json:"days"`
    Months            int  `json:"months"`
    Years             int  `json:"years"`
    IsNewlyRegistered bool `json:"isNewlyRegistered"`
    IsYoung           bool `json:"isYoung"`
}

type Expiration struct {
    DaysLeft       int  `json:"daysLeft"`
    IsExpiringSoon bool `json:"isExpiringSoon"`
    IsExpired      bool `json:"isExpired"`
}

type WhoisResponse struct {
    Name       string      `json:"name"`
    Registered bool        `json:"registered"`
    Source     string      `json:"source"`
    Created    string      `json:"created"`
    Changed    string      `json:"changed"`
    Expires    string      `json:"expires"`
    Status     []string    `json:"status"`
    Nameserver []string    `json:"nameserver"`
    Registrar  *Registrar  `json:"registrar,omitempty"`
    Age        *Age        `json:"age,omitempty"`
    Expiration *Expiration `json:"expiration,omitempty"`
}

func (c *WhoisClient) LookupTyped(ctx context.Context, domain string) (*WhoisResponse, error) {
    raw, err := c.Lookup(ctx, domain)
    if err != nil {
        return nil, err
    }
    b, err := json.Marshal(raw)
    if err != nil {
        return nil, err
    }
    var w WhoisResponse
    if err := json.Unmarshal(b, &w); err != nil {
        return nil, err
    }
    return &w, nil
}

For every field in the payload, see the WHOIS JSON field reference. When contacts are missing, follow working with redacted WHOIS.

Safe Field Access

Always nil-check nested objects. Unregistered domains and privacy-protected records omit fields your code might assume exist.

parse.goGo
func summarize(w *WhoisResponse) {
    registrar := "N/A"
    if w.Registrar != nil && w.Registrar.Name != "" {
        registrar = w.Registrar.Name
    }

    fmt.Printf("Domain    : %s\n", w.Name)
    fmt.Printf("Registered: %v\n", w.Registered)
    fmt.Printf("Registrar : %s\n", registrar)
    fmt.Printf("Created   : %s\n", w.Created)
    fmt.Printf("Expires   : %s\n", w.Expires)
    fmt.Printf("Source    : %s\n", w.Source)

    if w.Age != nil {
        fmt.Printf("Age days  : %d (new=%v)\n", w.Age.Days, w.Age.IsNewlyRegistered)
    }
    if w.Expiration != nil {
        fmt.Printf("Days left : %d (soon=%v)\n", w.Expiration.DaysLeft, w.Expiration.IsExpiringSoon)
    }
}

Handle HTTP 429 and Retries

Rate limits return429 Too Many Requests. Treat them as backpressure, not WHOIS data errors. Retry with exponential backoff and a hard attempt limit. Full patterns are documented in the rate limits guide.

retry.goGo
func (c *WhoisClient) LookupWithRetry(ctx context.Context, domain string, maxAttempts int) (*WhoisResponse, error) {
    var lastErr error
    for attempt := 1; attempt <= maxAttempts; attempt++ {
        w, err := c.LookupTyped(ctx, domain)
        if err == nil {
            return w, nil
        }
        lastErr = err
        if !isRetryable(err) || attempt == maxAttempts {
            break
        }
        delay := time.Duration(attempt*attempt) * time.Second
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(delay):
        }
    }
    return nil, lastErr
}

func isRetryable(err error) bool {
    msg := err.Error()
    return strings.Contains(msg, "HTTP 429") ||
        strings.Contains(msg, "HTTP 502") ||
        strings.Contains(msg, "HTTP 503") ||
        strings.Contains(msg, "HTTP 504")
}

Do not retry code 400 | or code 401 | — fix validation or authentication instead.

Bulk Lookups with a Worker Pool

For portfolios or threat feeds, bound concurrency. A small worker pool respects rate limits better than unbounded goroutines.

bulk.goGo
type Result struct {
    Domain string
    Whois  *WhoisResponse
    Err    error
}

func BulkLookup(ctx context.Context, client *WhoisClient, domains []string, workers int) []Result {
    jobs := make(chan string)
    out := make(chan Result, len(domains))

    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for domain := range jobs {
                w, err := client.LookupWithRetry(ctx, domain, 4)
                out <- Result{Domain: domain, Whois: w, Err: err}
            }
        }()
    }

    go func() {
        for _, d := range domains {
            jobs <- d
        }
        close(jobs)
        wg.Wait()
        close(out)
    }()

    results := make([]Result, 0, len(domains))
    for r := range out {
        results = append(results, r)
    }
    return results
}

Start with 3–5 workers on the free tier. Increase carefully on paid plans — see bulk WHOIS lookups for plan selection and pacing.

Use Case: Flag Newly Registered Domains

Use RDAP age.isNewlyRegistered when present. This mirrors the newly registered domains threat-signal guide.

nrd.goGo
func isNewlyRegistered(w *WhoisResponse) bool {
    if w == nil || !w.Registered {
        return false
    }
    if w.Age != nil {
        return w.Age.IsNewlyRegistered
    }
    return false
}

func scanNRD(ctx context.Context, client *WhoisClient, domains []string) {
    for _, r := range BulkLookup(ctx, client, domains, 4) {
        if r.Err != nil {
            fmt.Printf("%s ERROR %v\n", r.Domain, r.Err)
            continue
        }
        if isNewlyRegistered(r.Whois) {
            fmt.Printf("%s NEWLY_REGISTERED age=%dd\n", r.Domain, r.Whois.Age.Days)
        }
    }
}

Limits and Best Practices

  • Always pass a context.Context with a timeout — never rely only on http.Client.Timeout for cancel trees.
  • Store the API key in an environment variable or secret manager, not in source.
  • Reuse one *http.Client (connection pooling) across lookups.
  • Treat empty contacts as normal after GDPR — see redacted WHOIS guidance.
  • Prefer monitoring for continuous drift; use Go clients for pipelines, CI and on-demand enrichment.

Frequently Asked Questions

How do I query WHOIS data in Go?

Send an authenticated GET to https://whoisjson.com/api/v1/whois?domain=example.com with Authorization: TOKEN=YOUR_API_KEY, then decode the JSON response with encoding/json.

Do I need a third-party Go WHOIS library?

No. The standard library (net/http and encoding/json) is enough. A REST API avoids maintaining registrar-specific text parsers in Go.

How should Go code handle HTTP 429 from a WHOIS API?

Retry with exponential backoff and a maximum attempt count. Do not treat 429 as “domain not found”. Read Remaining-Requests when available.

How can I look up many domains concurrently in Go?

Use a fixed-size worker pool of goroutines with a job channel. Bound concurrency to stay within your plan rate limit.

Is there a free tier for Go integrations?

Yes. WhoisJSON includes 1,000 free requests per month across WHOIS, DNS, SSL, availability and related endpoints — no credit card required.

Conclusion

Go and a REST WHOIS API are a strong combination for domain intelligence: typed responses, context-aware timeouts, and controlled concurrency. Start with a small client wrapper, decode into structs, retry 429s carefully, then scale with a worker pool when you need bulk coverage.

The WhoisJSON API handles TLD routing, RDAP fallback, and structured JSON — so your Go code stays focused on workers, timeouts and business logic rather than registry parsers.

Start querying WHOIS data in Go

Create a free account and get 1,000 API requests per month. No credit card required.

Ready to build?

WHOIS, DNS, SSL, subdomain discovery and domain monitoring — one API token, 1,000 free requests every month.