> ## 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.

# Ejemplos de integración

> Patrones habituales para conectar la API a tu propio backend: proteger claves con un proxy, reintentar de forma segura, recibir webhooks y hacer llamadas por lotes

Estas son recetas para el puñado de problemas con los que se topa la mayoría de las integraciones, construidas sobre las formas reales de solicitud y respuesta de la [introducción a la API](/es/api-reference/introduction). Adapta el endpoint, los campos y los alcances al recurso con el que estés trabajando — lo que vale la pena reutilizar son los patrones en sí, no los payloads exactos.

## Mantén la clave de API fuera del cliente

Nunca llames a la API directamente desde código de navegador o de una app móvil — eso expone tu clave a cualquiera que abra las herramientas de desarrollo. En su lugar, pon un proxy ligero delante: tu frontend llama a tu propio backend, y solo tu backend guarda la clave de Famulor.

```js theme={null}
// server.js — ruta de Express que el frontend puede llamar de forma segura
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);
});
```

El frontend nunca ve `FAMULOR_API_KEY` — solo habla con `/api/trigger-call` en tu propio dominio.

## Reintentos con espera progresiva

Un `429` o un `5xx` normalmente vale la pena reintentarlo, no fallar de inmediato. Espera cada vez más entre intentos en lugar de machacar la 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));
  }
}
```

Cada intento fallido aproximadamente duplica la espera (500 ms, 1 s, 2 s…) con un poco de aleatoriedad (jitter) para que las llamadas en paralelo no reintenten todas al mismo tiempo.

## Recibir webhooks

Un webhook a nivel de espacio de trabajo firma cada envío con `X-Famulor-Signature: sha256=<hex digest>` — un HMAC-SHA256 del cuerpo **sin procesar** de la solicitud usando tu secreto de webhook. (Las URL de webhook a nivel de asistente no van firmadas y son específicas de cada asistente; consulta [Webhooks posteriores a la llamada](/es/assistants/webhooks) para conocer la diferencia y el payload completo). Verifica la firma antes de confiar en el payload:

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

const app = express();

// express.raw() conserva los bytes exactos — necesario para verificar la firma
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>
  Verifica contra los bytes sin procesar, antes de cualquier análisis JSON. Volver a serializar el cuerpo primero puede cambiar los espacios en blanco o el orden de las claves y romper la verificación de la firma sin previo aviso.
</Warning>

## Llamar en lote desde un CSV

Para marcar una lista en lugar de un único número, limita cuántas llamadas lanzas a la vez — la API rechaza las solicitudes en cuanto se alcanza tu retención de crédito o el límite de concurrencia, así que un bucle sin límite solo produce un muro de errores en lugar de terminar antes.

```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` aquí es el ayudante de reintentos con espera progresiva de arriba — el procesamiento por lotes te da concurrencia controlada, y el ayudante absorbe el `429` ocasional sin que falle toda la ejecución. Para un volumen que supere un script puntual, una [campaña](/es/campaigns/overview) o una [automatización](/es/automations/overview) activada por datos del CRM suele necesitar menos código propio que mantener que un script por lotes que tienes que seguir ejecutando.
