# Mobile apps

Talk to Nur from your iOS or Android app over WebRTC, straight from the phone, with a one-use secret your server mints so your key never ships in the app.

What you get:

- A voice conversation from the phone to EESI over WebRTC: echo cancellation,
  Opus, and interruptions handled for you.
- Your API key stays on your server. The app gets a one-use secret that opens a
  session your server already set up and locked: instructions, voice, tools.
- Limits the app cannot change: the longest a call may last and how long a
  silence it may sit through.
- Audio never passes through your server.

## Run it

Your server mints a secret and hands the app the relay servers it may need to
reach EESI from a mobile network:

```bash
export EESI_API_KEY=sk-eesi-...
node server.mjs
```

```js server.mjs
import { createServer } from "node:http";

const API = "https://api.eesi.ai/v1";
const headers = { Authorization: `Bearer ${process.env.EESI_API_KEY}`, "Content-Type": "application/json" };

createServer(async (request, response) => {
  // Your own login decides who may talk, and as whom.
  const secret = await fetch(`${API}/realtime/client_secrets`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      session: { instructions: "You are the voice of Example Deliveries. Keep answers short.", audio: { output: { voice: "Nur" } } },
      eesi_lock: "all",
      eesi_limits: { max_duration_seconds: 600, silence_hangup_seconds: 60 },
    }),
  }).then((r) => r.json());
  const { ice_servers } = await fetch(`${API}/realtime/ice-servers`, { headers }).then((r) => r.json());
  response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ token: secret.value, iceServers: ice_servers }));
}).listen(8080);
```

The app opens the call. This is React Native with `react-native-webrtc`; the
WebRTC libraries for Swift and Kotlin take the same steps:

```js
import { RTCPeerConnection, mediaDevices } from "react-native-webrtc";

const { token, iceServers } = await fetch("https://your-server.example/nur", { method: "POST" }).then((r) => r.json());
const pc = new RTCPeerConnection({ iceServers });
const mic = await mediaDevices.getUserMedia({ audio: true });
pc.addTrack(mic.getAudioTracks()[0], mic);
const events = pc.createDataChannel("oai-events");
await pc.setLocalDescription(await pc.createOffer());
await new Promise((resolve) => {
  // Send the offer once the phone's network addresses are in it.
  if (pc.iceGatheringState === "complete") return resolve();
  pc.addEventListener("icegatheringstatechange", () => pc.iceGatheringState === "complete" && resolve());
  setTimeout(resolve, 2000);
});
const answer = await fetch("https://api.eesi.ai/v1/realtime/calls", {
  method: "POST",
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/sdp" },
  body: pc.localDescription.sdp,
});
await pc.setRemoteDescription({ type: "answer", sdp: await answer.text() });
```

Nur's voice arrives as the call's remote audio track, which the WebRTC library
plays. Events (what the person said, Nur's words, when it starts and stops)
arrive as JSON on the `events` channel, and the app can send events there too:
`events.send(JSON.stringify({ type: "response.create" }))` once the channel
opens has Nur speak first. Close the peer connection to hang up.

The secret is spent by the first call that presents it and expires after a
minute if unused, so ask your server for a new one for each call.

## Good to know

- With `eesi_lock: "all"`, the app can still stream audio, send the person's
  typed messages and ask for a reply, but cannot change who Nur is or what it
  may do.
- When a limit is reached the app receives `{"type": "eesi.hangup", "reason": "silence"}`
  (or `max_duration`) and the call ends.
- `eesi_webhook` posts what was said to your server as it happens, signed, and
  `eesi_data` keeps a call out of training or out of recordings.

## Next

<CardGroup cols={2}>
  <Card title="Client secrets" icon="key" href="/realtime/client-secrets">
    Locks, limits, webhooks and what a session's words may be used for.
  </Card>
  <Card title="Live conversation" icon="bolt" href="/realtime/live">
    Session settings, voices, interruptions and every event.
  </Card>
  <Card title="Tools and MCP servers" icon="wrench" href="/realtime/tools">
    Let Nur look things up or act in your app.
  </Card>
  <Card title="Voices" icon="microphone" href="/speech/voices">
    Built-in voices, or clone your brand's own.
  </Card>
</CardGroup>
