SERP API 4 min read

Build a Go SERP API Client: JSON, Timeouts and Retries

Implement a typed Go SERP client with context cancellation, explicit response validation and bounded HTTP 429 retries. Includes a local test contract.

(Updated: ) • 664 words

A Go SERP client should map the response envelope, use context cancellation, and keep retries within a visible budget. The client below reads SearchCans data.organic into typed results. It treats a missing rank as missing, rather than manufacturing position zero. This is an implementation tutorial, not navigation for an unrelated GoSerp product.

Implement the request and response contract

Create a package named serp. Set Endpoint to https://www.searchcans.com/api/v1/search, read the key from your environment, and supply &http.Client{Timeout:60*time.Second}. Call Search with a context deadline. Keep credentials out of logs.

package serp

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "strconv"
    "time"
)

type Result struct {
    Title string `json:"title"`
    Link string `json:"link"`
    Position *int `json:"position"`
}
type Client struct { Endpoint, Key string; HTTP *http.Client }

func (c Client) Search(ctx context.Context, query string) ([]Result, error) {
    payload, err := json.Marshal(map[string]string{"t":"google", "s":query, "country":"us", "language":"en"})
    if err != nil { return nil, err }
    for attempt := 0; attempt < 2; attempt++ {
        req, err := http.NewRequestWithContext(ctx, "POST", c.Endpoint, bytes.NewReader(payload))
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer " + c.Key)
        req.Header.Set("Content-Type", "application/json")
        response, err := c.HTTP.Do(req)
        if err != nil { return nil, err }
        if response.StatusCode == 429 && attempt == 0 {
            response.Body.Close()
            seconds, err := strconv.Atoi(response.Header.Get("Retry-After"))
            if err != nil || seconds < 0 || seconds > 30 { return nil, fmt.Errorf("429 requires explicit bounded retry policy") }
            timer := time.NewTimer(time.Duration(seconds)*time.Second)
            select { case <-ctx.Done(): timer.Stop(); return nil, ctx.Err(); case <-timer.C: }
            continue
        }
        if response.StatusCode != 200 { response.Body.Close(); return nil, fmt.Errorf("HTTP %d", response.StatusCode) }
        var envelope struct {
            Code *int `json:"code"`
            Data *struct { Organic *[]Result `json:"organic"` } `json:"data"`
        }
        err = json.NewDecoder(response.Body).Decode(&envelope)
        response.Body.Close()
        if err != nil { return nil, err }
        if envelope.Code == nil || *envelope.Code != 0 || envelope.Data == nil || envelope.Data.Organic == nil {
            return nil, fmt.Errorf("business or schema error")
        }
        for _, row := range *envelope.Data.Organic { if row.Link == "" { return nil, fmt.Errorf("result link missing") } }
        return *envelope.Data.Organic, nil
    }
    return nil, fmt.Errorf("retry exhausted")
}

Bound concurrency outside the client

The function handles one search. Run it through a fixed worker pool when you have several queries; begin with three workers only if your account has at least three Parallel Lanes. Do not launch one goroutine per URL without a limit. A concurrency allowance is simultaneous work, not a guaranteed requests-per-second rate. Cancellation should stop queued work as well as in-flight requests.

Test failures with an HTTP test server

Use net/http/httptest to serve a valid envelope, an empty organic list, a malformed data object and a missing result link. Test a nonzero business code independently of HTTP status. For HTTP 429, this implementation accepts a numeric Retry-After from zero to thirty seconds and retries once. An absent, date-form or larger value returns an error for a caller policy to handle. No retry is attempted on other HTTP failures.

A cancelled context must end the request or backoff. Do not count an attempt that failed validation as a usable result. Record query, market, status, attempts and elapsed time without recording the key. The matching tests are local contract tests; they do not demonstrate live throughput or vendor reliability.

For what the fields mean, read the SERP API definition. For system-level source and action policy, use the agent architecture. Actual timing comparisons belong in the benchmark method.

Sources and reproduction

Tags:

SERP API API Development Web Scraping Tutorial Integration
SearchCans Team

SearchCans Team

SERP API & Reader API Experts

The SearchCans engineering team builds high-performance search APIs serving developers worldwide. We share practical tutorials, best practices, and insights on SERP data, web scraping, RAG pipelines, and AI integration.

Ready to build with SearchCans?

Test SERP API and Reader API with 100 free credits. No credit card required.