Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.DS_Store
.env
66 changes: 51 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,29 @@ results, err := client.Search(parameter)
fmt.Println(results)
```

This example runs a search for "coffee" on Google. It then returns the results a Go map.
See the [playground](https://serpapi.com/playground) to generate your own code.
This example runs a search for "coffee" on Google and returns the results as a Go map.

### Response formats

Use `Search` for structured JSON results decoded into a Go map:

```golang
results, err := client.Search(parameter)
```

Use `Md` for a token-efficient Markdown string optimized for LLMs and AI agents:

```golang
markdown, err := client.Md(parameter)
```

Use `Html` when you need the raw search-engine response:

```golang
rawHTML, err := client.Html(parameter)
```

Learn more about [SerpApi Markdown output](https://serpapi.com/markdown-output). See the [playground](https://serpapi.com/playground) to generate your own code.

## Advanced Usage
### Search API
Expand Down Expand Up @@ -69,6 +90,13 @@ func main() {
}
fmt.Println(rsp)

// token-efficient Markdown as a string
markdown, err := client.Md(parameter)
if err != nil {
panic(err)
}
fmt.Println(*markdown)

// raw search engine html as a String
// serpapi.com acts a proxy to provive high throughputs, no search limit and more.
raw_html, err := client.Html(parameter)
Expand Down Expand Up @@ -121,42 +149,45 @@ To fetch earlier results from the search_id.
First, you need to run a search and save the search id.
```golang
// First, you need to run a search and save the search id.
auth := map[string]string{
"engine": "google",
"api_key": "secret_api_key",
}
client := serpapi.NewClient(auth)
setting := serpapi.NewSerpApiClientSetting("secret_api_key")
setting.Engine = "google"
client := serpapi.NewClient(setting)
parameter := map[string]string{
"q": "Coffee",
"location": "Portland"}

rsp, err := client.Search(parameter)

if err != nil {
t.Error("unexpected error", err)
return
panic(err)
}

// Now let's retrieve the previous search results from the archive.
searchID := rsp["search_metadata"].(map[string]interface{})["id"].(string)
if len(searchID) == 0 {
t.Error("search_metadata.id must be defined")
return
panic("search_metadata.id must be defined")
}

searchArchive, err := client.SearchArchive(searchID)
if err != nil {
t.Error(err)
return
panic(err)
}

searchIDArchive := searchArchive["search_metadata"].(map[string]interface{})["id"].(string)
if searchIDArchive != searchID {
t.Error("search_metadata.id do not match", searchIDArchive, searchID)
panic("search_metadata.id does not match")
}
```

This code prints the search results from the archive. :)
This code prints the JSON search results from the archive. Archived results are also available as Markdown:

```golang
markdown, err := client.SearchArchiveMd(searchID)
if err != nil {
panic(err)
}
fmt.Println(*markdown)
```

### Account API
```golang
Expand Down Expand Up @@ -2005,6 +2036,9 @@ Go versions validated by Github Actions:
* see: [Github Actions.](https://github.com/serpapi/serpapi-golang/actions/workflows/ci.yml)

## Change logs
* [2026-09-01] 1.2.0 Markdown Output Support
- Add Markdown search and archive responses
- Decode JSON API errors returned for Markdown requests
* [2026-01-26] 1.1.0 Asynchronous & Persistent Mode Support
- Major features (async/persistent mode, API key handling, client configuration)
- New test examples
Expand Down Expand Up @@ -2046,9 +2080,11 @@ classDiagram
api_key String
params Map
search() Map
md() String
html() String
location() String
search_archive() Map
search_archive_md() String
account() Map
}
net/http <.. Client
Expand Down
66 changes: 51 additions & 15 deletions README.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,29 @@ results, err := client.Search(parameter)
fmt.Println(results)
```

This example runs a search for "coffee" on Google. It then returns the results a Go map.
See the [playground](https://serpapi.com/playground) to generate your own code.
This example runs a search for "coffee" on Google and returns the results as a Go map.

### Response formats

Use `Search` for structured JSON results decoded into a Go map:

```golang
results, err := client.Search(parameter)
```

Use `Md` for a token-efficient Markdown string optimized for LLMs and AI agents:

```golang
markdown, err := client.Md(parameter)
```

Use `Html` when you need the raw search-engine response:

```golang
rawHTML, err := client.Html(parameter)
```

Learn more about [SerpApi Markdown output](https://serpapi.com/markdown-output). See the [playground](https://serpapi.com/playground) to generate your own code.

## Advanced Usage
### Search API
Expand Down Expand Up @@ -92,6 +113,13 @@ func main() {
}
fmt.Println(rsp)

// token-efficient Markdown as a string
markdown, err := client.Md(parameter)
if err != nil {
panic(err)
}
fmt.Println(*markdown)

// raw search engine html as a String
// serpapi.com acts a proxy to provive high throughputs, no search limit and more.
raw_html, err := client.Html(parameter)
Expand Down Expand Up @@ -144,42 +172,45 @@ To fetch earlier results from the search_id.
First, you need to run a search and save the search id.
```golang
// First, you need to run a search and save the search id.
auth := map[string]string{
"engine": "google",
"api_key": "secret_api_key",
}
client := serpapi.NewClient(auth)
setting := serpapi.NewSerpApiClientSetting("secret_api_key")
setting.Engine = "google"
client := serpapi.NewClient(setting)
parameter := map[string]string{
"q": "Coffee",
"location": "Portland"}

rsp, err := client.Search(parameter)

if err != nil {
t.Error("unexpected error", err)
return
panic(err)
}

