# SwissApp Calls API — Documentation

> **URL de base** : `https://api.calls.swissapp.net`  
> **Worker** : `src_calls`  
> **Préfixe de clé** : `call_`  
> **Manager** : [https://manager.swissapp.net/apis/camam](https://manager.swissapp.net/apis/camam)  
> **Version** : 1.0  
> **Dernière mise à jour** : août 2026

Le Worker **ne relais jamais l’audio ni la vidéo**. Il délivre uniquement des identifiants ICE / TURN **éphémères**. Le média circule navigateur ↔ `turn.cloudflare.com`.

---

## Table des matières

1. [Vue d’ensemble](#vue-densemble)
2. [Authentification](#authentification)
3. [POST /credentials](#post-credentials)
4. [POST /revoke](#post-revoke)
5. [GET /health](#get-health)
6. [Codes d’erreur](#codes-derreur)
7. [Usage WebRTC](#usage-webrtc)
8. [Adresses et ports](#adresses-et-ports)
9. [Proxy via api.swissapp.org](#proxy-via-apiswissapporg)

---

## Vue d’ensemble

Les appels 1:1 (Taxibook, Cleaning, etc.) sont du **WebRTC P2P**. Le STUN public découvre l’adresse ; si le NAT est strict, ICE bascule sur un candidat **relay** TURN.

| Rôle | Qui |
|------|-----|
| Credentials ICE | `api.calls.swissapp.net` |
| Relais média | `turn.cloudflare.com` (hors Swissapp) |
| Signalisation (offer / answer / ICE) | votre API applicative (inchangée) |

### Endpoints publics

| Méthode | Chemin | Auth | Description |
|---------|--------|------|-------------|
| `GET` | `/` ou `/health` | — | Santé du service |
| `POST` | `/credentials` | clé `call_` | Génère `iceServers` |
| `POST` | `/v1/ice-servers` | clé `call_` | Alias de `/credentials` |
| `POST` | `/api/credentials` | clé `call_` | Alias de `/credentials` |
| `POST` | `/revoke` | clé `call_` | Révoque un username TURN |
| `POST` | `/api/revoke` | clé `call_` | Alias de `/revoke` |

CORS est ouvert pour les origines navigateur (`Authorization`, `Content-Type`, `X-API-Key`).

---

## Authentification

Clé créée sur [my.swissapp.org](https://my.swissapp.org) → **Calls** ou **Clés API** (service `calls`).

```
Authorization: Bearer call_xxxxxxxxxxxx
```

Variante acceptée : header `X-API-Key: call_xxxxxxxxxxxx`.

La clé doit commencer par `call_`. Une clé invalide, absente ou désactivée renvoie **401**.

---

## POST /credentials

Génère des ICE servers (STUN + TURN) prêts pour `new RTCPeerConnection({ iceServers })`.

Aliases : `POST /v1/ice-servers`, `POST /api/credentials`.

### Headers

```http
Authorization: Bearer call_xxxxxxxxxxxx
Content-Type: application/json
```

### Body (optionnel)

| Champ | Type | Défaut | Description |
|-------|------|--------|-------------|
| `ttl` | number | `3600` | Durée de vie en secondes. Clampé entre **300** et **172800** (48 h, max Cloudflare). |
| `custom_identifier` | string | `user_id` de la clé | Tag analytics / anti-abus (max 120 caractères). Alias JSON : `customIdentifier`. |

```json
{
  "ttl": 3600,
  "custom_identifier": "taxibook:MGR-YANNICK"
}
```

### Comportement

1. Vérifie la clé `call_`.
2. Clamp `ttl` (min 300, max 172800, défaut 3600).
3. Appelle Cloudflare Realtime `generate-ice-servers` **côté serveur** (la TURN key n’est jamais exposée).
4. Filtre les URLs `:53` (souvent bloquées par Chrome / Firefox / FAI).
5. Journalise la session en D1 (`custom_identifier`, `ttl`, `username`, statut).
6. Répond `{ success, iceServers, expires_in, ttl }`.

### Exemple

```bash
curl -X POST https://api.calls.swissapp.net/credentials \
  -H "Authorization: Bearer call_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "ttl": 3600,
    "custom_identifier": "taxibook:MGR-YANNICK"
  }'
```

### Réponse 200

```json
{
  "success": true,
  "iceServers": [
    {
      "urls": [
        "stun:stun.cloudflare.com:3478"
      ]
    },
    {
      "urls": [
        "turn:turn.cloudflare.com:3478?transport=udp",
        "turn:turn.cloudflare.com:3478?transport=tcp",
        "turn:turn.cloudflare.com:80?transport=tcp",
        "turns:turn.cloudflare.com:5349?transport=tcp",
        "turns:turn.cloudflare.com:443?transport=tcp"
      ],
      "username": "…",
      "credential": "…"
    }
  ],
  "expires_in": 3600,
  "ttl": 3600
}
```

`expires_in` = `ttl` effectif après clamp. Si l’appel dépasse le TTL : redemander des credentials et `pc.setConfiguration({ iceServers: fresh })`.

Les **deux** pairs doivent chacun demander leurs credentials. Pas besoin du même `username`.

ICE : P2P d’abord (`host` / `srflx`), puis `relay` si le NAT est strict. Pas de ralentissement si le réseau est ouvert.

---

## POST /revoke

Révoque un username TURN avant l’expiration du TTL (fermeture de session, abus).

Aliases : `POST /api/revoke`.

### Body

| Champ | Type | Requis | Description |
|-------|------|--------|-------------|
| `username` | string | ✅ | Username renvoyé dans `iceServers` |

```bash
curl -X POST https://api.calls.swissapp.net/revoke \
  -H "Authorization: Bearer call_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"username":"…"}'
```

### Réponse 200

```json
{ "success": true }
```

Si le TTL expire pendant un appel, Cloudflare coupe le relais après un court délai.

---

## GET /health

Santé publique, sans authentification. Alias : `GET /`.

```bash
curl https://api.calls.swissapp.net/health
```

```json
{
  "success": true,
  "service": "src_calls",
  "version": "1.0.0",
  "turn_configured": true
}
```

`turn_configured` est `false` si les secrets Worker `TURN_KEY_ID` / `TURN_KEY_API_TOKEN` ne sont pas posés. Dans ce cas `/credentials` répond **503**.

---

## Codes d’erreur

| HTTP | Cas |
|------|-----|
| `400` | Body invalide (`username` manquant sur `/revoke`) |
| `401` | Clé absente, invalide, ou ne commençant pas par `call_` |
| `429` | Quota Cloudflare TURN |
| `502` | Cloudflare Realtime indisponible |
| `503` | Secrets TURN non configurés sur le Worker |
| `404` | Chemin inconnu |

```json
{ "success": false, "error": "API key invalide ou manquante" }
```

---

## Usage WebRTC

Avant `createPc()` (appel sortant **et** acceptation) :

```js
async function getIceServers(apiKey, customIdentifier) {
  const r = await fetch('https://api.calls.swissapp.net/credentials', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      ttl: 3600,
      custom_identifier: customIdentifier
    })
  });
  const data = await r.json();
  if (!r.ok || data.success === false) {
    throw new Error(data.error || 'Calls API error');
  }
  return data.iceServers;
}

const iceServers = await getIceServers('call_xxxxxxxxxxxx', 'taxibook:MGR-YANNICK');
const pc = new RTCPeerConnection({ iceServers });
```

Côté manager, on peut aussi passer par le proxy SSO (la clé `call_` ne quitte pas le serveur) :

```
POST https://api.swissapp.org/api/calls/credentials
Authorization: Bearer <token SSO my.swissapp.org>
```

La **signalisation** ne change pas : offer / answer / candidats ICE restent sur votre API (ex. Taxibook `POST /chat/calls/signal`).

---

## Adresses et ports

Relais Cloudflare, **pas** le Worker.

| Protocole | Hôte | Port principal | Alternate |
|-----------|------|----------------|-----------|
| STUN UDP | `stun.cloudflare.com` | 3478 | 53 (filtré) |
| TURN UDP | `turn.cloudflare.com` | 3478 | 53 (filtré) |
| TURN TCP | `turn.cloudflare.com` | 3478 | 80 |
| TURNS TLS | `turn.cloudflare.com` | 5349 | 443 |

STUN Cloudflare reste gratuit et illimité. Le relais TURN n’est utilisé **que** si le P2P échoue.

---

## Proxy via api.swissapp.org

Pour l’espace client (SSO), sans exposer la clé projet au navigateur de test :

| Méthode | Chemin | Description |
|---------|--------|-------------|
| `GET` | `/api/calls/usage` | Clés Calls du user + compteurs |
| `GET` | `/api/calls/history` | Historique des sessions ICE |
| `POST` | `/api/calls/credentials` | Proxy vers `/credentials` avec la clé du user |

Auth : `Authorization: Bearer <token SSO>`.
