Calling an external API
How a mini-app reaches a third-party HTTPS API — declare the origin in manifest.network, then call it with normal fetch(). The host enforces the allow-list as a CSP.
A mini-app reaches external HTTPS APIs the normal web way —
fetch() (or XMLHttpRequest). There is no SDK proxy, no host round
trip, and no ops-managed routing table. The one rule: every origin you
call must be declared up front in your manifest's network field.
The model in one breath
- List the bare HTTPS origins you need in
manifest.network. - Call them with
fetch()like any web app. - The car host turns your
networklist into a per-app Content-Security-Policy and blocks every origin you didn't declare.
// manifest.json
{
"id": "fuel-prices",
"name": { "en": "Fuel Prices" },
"icon": "./icon.svg",
"url": "https://miniapps.i99dash.app/fuel-prices/",
"version": "1.0.0",
"category": "services",
"network": ["https://api.fuel.example.com"]
}// src/main.ts — just fetch().
const res = await fetch('https://api.fuel.example.com/v1/stations?lat=24.7&lng=46.6');
if (!res.ok) return renderError(`HTTP ${res.status}`);
const { stations } = await res.json();
renderStations(stations);That's the whole surface. No client.callApi, no envelope, no route
keys — the SDK is not involved in your network calls at all.
network is the allow-list
network is an optional array of bare HTTPS origins. Each entry
is https://host[:port] — host and optional port only:
- No path, query, fragment, or userinfo (
https://api.example.com/v1is invalid — drop the path). - No wildcards (
https://*.example.com), IP literals, orlocalhost. - HTTPS only. Plain
http://is rejected. - Max 10 origins. Entries are lowercased, canonicalized, and de-duplicated at publish time, so the stored value is byte-stable.
Omit the field (or pass []) and the app reaches no third-party
network — it can still load its own bundle from url, but every
cross-origin fetch() is blocked.
The grammar is shared, byte-for-byte, by the SDK, the backend, and the
car host — see canonicalizeMiniAppOrigin.
How the host enforces it
When the host loads your app it builds a per-app
Content-Security-Policy from network (union with your own bundle
origin) and delivers it as an HTTP response header. The browser then
refuses any connect-src to an undeclared origin — your fetch()
rejects with a TypeError, the same as a CSP violation on the open
web. The host also intercepts requests as a backstop, so the policy
holds even for transports the CSP can't see.
Redirects are checked too. If a declared origin 3xx-redirects to an origin you did not declare, the host blocks the redirect. Declare every origin in the chain, not just the first hop.
Be honest about what this is — and isn't
Declared egress is unauthenticated. It is a raw browser fetch():
- No i99dash credentials are attached. The host does not inject a session token, JWT, or any identity header. If your API needs to know who the user is, you authenticate it yourself (e.g. an API key you obtained out of band — but see the warning below).
- It is a least-privilege control, not a sandbox.
networkis reviewed at publish time and narrows what a well-behaved app can reach. It is not a guarantee against a hostile author — a malicious developer can still call whatever they declared.
So treat network as "the minimum set of origins this app legitimately
needs", and treat anything reachable from a declared origin as part of
your app's public attack surface.
The bundle is public
Everything you ship — code, config, any key you embed — is downloaded
by every user and trivially extractable (unzip + strings).
- Don't embed secrets. API keys, tokens, and credentials in the
bundle are public. If an API requires a secret, put a thin
HTTPS service of your own in front of it, declare that origin in
network, and keep the secret server-side. - Validate responses defensively. A third-party API can return anything — including a payload shaped to break your renderer or inject markup. Escape on render; parse with a schema (e.g. zod) if the shape matters.
Patterns
Failures are normal fetch() failures
There is no success/error envelope anymore. Branch the way you would on the open web:
try {
const res = await fetch('https://api.example.com/v1/data');
if (!res.ok) return renderError(`HTTP ${res.status}`); // 4xx / 5xx
return await res.json();
} catch (e) {
// network down, DNS, TLS, OR a CSP block (undeclared origin) →
// fetch() rejects with TypeError.
return renderError('Network error');
}A TypeError from fetch() covers both a genuine network failure and
a CSP block from calling an origin you forgot to declare. If your call
fails only on-car (but works in a browser tab), the missing
network entry is the first thing to check.
Cancellation on unmount
AbortController works exactly as on the web:
const controller = new AbortController();
fetch('https://api.example.com/v1/data', { signal: controller.signal })
.then((r) => r.json())
.then(setData)
.catch(() => {});
// later:
controller.abort();Timeout
Use AbortSignal.timeout() (or your own timer + controller.abort()):
const res = await fetch('https://api.example.com/v1/report', {
signal: AbortSignal.timeout(60_000),
});Related
MiniAppManifest— thenetworkfield and the rest of the manifest schema.canonicalizeMiniAppOrigin— the exact origin grammar the host enforces.- Recipe — Call a third-party API — a minimal end-to-end example.
- Recipe — Fetch and render a list — the fuller walkthrough with loading / empty / error states.
- Best practices — production error-handling patterns.