# Public API Integration

> Use when an interface needs live third-party data and no API is chosen yet: selecting from public-apis, probing it, and wiring it safely.

- Skill: `yogvidwankhede/public-api-integration` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add yogvidwankhede/public-api-integration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yogvidwankhede/public-api-integration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: yogvidwankhede (https://skillmd.com/u/yogvidwankhede)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/yogvidwankhede/public-api-integration

---


<!--
  Generated by Vishwakarma. Do not edit this file directly.
  Edit the source skill and run `vishwakarma sync` to regenerate.
-->

# Public API Integration

The source catalog is `github.com/public-apis/public-apis` — a Markdown table at
`README.md`, fetchable raw from
`raw.githubusercontent.com/public-apis/public-apis/master/README.md`. **1,683 entries,
52 categories, five columns: API, Description, Auth, HTTPS, CORS.**

There is no rate-limit column, no uptime column, no licence column, and no
last-verified column. Reading a row and writing a fetch call against it assumes all
four. Treat the table as a *shortlist generator*, never as an integration spec.

> The old `api.publicapis.org` JSON service is unreliable and should not be depended on.
> Parse the Markdown; it is the artefact the project actually maintains.

---

## 1. What the columns actually say

| Column | Values in the table | What it means for you |
|---|---|---|
| **Auth** | `No` (782) · `apiKey` (728) · `OAuth` (150) | `No` = usable immediately. `apiKey` = signup + a secret. `OAuth` = a full consent flow; rarely worth it for read-only display data. |
| **HTTPS** | Yes (1,580) · No (92) | `No` is disqualifying. Not a preference — a plaintext dependency in a TLS page is a mixed-content block and a tampering surface. |
| **CORS** | Unknown (977) · Yes (545) · No (150) | `Unknown` means *nobody checked*, not "probably fine". |

**Only 297 of 1,683 entries are simultaneously no-auth, HTTPS, and CORS `Yes`** — under
one in five. That single number decides the architecture below.

Parsing note: rows are not perfectly uniform. Some carry trailing pipes, a few wrap the
Auth value in backticks or embed a Postman button in the cell. Match on
`^\|\s*\[(name)\]\((url)\)\s*\|` and split the remainder on `|`, then normalise —
strip backticks, trim, and treat anything unrecognised as `Unknown`.

---

## 2. The pipeline

**Frame → Shortlist → Probe → Architect → Adapt → Degrade → Record.**

### Frame
Write one sentence: *what field does the interface render, at what freshness, for how many
users?* "A weather API" is not a requirement. "Current temperature and condition icon for
one city, refreshed every 10 minutes, on a page with maybe 200 daily views" is — and it
rules out three quarters of the candidates immediately.

### Shortlist
Fetch the raw README, filter to the category, then rank: HTTPS `Yes` is a hard filter;
Auth `No` beats `apiKey` beats `OAuth`; CORS `Yes` beats `Unknown` beats `No`. Keep
**three** candidates, not one. The top choice fails the probe often enough that having a
second costs nothing now and saves a round trip later.

### Probe — the step that is always skipped
Before writing a line of integration code, call the real endpoint:

```bash
curl -s -o /tmp/probe.json -w '%{http_code} %{time_total}s %{size_download}B\n' \
  'https://api.example.com/v1/thing?q=test'
curl -sI 'https://api.example.com/v1/thing?q=test' | grep -iE 'access-control|ratelimit|retry-after'
```

You are answering five questions the table cannot: does it still exist, what is the real
response shape, how slow is it, what rate limit headers come back, and does
`Access-Control-Allow-Origin` actually appear. **A CORS header that is absent here
overrides a `Yes` in the table.** The table drifts; the response does not.

### Architect
- `Access-Control-Allow-Origin` present **and** auth is `No` → browser-direct is allowed.
- Anything else → **server-side route**. This is the default, and it is the right default
  for two thirds of the catalog. A proxy route also gives you the only place to put
  caching, the key, and a timeout.

### Adapt
One module per provider, exporting a function that returns *your* domain type, not theirs:

```ts
export interface Weather { tempC: number; condition: string; observedAt: Date }
export async function getWeather(city: string): Promise<Weather> { /* map here */ }
```

Provider fields never leak past this file. Swapping APIs then edits one module instead of
every component that touched `data.main.temp`.

### Degrade
Every call gets a timeout, a bounded retry with jitter on 429/5xx only, and a defined
state for *failed* as well as *loading* and *empty*. A free API you do not pay for owes
you nothing; treat unavailability as a normal state, not an exception.

### Record
Note which API was chosen, which two were rejected and why, and what the probe measured.
The next person to hit a failure needs to know whether the alternatives were already
tried.

---

## 3. Running it

The three steps above ship as scripts, so this is a pipeline rather than a reading
exercise. Run them; do not reimplement them.

The scripts ship beside this file, and the working directory is the project root rather
than the skill directory — so resolve the skill root once and use it. A project install and
a user install are both possible, hence the two candidates:

```bash
API_SKILL="$(ls -d .claude/skills/public-api-integration \
  ~/.claude/skills/public-api-integration 2>/dev/null | head -1)"

python3 "$API_SKILL/scripts/find_api.py" --need "weather forecast" --no-auth
python3 "$API_SKILL/scripts/probe_api.py" --url '<endpoint>' --name weather \
  --out probe-report.json
python3 "$API_SKILL/scripts/scaffold_client.py" --report probe-report.json --out ./src \
  --env-key WEATHER_API_KEY
```

`find_api.py` shortlists candidates and prints a `docs_url` for each. That is a
documentation link, not a callable endpoint: open it, find the request URL its docs
specify, and probe that. Probing the `docs_url` directly returns an HTML page, which
`probe_api.py` reports as `documentation-page`.

`find_api.py` returns candidates, never a decision — it filters HTTPS hard, ranks
relevance from term hits alone, and refuses to let a key-free API look like a match
just because it is convenient. `probe_api.py` answers what the table cannot and
writes the report. `scaffold_client.py` builds the client from the payload that was
actually observed, so the types describe the response rather than the documentation.

Scaffolding refuses to overwrite without `--force`, and `--dry-run` prints the plan
first. When the probe verdict is `server-side-only` the generated client points at a
route handler rather than the third party, because that is the only arrangement in
which the credential stays off the client.

## 4. Secrets

An `apiKey` entry means a secret exists from that moment. It goes in `.env.local`,
is read **only** in server code, and `.env*` is in `.gitignore` before the key is pasted
anywhere. `NEXT_PUBLIC_`, `VITE_`, and every equivalent prefix ship the value to the
browser — a key behind one of those is public, and rotating it is the only fix.

## Rules

### MUST NOT — Integrate an API whose HTTPS column is `No`, or any endpoint that answers only over http://.

*Why:* A plaintext dependency inside an HTTPS page is blocked as mixed content by every current browser, so it fails outright in production even when it works in local development. Where it is not blocked — a server-side call — the response is modifiable in transit by anything on the path, which means the interface renders attacker-controlled data.

Incorrect:

```text
Row: | [SomeAPI](http://api.example.com) | ... | No | No | Unknown |  → integrated anyway
```

Correct:

```text
HTTPS column is `No` → discard the candidate and take the next one on the shortlist.
```

### MUST NOT — Place an API key in client-side code, in a client-exposed environment variable (NEXT_PUBLIC_, VITE_, REACT_APP_), or in a committed file.

*Why:* Build-time inlining means a prefixed variable is a literal string in the shipped bundle, readable by anyone who opens the network tab — the prefix is an explicit declaration that the value is public, not a naming convention. A committed key stays in git history after deletion, so the only real remediation is rotation, and that is a task nobody schedules until the quota is already exhausted by someone else.

Incorrect:

```ts
const key = process.env.NEXT_PUBLIC_WEATHER_KEY  // shipped to the browser
```

Correct:

```ts
// app/api/weather/route.ts — server only
const key = process.env.WEATHER_API_KEY
```

### MUST NOT — Integrate several APIs for the same field "for redundancy" before one has been shown to be insufficient.

*Why:* Each additional provider multiplies the failure surface, the response shapes to reconcile, and the secrets to manage, in exchange for redundancy the interface has not yet been shown to need. Fallback chains are also the code least likely to be exercised, so the second provider is usually broken by the time the first one fails.

*Exceptions:*
- A measured availability requirement exists and the primary has been observed to miss it.

### MUST — Call the live endpoint and inspect status, body shape, latency, and response headers before writing any integration code against it.

*Why:* The catalog carries no last-verified field, so a row is evidence that an API existed when someone added it and nothing more. The probe is also the only source for the four facts the table omits entirely — real response shape, rate limit headers, latency, and whether CORS headers are actually sent — and each of those changes the code you would write.

Incorrect:

```bash
// table says CORS: Yes
const r = await fetch(url)  // written before anyone called the endpoint
```

Correct:

```bash
curl -s -o /tmp/p.json -w '%{http_code} %{time_total}s\n' 'https://api.example.com/v1/thing'
curl -sI 'https://api.example.com/v1/thing' | grep -i 'access-control\|ratelimit'
```

### MUST — Route third-party calls through a server-side handler unless the probe showed an `Access-Control-Allow-Origin` header and the API needs no key.

*Why:* CORS is Unknown for 977 of the 1,683 catalogued entries and No for 150, so browser-direct is unsupported or unverified for roughly two thirds of the catalog and a Yes in the table is a claim rather than a measurement. The server route is also the only place that can hold the key, the cache, and the timeout, so choosing it by default collapses four decisions into one.

*Exceptions:*
- A probe confirmed the CORS header and the API requires no credential — then browser-direct removes a hop and a server dependency.

### MUST — Wrap every third-party call in one module that returns a domain type you define, so no provider-shaped field reaches a component.

*Why:* Free APIs are the dependencies most likely to disappear, rate-limit, or change shape without notice, and the cost of that event is set entirely by how many files mention their field names. An adapter makes the swap a single-file edit and makes the response shape mockable in tests without a network.

Incorrect:

```ts
<span>{data.main.temp}</span>  // OpenWeather's shape, in a component
```

Correct:

```ts
const weather = await getWeather(city)   // Weather { tempC, condition, observedAt }
<span>{weather.tempC}</span>
```

### MUST — Give every third-party call an explicit timeout, a bounded retry with jitter on 429 and 5xx only, and a designed failed state alongside loading and empty.

*Why:* An unpaid third-party API owes the project no availability, so failure is a normal operating state rather than an exception, and an interface with no failed state renders a spinner forever when it occurs. Retrying on 4xx other than 429 cannot succeed — the request is wrong, not unlucky — and retrying without jitter synchronises every client into a thundering herd against a service that is already struggling.

Incorrect:

```ts
const r = await fetch(url)          // no timeout, no retry policy, no failed state
```

Correct:

```ts
const r = await fetch(url, { signal: AbortSignal.timeout(5000) })
// retry 429/5xx only, 2 attempts, backoff * (1 + Math.random() * 0.3)
```

### MUST — Read the provider's own terms, quota, and attribution requirements before shipping, and record what they say.

*Why:* The catalog has no licence or rate-limit column, so a row cannot tell you whether commercial use is permitted, whether attribution is required, or how many requests a day are free. Discovering any of those after launch means either a takedown or an unplanned migration, and both cost more than the ten minutes the check takes.

### SHOULD NOT — Treat the Auth, HTTPS, or CORS columns as authoritative once a live response contradicts them.

*Why:* The table is community-maintained with no automated re-verification, so its columns record what was true when a contributor last looked. Where the observed response and the row disagree, the response is the current fact and the row is history.

### SHOULD — Carry three candidates through the probe rather than committing to the first matching row.

*Why:* Probes fail often enough on a directory with no liveness checking that a single candidate turns a routine step into a restart of the whole selection. The marginal cost of two extra probes is seconds; the cost of re-deriving the shortlist later is the whole task.

## Before reporting completion

Run these checks against your own output. Answer each question explicitly rather than
assuming the answer, because the point of the exercise is to notice what you did not
notice while building.

### Probe the endpoint and fail if it is unreachable, non-2xx, or not JSON. Writes the report the scaffolder needs. (blocking)

```bash
python3 "$(ls -d .claude/skills/public-api-integration ~/.claude/skills/public-api-integration 2>/dev/null | head -1)"/scripts/probe_api.py --url '<endpoint>' --name '<name>' --out probe-report.json
```

### Show every file the scaffolder would write, before it writes any of them.

```bash
python3 "$(ls -d .claude/skills/public-api-integration ~/.claude/skills/public-api-integration 2>/dev/null | head -1)"/scripts/scaffold_client.py --report probe-report.json --out ./src --dry-run
```

### Confirm the API was chosen from evidence rather than from the first matching row. (blocking)

- Was the live endpoint actually called before integration code was written, and what were the status, latency, and response shape?
- Did the probe show an Access-Control-Allow-Origin header, and does that agree with the CORS column? If they disagree, which one did the architecture follow?
- Is the HTTPS column `Yes` for the chosen API, and does the endpoint refuse plaintext?
- Were three candidates shortlisted, and is it written down which two were rejected and why?
- Have the provider’s terms, quota, and attribution requirements been read, and is what they say recorded somewhere the next person will find it?

### Confirm no credential reaches the client and the call path is deliberate. (blocking)

- Grep the built client bundle for the key value itself, not the variable name. Does it appear?
- Is every environment variable holding a secret free of a client-exposed prefix (NEXT_PUBLIC_, VITE_, REACT_APP_, PUBLIC_)?
- Was `.env*` in `.gitignore` before any real key was written to disk, and does `git log -S "<key prefix>"` come back empty?
- If the call is browser-direct, was that chosen because a probe confirmed CORS and no credential is needed — or because it was simply easier?
- Does the server route validate and constrain its own input, so it cannot be used as an open proxy to arbitrary upstream URLs?

### Confirm the integration behaves when the third party is slow, rate-limited, or gone. (blocking)

- Block the endpoint in DevTools and reload. Does the interface reach a designed failed state, or does it spin forever?
- Does every call carry an explicit timeout, and is the retry policy limited to 429 and 5xx with jitter on the backoff?
- Does any provider-shaped field name appear outside the adapter module?
- What happens on a 429 specifically — is Retry-After read, and is the user told something more useful than "error"?
- Can the whole integration be tested without a network, by mocking the adapter rather than the HTTP layer?

## Further reference

These are not loaded by default. Read one only when its question is the question you
currently have.

- `references/catalog-parsing.md` — How do I turn the README table into a filtered, ranked candidate list, and what does the messy real-world markup look like?

