i99dash docs
Concepts

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 wayfetch() (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

  1. List the bare HTTPS origins you need in manifest.network.
  2. Call them with fetch() like any web app.
  3. The car host turns your network list 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/v1 is invalid — drop the path).
  • No wildcards (https://*.example.com), IP literals, or localhost.
  • 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. network is 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),
});

On this page