Official typed SDKs for AnyAPI: any API, one wallet, USD, no subscriptions. Reach hundreds of scraping and data APIs through one interface and one key; pay per request in real US dollars.
- TypeScript:
@getanyapi/sdk(npm) - zero runtime deps, ESM + CJS, Node 18+ and edge runtimes. - Python:
getanyapi(PyPI) - httpx + pydantic v2, Python 3.10+, sync and async clients.
Generated per-SKU methods track the platform's /openapi.json; the handwritten account and
discovery clients are maintained separately and must be reviewed when catalog/search/detail
contracts change. See the main repository's ECOSYSTEM.md before changing either surface.
The gateway owns validation, routing, lane order, failover, pricing relationships, health
semantics, and billing. The discovery clients safety-scan customer-facing invariants and
project known fields, but intentionally ignore safe additive fields and do not reimplement
gateway business rules.
TypeScript:
import { AnyAPI } from "@getanyapi/sdk";
const client = new AnyAPI({ apiKey: process.env.ANYAPI_API_KEY });
const res = await client.google.search({ query: "best coffee maker" });
if (res.output.found) console.log(res.output.data, res.costUsd);Python:
from getanyapi import AnyAPI
client = AnyAPI() # reads ANYAPI_API_KEY from the environment
res = client.google.search(query="best coffee maker")
if res.output.found:
print(res.output.data, res.cost_usd)Both packages share one surface; the per-package READMEs (TypeScript, Python) have full detail. The essentials:
- Not found vs error. A success always resolves. Current generated operations wrap the
payload in a
foundflag (output.foundisfalsewhen the upstream had no match, which is not an error).unwrap(res)returns the data or throwsResultNotFoundError(a subclass ofNotFoundError) on an empty result. If a future committed schema uses a bare output,unwrapreturns its data object as-is. - Replays.
res.replayedistruewhen the gateway served a stored response for a repeated idempotency key rather than running the SKU again (a replay is not billed twice). A replay can outlive its stored payload (a 24h expiry, or a payload too large to store); that response carries the run metadata with a nulloutput, andunwrapthrowsAnyAPIErrorrather than hand back an empty payload typed as your data. Re-run without the idempotency key to fetch the data again. - Pagination. Paginated SKUs expose an iterator (
client.reddit.iterSearch(...)/client.reddit.iter_search(...)) that yields items across pages and follows the cursor. Call.pages()on it to walk whole results and read each page'scostUsd/cost_usd. - Request options.
fields,maxItems/max_items, andsummarytrim the response to save context/bandwidth; they do NOT change the price. Per-calltimeout(Ms)and retry overrides live here too. - Errors and retries. Every failure raises an
AnyAPIErrorsubclass mapped from the HTTP status (400/401/402/404/429/502, plusConnection/Timeoutat status 0). Only 429 and proven pre-send network failures retry, with jittered backoff honoringRetry-After; timeouts never retry. For billed TypeScriptrun()calls, Node's built-in undici transport and Bun 1.3.11 expose qualifying proof. Cloudflare Workers, Deno, and browsers do not, so an unknown send phase gets no automatic network retry. Pythonhttpx.ConnectErrorqualifies, while ambiguousReadErrordoes not. DefaultmaxRetries/max_retriesis 2. - Waiting out an in-flight run. A 409 whose code is
idempotency_in_progressmeans the gateway is still executing the run that key claims, so it is the one retryable 4xx: the SDK waits the server'sRetry-Afterin full (past the 8s ordinary ceiling) and returns the replayed result.maxInProgressWaitMs/max_in_progress_wait(default 60s) bounds the total time one call may block on that. Every other 409 raises immediately. - Agent signup.
agentSignup()/agent_signup()bootstraps a capped starter key with no account, for autonomous agents; a human funds it via the returned claim URL.
SPEC.md- the frozen contract (IR shape, naming rules, runtime signatures). Read this before touching anything.openapi.json+catalog.json- committed snapshots of the live gateway. OpenAPI owns generated method schemas; discovery owns category and the completepricing.fromfirst runtime-lane USD offer. Both are verbatim upstream artifacts and the only files exempt from the dash guard.generator/- internal codegen (not published). It reads the two snapshots, emits a deterministicir.json(the normalized intermediate the emitters consume), afixtures.json(one synthetic run-response per SKU, used by the integration tests), and the two generated SDK trees.ir.jsonandfixtures.jsonare committed.packages/typescript/- the@getanyapi/sdkpackage. Handwritten runtime insrc/core/; emitted namespaces, sku-map, and client insrc/generated/.packages/python/- thegetanyapipackage. Handwritten runtime insrc/getanyapi/(_*.pytypes.py); emitted namespaces insrc/getanyapi/platforms/.
openapi.json + catalog.json (committed upstream snapshots)
| generator/src/ir.ts (extractor: envelope crack, naming, USD pricing,
v dash normalization, pagination detection)
ir.json (deterministic, sorted, committed)
| emit-ts.ts / emit-py.ts / fixtures.ts
v
packages/*/src/generated/** + generator/fixtures.json
The emitters read ONLY ir.json, never the raw OpenAPI. During generation, snapshot pricing
is checked for the fields required to document generated methods and every USD amount must be
finite and non-negative rather than silently becoming zero. This build-time snapshot check is
separate from the handwritten tolerant discovery readers.
pnpm generate runs the whole
pipeline; pnpm generate:check regenerates in memory and byte-compares against the
committed output (the CI drift gate). Two runs on the same snapshots are byte-identical.
To refresh from the live gateway: pnpm --filter @anyapi/generator run fetch then
pnpm generate.
pnpm install
pnpm generate # snapshots -> ir.json -> fixtures.json + both generated trees
pnpm check # dash guard + drift check + tsc/vitest/tsup (TS) across packagesPython side (its gates run in CI separately from pnpm check):
pip install -e "packages/python[dev]"
cd packages/python && pyright && mypy && pytestThe deterministic tests are mock-only, while required CI also runs credentialless live
catalog/search/detail canaries through both handwritten readers. The published-artifact smoke
adds registry and paid-run coverage. It installs BOTH SDKs FROM the registries (npm + PyPI)
into throwaway temp dirs outside the repo, mints an ephemeral capped key via the public
/agent/signup endpoint (no secrets needed), exercises catalog, search, and describe, then
makes one real production call through each. Together these catch discovery drift as well as
packaging bugs, missing files, broken exports or types, that source-tree tests cannot see.
bash scripts/smoke.sh # smoke the latest published version
VERSION=0.7.0 bash scripts/smoke.shIt exits nonzero if either SDK fails and prints a per-SDK PASS/FAIL summary. In CI it is a
standalone workflow (.github/workflows/smoke.yml) that runs nightly (08:00 UTC) and on manual
dispatch (with an optional version input). Release recovery also smokes both exact versions
before creating the GitHub Release. The smoke is not wired into ci.yml or branch protection,
so registry or upstream flakiness never blocks a PR.
Releases are automated from the live catalog. Two workflows drive it:
-
.github/workflows/regen.yml(auto-bump) runs on arepository_dispatchof typesdk-refresh(the AnyAPI monorepo fires this after each gateway deploy), a nightly cron fallback, and manual dispatch. It fetches the liveopenapi.json+catalog.json, regenerates the whole tree, and classifies the IR diff:- a new SKU, optional field, enum member, or platform -> minor;
- proven documentation, pricing, or emitter-neutral metadata changes -> patch;
- byte-identical IR, fixtures, and both emitted trees -> none (no commit, loop-safe);
- removals, contract changes, or unexplained generated output -> blocked.
The workflow first proves that the committed generated trees match the old IR. Public generated changes must then appear in both language trees, and the post-refresh drift check proves that both match the new IR. A blocked run uploads the old/new IR, release notes, and generated diff, then fails before versioning, committing, or tagging. On patch or minor it applies the version to BOTH
packages/typescript/package.jsonandpackages/python/pyproject.tomlin lockstep, commits the regenerated tree, tagsv<X.Y.Z>, pushes, and dispatchesrelease.yml. The change summary is the commit body and becomes the GitHub Release notes. -
.github/workflows/release.yml(publish) runs on av*tag push and on manual dispatch (taginput).verifyre-runs the full gate suite and asserts the tag matches both manifests. It queries the exact version on npm and PyPI, skips a side already present, publishes a missing side, then re-queries both and smokes both exact versions. When PyPI was missing at preflight, terminal verification first waits its documented 60-second default cache TTL so the pre-publish 404 cannot mask the upload. Only then does it create the GitHub Release. This makes rerunning the same tag a recovery operation instead of attempting to republish an immutable version.
For a coordinated discovery cut, bump both manifests in lockstep, merge the SDK change, then
tag that exact main commit. release.yml verifies the full suite and version match before
repairing either missing registry side and smoking both exact versions. Follow the
cross-repository release order in the main
repository's ECOSYSTEM.md; do not infer it from generator automation.
The npm publish works immediately (the NPM_TOKEN secret is set on the repo). The PyPI
publish requires the trusted publisher to exist first (see next).
Trusted publishing has no API token; it authorizes this repo's workflow via OIDC. Until it is
configured, publish-pypi fails with a clear "trusted publisher not configured" message
(that is expected, and we do NOT fall back to a token). Configure it once on PyPI:
-
For a project that does not exist yet, add a pending publisher at https://pypi.org/manage/account/publishing/; for an existing project use its Settings -> Publishing page. Fill in:
- PyPI Project Name:
getanyapi - Owner:
getanyapi-com - Repository name:
sdks - Workflow name:
release.yml - Environment name:
pypi
These must match
release.ymlexactly (thepublish-pypijob runs inenvironment: pypi). Re-run the failedpublish-pypijob (or the whole release) once the publisher is saved. - PyPI Project Name:
- Prices are always USD. Credits (an internal unit) never appear in the SDK.
- The provider is always
"AnyAPI". Upstream backends are never named. - Ranked queries use
search;catalogis category-only browsing. - ASCII hyphen only: no em dashes or en dashes anywhere except the upstream
openapi.jsonsnapshot.
Generated code is committed and carries a Generated - do not edit header. Edit the
generator, not the output.
MIT