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

# API-Integrationsbeispiele

> Gängige Muster, um die API in dein eigenes Backend einzubinden: Keys proxen, sicher wiederholen, Webhooks empfangen und Batch-Anrufe tätigen

Das sind Rezepte für dieselbe Handvoll Probleme, auf die die meisten Integrationen stoßen, gebaut auf den echten Request- und Response-Formen aus der [API-Einführung](/de/api-reference/introduction). Passe Endpunkt, Felder und Scopes an die Ressource an, mit der du arbeitest – die Muster selbst (nicht die exakten Payloads) sind es, was sich wiederzuverwenden lohnt.

## Den API-Key vom Client fernhalten

Rufe die API niemals direkt aus Browser- oder Mobile-Code auf – das legt deinen Key gegenüber jedem offen, der die Entwicklertools öffnet. Setze stattdessen einen schlanken Proxy davor: Dein Frontend ruft dein eigenes Backend auf, und nur dein Backend kennt den Famulor-Key.

```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);
});
```

Das Frontend bekommt `FAMULOR_API_KEY` nie zu Gesicht – es spricht nur mit `/api/trigger-call` auf deiner eigenen Domain.

## Mit Backoff wiederholen

Ein `429` oder ein `5xx` lohnt sich meist zu wiederholen, statt sofort aufzugeben. Warte zwischen den Versuchen, statt die API im Dauerfeuer zu belasten:

```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));
  }
}
```

Jeder fehlgeschlagene Versuch verdoppelt ungefähr die Wartezeit (500 ms, 1 s, 2 s …) mit ein wenig zufälligem Jitter, damit nicht alle parallelen Aufrufer im Gleichschritt erneut versuchen.

## Webhooks empfangen

[Buchungs-Webhooks](/de/assistants/native-calendar#webhooks) sind signiert: Setze eine URL auf einem Buchungs-Event-Typ, und jede Zustellung trägt `X-Famulor-Signature: sha256=<hex digest>` – ein HMAC-SHA256 des **rohen** Request-Bodys mit dem Secret, das dir beim Speichern der URL einmalig angezeigt wird. Der [Variablen-Webhook](/de/assistants/variables#signatur-verifizieren) verwendet dasselbe Schema mit einem eigenen Secret, während [Webhooks nach dem Anruf](/de/assistants/webhooks) auf Assistentenebene unsigniert sind. Prüfe die Signatur, bevor du der Payload vertraust:

```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-booking",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const expected =
      "sha256=" +
      crypto.createHmac("sha256", process.env.FAMULOR_BOOKING_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 === "booking.created") {
      // event.data.booking_id, .invitee, .start_time, .end_time, ...
    }
    res.status(200).send("ok");
  }
);
```

<Warning>
  Prüfe gegen die rohen Bytes, vor jedem JSON-Parsing. Wird der Body vorher neu serialisiert, können sich Whitespace oder Feldreihenfolge ändern und die Signaturprüfung stillschweigend brechen.
</Warning>

## Eine CSV im Batch anrufen

Um eine Liste statt einer einzelnen Nummer anzurufen, begrenze, wie viele Anrufe du gleichzeitig auslöst – die API lehnt Anfragen ab, sobald dein Credit-Hold oder dein Concurrency-Limit erreicht ist, sodass eine unbegrenzte Schleife nur einen Berg von Fehlern statt eines schnelleren Ergebnisses produziert.

```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` ist hier der Retry-with-Backoff-Helfer von oben – das Batching gibt dir kontrollierte Nebenläufigkeit, und der Helfer fängt den gelegentlichen `429` ab, ohne den ganzen Lauf scheitern zu lassen. Für mehr Volumen als ein einmaliges Skript braucht eine [Kampagne](/de/campaigns/overview) oder eine durch CRM-Daten ausgelöste [Automatisierung](/de/automations/overview) meist weniger eigenen Code, um sie am Laufen zu halten, als ein Batch-Skript, das du selbst am Leben halten musst.