// Now let's retrieve the previous search results from the archive.
searchID := rsp["search_metadata"].(map[string]interface{})["id"].(string)
if len(searchID) == 0 {
t.Error("search_metadata.id must be defined")
return
panic("search_metadata.id must be defined")
}

searchArchive, err := client.SearchArchive(searchID)
if err != nil {
t.Error(err)
return
panic(err)
}

searchIDArchive := searchArchive["search_metadata"].(map[string]interface{})["id"].(string)
if searchIDArchive != searchID {
t.Error("search_metadata.id do not match", searchIDArchive, searchID)
panic("search_metadata.id does not match")
}
```

This code prints the search results from the archive. :)
This code prints the JSON search results from the archive. Archived results are also available as Markdown:

```golang
markdown, err := client.SearchArchiveMd(searchID)
if err != nil {
panic(err)
}
fmt.Println(*markdown)
```

### Account API
```golang
Expand Down Expand Up @@ -366,6 +397,9 @@ Go versions validated by Github Actions:
* see: [Github Actions.](https://github.com/serpapi/serpapi-golang/actions/workflows/ci.yml)

## Change logs
* [2026-09-01] 1.2.0 Markdown Output Support
- Add Markdown search and archive responses
- Decode JSON API errors returned for Markdown requests
* [2026-01-26] 1.1.0 Asynchronous & Persistent Mode Support
- Major features (async/persistent mode, API key handling, client configuration)
- New test examples
Expand Down Expand Up @@ -407,9 +441,11 @@ classDiagram
api_key String
params Map
search() Map
md() String
html() String
location() String
search_archive() Map
search_archive_md() String
account() Map
}
net/http <.. Client
Expand Down
64 changes: 58 additions & 6 deletions serpapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"strings"
"time"
)

const (
VERSION = "1.1.0"
VERSION = "1.2.0"
BaseURL = "https://serpapi.com"
DefaultTimeout = 60 * time.Second
)
Expand Down Expand Up @@ -79,7 +81,17 @@ func (client *SerpApiClient) Html(parameter map[string]string) (*string, error)
return nil, err
}
defer rsp.Body.Close()
return client.decodeHTML(rsp.Body)
return client.decodeText(rsp.Body)
}

// Md returns search results formatted as Markdown
func (client *SerpApiClient) Md(parameter map[string]string) (*string, error) {
rsp, err := client.execute("/search", "md", parameter)
if err != nil {
return nil, err
}
defer rsp.Body.Close()
return client.decodeTextResponse(rsp)
}

// Location returns standardized location data
Expand Down Expand Up @@ -116,6 +128,16 @@ func (client *SerpApiClient) SearchArchive(id string) (map[string]interface{}, e
return client.decodeJSON(rsp.Body)
}

// SearchArchiveMd retrieves previous search results formatted as Markdown
func (client *SerpApiClient) SearchArchiveMd(id string) (*string, error) {
rsp, err := client.execute("/searches/"+id+".md", "md", map[string]string{})
if err != nil {
return nil, err
}
defer rsp.Body.Close()
return client.decodeTextResponse(rsp)
}

// decodeJSON decodes response body to a map
func (client *SerpApiClient) decodeJSON(body io.ReadCloser) (map[string]interface{}, error) {
defer body.Close()
Expand All @@ -141,9 +163,8 @@ func (client *SerpApiClient) decodeJSONArray(body io.ReadCloser) ([]interface{},
return rsp, nil
}

// decodeHTML decodes response body to an HTML string
func (client *SerpApiClient) decodeHTML(body io.ReadCloser) (*string, error) {
defer body.Close()
// decodeText decodes a response body to a string.
func (client *SerpApiClient) decodeText(body io.Reader) (*string, error) {
buffer, err := io.ReadAll(body)
if err != nil {
return nil, err
Expand All @@ -152,6 +173,37 @@ func (client *SerpApiClient) decodeHTML(body io.ReadCloser) (*string, error) {
return &text, nil
}

// decodeTextResponse decodes Markdown responses to a string. SerpApi returns
// API errors as JSON even when Markdown output was requested.
func (client *SerpApiClient) decodeTextResponse(rsp *http.Response) (*string, error) {
buffer, err := io.ReadAll(rsp.Body)
if err != nil {
return nil, err
}

mediaType, _, _ := mime.ParseMediaType(rsp.Header.Get("Content-Type"))
mediaType = strings.ToLower(mediaType)
isJSON := mediaType == "application/json" || strings.HasSuffix(mediaType, "+json")
if isJSON {
var data map[string]interface{}
if err := json.Unmarshal(buffer, &data); err == nil {
if errorMessage, exists := data["error"].(string); exists {
return nil, errors.New(errorMessage)
}
}
}

if rsp.StatusCode < http.StatusOK || rsp.StatusCode >= http.StatusMultipleChoices {
return nil, fmt.Errorf("HTTP request failed with status: %d", rsp.StatusCode)
}
if isJSON {
return nil, errors.New("expected a Markdown response, received JSON")
}

text := string(buffer)
return &text, nil
}

// execute sends an HTTP GET request and returns the response
func (client *SerpApiClient) execute(path string, output string, parameter map[string]string) (*http.Response, error) {
query := url.Values{}
Expand All @@ -177,7 +229,7 @@ func (client *SerpApiClient) execute(path string, output string, parameter map[s
query.Add("async", "true")
}
query.Add("source", "go:"+VERSION)
query.Add("output", output)
query.Set("output", output)

endpoint := BaseURL + path + "?" + query.Encode()
rsp, err := client.HttpSearch.Get(endpoint)
Expand Down
Loading
Loading