> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ouraicalling.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Exemples d’intégration

> Modèles courants pour intégrer l’API à votre propre backend : mettre les clés derrière un proxy, réessayer en toute sécurité, recevoir des webhooks et effectuer des appels en lot

Voici des recettes pour la poignée de problèmes que rencontrent la plupart des intégrations, construites à partir des formes réelles de requêtes et de réponses de l’[introduction à l’API](/fr/api-reference/introduction). Adaptez le point de terminaison, les champs et les portées à la ressource avec laquelle vous travaillez — ce sont les modèles eux-mêmes (et non les payloads exacts) qui valent la peine d’être réutilisés.

## Garder la clé API hors du client

N’appelez jamais l’API directement depuis du code navigateur ou mobile — cela expose votre clé à quiconque ouvre les outils de développement. Placez plutôt un proxy léger devant l’API : votre frontend appelle votre propre backend, et seul votre backend détient la clé Famulor.

```js theme={null}
// server.js — Express route the frontend can safely call
import express from "express";

const app = express();
app.use(express.json());

app.post("/api/trigger-call", async (req, res) => {
  const { to_number, assistant_id, lead } = req.body;
  if (!to_number || !assistant_id) {
    return res.status(400).json({ error: "to_number and assistant_id are required" });
  }

  const famulorRes = await fetch("https://app.famulor.io/api/v1/calls", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.FAMULOR_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ to_number, assistant_id, lead }),
  });

  const body = await famulorRes.json();
  res.status(famulorRes.status).json(body);
});
```

Le frontend ne voit jamais `FAMULOR_API_KEY` — il ne communique qu’avec `/api/trigger-call` sur votre propre domaine.

## Réessayer avec un backoff

Un `429` ou un `5xx` mérite généralement d’être réessayé plutôt que de faire échouer immédiatement la requête. Espacez les tentatives au lieu de marteler l’API :

```js theme={null}
async function famulorRequest(path, options = {}, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fetch(`https://app.famulor.io/api/v1${path}`, {
      ...options,
      headers: {
        Authorization: `Bearer ${process.env.FAMULOR_API_KEY}`,
        "Content-Type": "application/json",
        ...options.headers,
      },
    });

    if (res.ok) return res.json();
    if (![429, 500, 502, 503].includes(res.status) || attempt === maxRetries) {
      const body = await res.json().catch(() => ({}));
      throw new Error(body?.error?.message ?? `Request failed with ${res.status}`);
    }

    const delayMs = 2 ** attempt * 500 + Math.random() * 250;
    await new Promise((r) => setTimeout(r, delayMs));
  }
}
```

Chaque tentative échouée double approximativement le temps d’attente (500 ms, 1 s, 2 s…) avec un peu de gigue aléatoire, afin que les appelants parallèles ne réessaient pas tous en même temps.

## Recevoir des webhooks

Un webhook au niveau de l’espace de travail signe chaque envoi avec `X-Famulor-Signature: sha256=<hex digest>` — un HMAC-SHA256 du corps de la requête **brut**, calculé avec votre secret de webhook. (Les URL de webhook au niveau de l’assistant ne sont pas signées et sont spécifiques à l’assistant ; consultez [Webhooks post-appel](/fr/assistants/webhooks) pour connaître la différence et le payload complet.) Vérifiez la signature avant de faire confiance au payload :

```js theme={null}
import express from "express";
import crypto from "node:crypto";

const app = express();

// express.raw() keeps the exact bytes — required for signature verification
app.post(
  "/webhooks/famulor",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const expected =
      "sha256=" +
      crypto.createHmac("sha256", process.env.FAMULOR_WEBHOOK_SECRET).update(req.body).digest("hex");
    const signature = req.header("X-Famulor-Signature") ?? "";
    const a = Buffer.from(signature);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).send("invalid signature");
    }

    const event = JSON.parse(req.body.toString("utf8"));
    if (event.event === "call.completed") {
      // event.data.transcript, .analysis, .collected, .variables, ...
    }
    res.status(200).send("ok");
  }
);
```

<Warning>
  Vérifiez la signature à partir des octets bruts, avant tout traitement JSON. Reformater le corps au préalable peut modifier les espaces ou l’ordre des clés et casser silencieusement la vérification de signature.
</Warning>

## Appeler un CSV en lot

Pour composer une liste plutôt qu’un seul numéro, limitez le nombre d’appels déclenchés simultanément — l’API rejette les requêtes dès que votre réserve de crédits ou votre limite de concurrence est atteinte, si bien qu’une boucle non bornée ne fait que produire un mur d’erreurs au lieu de terminer plus vite.

```js theme={null}
import { parse } from "csv-parse/sync";
import { readFileSync } from "node:fs";

const rows = parse(readFileSync("leads.csv"), { columns: true });
const CONCURRENCY = 5;

async function processBatch(rows) {
  const results = [];
  for (let i = 0; i < rows.length; i += CONCURRENCY) {
    const batch = rows.slice(i, i + CONCURRENCY);
    const batchResults = await Promise.allSettled(
      batch.map((row) =>
        famulorRequest("/calls", {
          method: "POST",
          body: JSON.stringify({
            assistant_id: process.env.ASSISTANT_ID,
            to_number: row.phone,
            lead: { name: row.name, company: row.company },
          }),
        })
      )
    );
    results.push(...batchResults);
  }
  return results;
}
```

`famulorRequest` est ici l’assistant de réessai avec backoff vu plus haut — le traitement par lot vous donne une concurrence maîtrisée, et cet assistant absorbe les `429` occasionnels sans faire échouer l’ensemble du run. Pour un volume dépassant un script ponctuel, une [campagne](/fr/campaigns/overview) ou une [automatisation](/fr/automations/overview) déclenchée par des données CRM demande généralement moins de code personnalisé à maintenir qu’un script de traitement par lot qu’il faut garder en fonctionnement.
