# Fetch data from an API

> Call external HTTP APIs from a voxal app with the host-bridged fetch, including JSON, errors, and limits

Voxal apps run inside a sandbox with no network access of their own. To reach the outside world you import `fetch` from `@voxalsh/sdk`, which runs the request in the trusted host and hands the result back to your app. This guide covers GET and POST, parsing JSON, handling errors, showing a loading state, and the limits you need to design around.

## The shape of fetch

```js

const res = await fetch(url, { method, headers, body });
// res: { status, headers, body }
```

Two things differ from the browser `fetch`:

- The request `body`, if present, must be a string. Serialize JSON yourself with `JSON.stringify`.
- The result `body` is always a UTF-8 string, not a stream or a parsed object. Call `JSON.parse(res.body)` to get JSON.

## A GET request

```js title="app.js"

createApp()
  .onKey(async (conn, data) => {
    if (data !== 'c') return;
    try {
      const res = await fetch('https://catfact.ninja/fact');
      const fact = JSON.parse(res.body).fact;
      conn.write('🐱 ' + fact + '\r\n');
    } catch (err) {
      conn.write('(fetch failed: ' + err.message + ')\r\n');
    }
  })
  .listen();
```

<Callout type="warn">
The SDK does not await your handlers, so always wrap an `await fetch(...)` in `try/catch`. An unhandled rejection is logged to the server logs, not shown to the user, so they would see nothing happen.
</Callout>

## A POST request with JSON

Set the method, headers, and a stringified body.

```js title="app.js"
const res = await fetch('https://api.example.com/items', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ name: 'widget', qty: 3 }),
});
```

## Handling non-2xx responses

A non-2xx status does not throw. Only network-level failures (timeout, blocked address, transport error) reject the promise. Check `res.status` yourself.

```js title="app.js"
const res = await fetch('https://api.example.com/data');

if (res.status >= 200 && res.status < 300) {
  const data = JSON.parse(res.body);
  // use data
} else {
  conn.write('request failed with status ' + res.status + '\r\n');
}
```

## A loading state in a UI app

In the UI layer, track a `busy` flag in state, set it before the request, and clear it in a `finally` so it resets even on error.

```jsx title="app.jsx"

function Fact() {
  const [fact, setFact] = useState('press f for a cat fact');
  const [busy, setBusy] = useState(false);

  const load = async () => {
    setBusy(true);
    try {
      const res = await fetch('https://catfact.ninja/fact');
      setFact(JSON.parse(res.body).fact);
    } catch (err) {
      setFact('(failed: ' + err.message + ')');
    } finally {
      setBusy(false);
    }
  };

  useInput((input) => {
    if (input === 'f' && !busy) load();
  });

  return (
    <Box borderStyle="round" paddingX={1}>
      <Text>{busy ? '…fetching' : fact}</Text>
    </Box>
  );
}

serve(() => <Fact />);
```

## Limits

`fetch` runs from the host, so it is guarded and rate-limited. Design for these:

| Limit | Value |
| --- | --- |
| Schemes | `http` and `https` only |
| Blocked targets | private, loopback, link-local, and cloud-metadata addresses |
| Redirects | up to 5 hops |
| Timeout | 10 seconds per request |
| Response size | 5 MiB cap |
| Rate | 30 requests per 10 seconds per app |
| Concurrency | 6 in flight per app |

The blocked-address rules cannot be turned off; they exist so that a deployed app cannot probe the host's internal network. If you need a request to a private service, it has to be reachable on a public address.

## Next steps

<Cards>
  <Card title="fetch reference" href="/sdk/fetch" description="The full signature and guarantees" />
  <Card title="Platform limits" href="/platform/limits" description="Every limit in one place" />
  <Card title="Security" href="/platform/security" description="Why fetch is host-bridged and SSRF-guarded" />
</Cards>

---

Source: https://docs.voxal.sh/guides/fetching-data
