Warning
This library is under heavy development and is not production ready. The API may change without notice between releases.
Integrate search data into your AI workflow, RAG / fine-tuning, or Zig application using this wrapper for SerpApi.
SerpApi supports Google, Google Maps, Google Shopping, Baidu, Yandex, Yahoo, eBay, App Stores, and more.
Query a vast range of data at scale, including web search results, flight schedules, stock market data, news headlines, and more.
persistent→ Keep socket connection open to save on SSL handshake / reconnection (2x faster).- zero dependency → only the Zig standard library (
std.http,std.json), nothing else to fetch. - cross platform → Linux, macOS, and Windows.
- extensive documentation → easy to follow.
Zig 0.16.0 or higher is required.
Add the dependency to your project:
zig fetch --save 'git+https://github.com/serpapi/serpapi-zig#v1.0.0'Then wire the module in your build.zig:
const serpapi = b.dependency("serpapi", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("serpapi", serpapi.module("serpapi"));Zig has no central package registry, so there is no version range to
resolve: a dependency is a URL plus a content hash. Releases are published
as git tags named vMAJOR.MINOR.PATCH, following
semantic versioning, and the fragment after #
selects which one you get:
# a released version (recommended)
zig fetch --save 'git+https://github.com/serpapi/serpapi-zig#v1.0.0'
# the development branch — moves, may break
zig fetch --save 'git+https://github.com/serpapi/serpapi-zig'
# an exact commit; the SHA must be the full 40 characters
zig fetch --save 'git+https://github.com/serpapi/serpapi-zig#4451c6c4b5a2dc68d7249a2a97f032470ab2070d'Whichever form you use, --save resolves it at fetch time and records the
result as an immutable pin, so a tag that is later moved cannot change your
build:
.dependencies = .{
.serpapi = .{
.url = "git+https://github.com/serpapi/serpapi-zig?ref=v1.0.0#b7b6dbd303fb36b3331fc8265bc28925ba4ca2a5",
.hash = "serpapi-1.0.0-PYmxE0B-AABZru95tORJSVF468kIbL_5Ri3-JC6bZBxT",
},
},The hash — not the URL — is what actually identifies the package, so
always upgrade by re-running zig fetch --save with the new tag, which
rewrites the URL, the resolved commit and the hash together. Editing the
version in the URL by hand does not upgrade anything: the old hash still
resolves to the old package, and the build quietly keeps using it.
A release tarball works too, and produces the same hash as its tag:
zig fetch --save 'https://github.com/serpapi/serpapi-zig/archive/refs/tags/v1.0.0.tar.gz'Released versions are listed on the releases page; changes are recorded in CHANGELOG.md.
Query parameters are plain anonymous structs — field names are parameter names; values may be strings, integers, floats, or booleans.
const std = @import("std");
const serpapi = @import("serpapi");
pub fn main(init: std.process.Init) !void {
const api_key = init.environ_map.get("SERPAPI_KEY") orelse
return error.MissingSerpApiKey;
var client = try serpapi.Client.init(init.gpa, .{
.engine = "google",
.api_key = api_key,
});
defer client.deinit();
var results = try client.search(.{ .q = "coffee" });
defer results.deinit();
std.debug.print("{f}\n", .{std.json.fmt(results.value, .{ .whitespace = .indent_2 })});
}This example runs a search for "coffee" on Google. It returns the results as a
std.json.Parsed(std.json.Value) tree. See the
playground to generate your own query.
A complete, runnable version of this program lives in
oobt/demo.zig — it prints the title and link of every
organic result. Run it against the live API with zig build oobt.
The SerpApi key can be obtained from serpapi.com/signup.
Environment variables are a secure, safe, and easy way to manage secrets:
set export SERPAPI_KEY=<secret_serpapi_key> in your shell, and the example
above reads it with init.environ_map.get("SERPAPI_KEY") — never hardcode
the key in source code.
As everywhere in Zig, the caller owns returned resources: results from JSON
APIs are released with deinit(), raw HTML slices with allocator.free().
The remaining examples omit the defer cleanup lines for brevity.
The serpapi.Client.init constructor takes an allocator and an anonymous
struct. timeout and persistent configure the client; every other field
becomes a default query parameter applied to every request:
var client = try serpapi.Client.init(allocator, .{
.api_key = api_key, // read from the SERPAPI_KEY environment variable
.engine = "google", // default search engine
.gl = "us", // any other field: default query parameter
.timeout = 30, // HTTP timeout in seconds [default: 120]
.persistent = true, // keep the connection open [default: true]
});All fields are optional. Parameters passed to a method call override the
defaults provided to the constructor. Call client.deinit() when the client
is no longer needed; it closes the connection and frees all resources.
var results = try client.search(.{
.q = "coffee",
.location = "Austin, TX, Texas, United States",
});
const organic = results.value.object.get("organic_results").?.array;Following std.json.parseFromSlice, every JSON method has an As variant
that decodes into your own struct instead of a dynamic tree. Unknown JSON
fields are ignored, so declare only what you need:
const Answer = struct {
search_metadata: struct { id: []const u8, status: []const u8 },
};
var results = try client.searchAs(Answer, .{ .q = "coffee" });
std.debug.print("status: {s}\n", .{results.value.search_metadata.status});Also available: locationAs, searchArchiveAs, and accountAs.
html returns the raw HTML page from the search engine. It is useful for
training AI models, RAG, debugging, or when you need to parse the HTML
yourself.
const page = try client.html(.{ .q = "coffee" });var locations = try client.location(.{ .q = "Austin", .limit = 3 });doc: serpapi.com/locations-api
Retrieve a past search by id — the id comes from
results.value.object.get("search_metadata").?.object.get("id").
var archived = try client.searchArchive(search_id);
// or as raw HTML:
const page = try client.searchArchiveHtml(search_id);doc: serpapi.com/search-archive-api
var account = try client.account();The api_key provided to the constructor is used; override it with
client.accountAs(std.json.Value, .{ .api_key = "other key" }).
Methods return a Zig error union. When serpapi.com reports a failure, the
call returns error.SerpApiError and the backend message is available from
client.errorMessage():
const results = client.search(.{}) catch |err| switch (err) {
error.SerpApiError => {
std.debug.print("serpapi.com says: {s}\n", .{client.errorMessage().?});
return err;
},
else => return err,
};Other errors: error.HttpRequestFailed (non-200 status without an error
payload), error.JsonParseError (response was not valid JSON), plus any
network / TLS / allocation errors propagated from the standard library.
Pass async = true to submit a search without blocking on the result, then
fetch it later from the Search Archive API:
var submitted = try client.search(.{ .q = "coffee", .async = true });
const search_id = submitted.value.object.get("search_metadata").?.object.get("id").?.string;
// ... later: poll until search_metadata.status is "Success"
var results = try client.searchArchive(search_id);With persistent = true (the default), the client keeps the TLS connection
to serpapi.com open between requests, which roughly doubles throughput on
repeated searches (measure it yourself with zig build bench):
for (queries) |query| {
var results = try client.search(.{ .q = query });
defer results.deinit();
// process results...
}zig build test # run unit tests (no network)
zig build itest # run integration tests against serpapi.com (needs SERPAPI_KEY)
zig build oobt # out-of-box testing: build + run the demo app (needs SERPAPI_KEY)
zig build bench # benchmark persistent vs non-persistent connections (needs SERPAPI_KEY)
zig build cov # measure code coverage (needs kcov + SERPAPI_KEY)
zig build wasm # build the browser wasm demo into zig-out/demo-wasm/
zig build serve # serve the browser wasm demo at http://127.0.0.1:8080 (needs SERPAPI_KEY)
zig build lint # check formatting (zig fmt --check)
zig build doc # generate API documentation under zig-out/docsA Rakefile wraps the same steps for anyone used to the other SerpApi
libraries — rake --tasks lists them all.
Zig cross-compiles without a toolchain to install, so the library builds for every architecture listed in the cross-compilation guide straight from a laptop:
rake cross # all six architectures
rake cross:aarch64 # or one at a time| architecture | zig target | verified |
|---|---|---|
x86_64 |
x86_64-linux |
ELF 64-bit x86-64 |
arm |
arm-linux |
ELF 32-bit ARM EABI5 |
aarch64 |
aarch64-linux |
ELF 64-bit ARM aarch64 |
i386 |
x86-linux |
ELF 32-bit Intel 80386 |
riscv64 |
riscv64-linux |
ELF 64-bit UCB RISC-V |
wasm32 |
wasm32-wasi |
WebAssembly module |
Note that Zig names the 32-bit x86 architecture x86, not i386.
To run those foreign binaries — and the test suite — on a macOS host,
install QEMU and pass -fqemu:
rake install:qemu # brew install qemu
rake cross:test # zig build test -Dtarget=<triple> -fqemudemo/wasm runs part of the client as WebAssembly inside a web page, with a small native server handling what a browser cannot:
export SERPAPI_KEY=<secret_serpapi_key>
zig build serve
# open http://127.0.0.1:8080See demo/wasm/README.md for how it fits together,
the JS/wasm interface, and why the whole serpapi.Client cannot run in a
browser.
Coverage is measured with kcov, which Zig binaries support out of the box — no instrumentation flags required.
brew install kcov # macOS; on Debian/Ubuntu: sudo apt-get install kcov
export SERPAPI_KEY=<secret_serpapi_key>
zig build cov
open zig-out/coverage/merged/kcov-merged/index.htmlCurrent state: 90.4% of lines covered (225 of 249 in src/client.zig)
without an API key, since the tests covering html, searchArchive, and
account skip themselves rather than fail. With SERPAPI_KEY set — as in
CI, which publishes the figure to every job summary — those paths execute
too and coverage rises accordingly.
The lines that remain uncovered either way are errdefer branches that only
execute if the operating system refuses an allocation midway through a
request.
One caveat specific to Zig: a coverage run over the unit tests alone reports
a flattering ~97% while actually exercising far less, because Zig never
generates code for a generic (anytype) function that no test instantiates
— so every HTTP method disappears from the denominator instead of counting
as uncovered. zig build cov therefore measures the unit and integration
binaries and merges the two reports.
MIT License — see LICENSE.