Integrations

Language SDKs

There is no bespoke SDK to install — the API is plain JSON over HTTPS, so any language with an HTTP client works. Below is the same submit-and-poll pattern in the languages people ask for most.

The pattern

Every integration is the same three steps, regardless of language:

  1. POST /v1/search with your keyword and location → get a job_id.
  2. GET /v1/search/{job_id}?output=serpapi in a loop → 202 while running, 200 with the parsed body when terminal.
  3. Read the results. Back off on 429, retry with backoff on 5xx.

Server-side only

The X-API-Key grants full access to your quota. Call the API from your backend and proxy results to your frontend — never ship the key in browser code, a mobile app, or a public repo. See Authentication.

Prefer a typed client generated from the schema? See OpenAPI & Swagger.

Node.js / TypeScript

Uses the built-in fetch (Node 18+). No dependencies.

typescript
const API_BASE = 'https://serp-api.hoangha.shop'
const API_KEY = process.env.SERP_API_KEY!

async function search(keyword: string, location: string) {
  // 1. submit
  const submit = await fetch(`${API_BASE}/v1/search`, {
    method: 'POST',
    headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ keyword, location }),
  })
  if (!submit.ok) throw new Error(`submit failed: ${submit.status}`)
  const { job_id } = await submit.json()

  // 2. poll
  for (;;) {
    const res = await fetch(`${API_BASE}/v1/search/${job_id}?output=serpapi`, {
      headers: { 'X-API-Key': API_KEY },
    })
    if (res.status === 200) return res.json()        // done / partial
    if (res.status === 202) {                          // still running
      await new Promise((r) => setTimeout(r, 1500))
      continue
    }
    throw new Error(`fetch failed: ${res.status}`)
  }
}

const data = await search('best running shoes', 'Hanoi, Vietnam')
console.log(data.organic_results?.slice(0, 3))

Python

Uses the requests library (pip install requests).

python
import os
import time
import requests

API_BASE = "https://serp-api.hoangha.shop"
API_KEY = os.environ["SERP_API_KEY"]
HEADERS = {"X-API-Key": API_KEY}


def search(keyword: str, location: str) -> dict:
    # 1. submit
    r = requests.post(
        f"{API_BASE}/v1/search",
        headers=HEADERS,
        json={"keyword": keyword, "location": location},
        timeout=30,
    )
    r.raise_for_status()
    job_id = r.json()["job_id"]

    # 2. poll
    while True:
        r = requests.get(
            f"{API_BASE}/v1/search/{job_id}",
            headers=HEADERS,
            params={"output": "serpapi"},
            timeout=30,
        )
        if r.status_code == 200:      # done / partial
            return r.json()
        if r.status_code == 202:      # still running
            time.sleep(1.5)
            continue
        r.raise_for_status()


data = search("best running shoes", "Hanoi, Vietnam")
for row in data.get("organic_results", [])[:3]:
    print(row["position"], row["title"])

PHP

Uses cURL from the standard library.

php
<?php
$API_BASE = 'https://serp-api.hoangha.shop';
$API_KEY  = getenv('SERP_API_KEY');

function serp_call(string $method, string $url, ?array $body = null): array {
    global $API_KEY;
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ['X-API-Key: ' . $API_KEY, 'Content-Type: application/json'],
    ]);
    if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    $res  = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    return ['code' => $code, 'json' => json_decode($res, true)];
}

function search(string $keyword, string $location): array {
    global $API_BASE;
    // 1. submit
    $r = serp_call('POST', "$API_BASE/v1/search", ['keyword' => $keyword, 'location' => $location]);
    $jobId = $r['json']['job_id'];

    // 2. poll
    while (true) {
        $r = serp_call('GET', "$API_BASE/v1/search/$jobId?output=serpapi");
        if ($r['code'] === 200) return $r['json'];   // done / partial
        if ($r['code'] === 202) { sleep(2); continue; } // still running
        throw new Exception("fetch failed: {$r['code']}");
    }
}

$data = search('best running shoes', 'Hanoi, Vietnam');
print_r(array_slice($data['organic_results'] ?? [], 0, 3));

Go

Standard library only.

go
package main

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

const apiBase = "https://serp-api.hoangha.shop"

var apiKey = os.Getenv("SERP_API_KEY")

func do(method, url string, body []byte) (int, []byte, error) {
	req, _ := http.NewRequest(method, url, bytes.NewReader(body))
	req.Header.Set("X-API-Key", apiKey)
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return 0, nil, err
	}
	defer res.Body.Close()
	b, _ := io.ReadAll(res.Body)
	return res.StatusCode, b, nil
}

func search(keyword, location string) (map[string]any, error) {
	// 1. submit
	body, _ := json.Marshal(map[string]string{"keyword": keyword, "location": location})
	_, b, err := do("POST", apiBase+"/v1/search", body)
	if err != nil {
		return nil, err
	}
	var sub struct{ JobID string `json:"job_id"` }
	json.Unmarshal(b, &sub)

	// 2. poll
	for {
		code, b, err := do("GET", apiBase+"/v1/search/"+sub.JobID+"?output=serpapi", nil)
		if err != nil {
			return nil, err
		}
		if code == 200 {
			var out map[string]any
			json.Unmarshal(b, &out)
			return out, nil
		}
		if code == 202 {
			time.Sleep(1500 * time.Millisecond)
			continue
		}
		return nil, fmt.Errorf("fetch failed: %d", code)
	}
}

func main() {
	data, err := search("best running shoes", "Hanoi, Vietnam")
	if err != nil {
		panic(err)
	}
	fmt.Println(data["organic_results"])
}

Ruby

Standard library only.

ruby
require 'net/http'
require 'json'
require 'uri'

API_BASE = 'https://serp-api.hoangha.shop'
API_KEY  = ENV.fetch('SERP_API_KEY')

def serp_request(req, uri)
  req['X-API-Key'] = API_KEY
  req['Content-Type'] = 'application/json'
  Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
end

def search(keyword, location)
  # 1. submit
  uri = URI("#{API_BASE}/v1/search")
  post = Net::HTTP::Post.new(uri)
  post.body = { keyword: keyword, location: location }.to_json
  job_id = JSON.parse(serp_request(post, uri).body)['job_id']

  # 2. poll
  uri = URI("#{API_BASE}/v1/search/#{job_id}?output=serpapi")
  loop do
    res = serp_request(Net::HTTP::Get.new(uri), uri)
    return JSON.parse(res.body) if res.code == '200'   # done / partial
    raise "fetch failed: #{res.code}" unless res.code == '202'
    sleep 1.5                                          # still running
  end
end

data = search('best running shoes', 'Hanoi, Vietnam')
data['organic_results'].first(3).each { |r| puts "#{r['position']} #{r['title']}" }

Other languages

Java, C#, Rust, and everything else

The contract is just two HTTPS endpoints returning JSON, so any language works with its standard HTTP client. Rather than hand-write models for a statically-typed language, generate a client from the schema:

generate a client
# Java, C#, Rust, Kotlin, Swift, Dart, …
openapi-generator-cli generate \
  -i https://serp-api.hoangha.shop/openapi.json \
  -g java \
  -o ./serp-api-client

See OpenAPI & Swagger for the full codegen options and the interactive explorer.

Scaling to batches

To run many keywords, submit them all first (collect the job ids), then poll in parallel — this keeps many crawls in flight instead of serializing on each result. Keep concurrent submissions under your key’s rate_limit_per_min and back off on 429. The Guides page has a worked batch example and caching tips.