# Guía para desarrolladores

Esta página es para quien arma el sitio web propio de tu iglesia o sus automatizaciones.
HolyCRM ofrece dos formas de conectarse:

- **Datos públicos en JSON**: tu sitio web *lee* desde HolyCRM tus eventos, células,
  horarios de reunión y ministerios, siempre actualizados.
- **Webhooks**: HolyCRM *envía* los comunicados y lo que pasa en tu iglesia (nuevas
  visitas, miembros, pedidos de oración, inscripciones, respuestas a turnos) a una
  dirección tuya.

Las dos son parte del plan Pro, y solo un Administrador puede activarlas, en
**Configuración → [Integraciones](#/integrations)**.

## Datos públicos en JSON

### Cómo activarlos

En **Integraciones → Datos públicos en JSON**, un Administrador marca las fuentes que
quiere compartir y hace clic en **Guardar**. Debajo de cada fuente aparece su dirección,
con un botón **Copiar**. Desmarcá una fuente y guardá para cortarla al instante.

Cada fuente lleva **exactamente lo que muestra el sitio web de HolyCRM de la iglesia,
nunca más**. Nunca se incluyen fichas de miembros, niños, notas ni direcciones de casas o
anfitriones de células.

### Direcciones

Cada iglesia tiene sus propias direcciones, armadas con su id de HolyCRM (la pantalla las
muestra completas):

| Dirección | Qué devuelve |
|---|---|
| `https://pb.holycrm.app/backend/api/public/feeds/<id>` | El nombre y la zona horaria de la iglesia, y la dirección de cada fuente activada |
| `…/feeds/<id>/events` | Próximos eventos |
| `…/feeds/<id>/groups` | Células marcadas con "Mostrar en el sitio web" |
| `…/feeds/<id>/services` | Horarios semanales marcados con "Mostrar en el sitio web" |
| `…/feeds/<id>/ministries` | Ministerios activos |

No necesitan clave ni inicio de sesión, y se pueden llamar directo desde una página web en
cualquier sitio (se permiten pedidos de otro origen). Las respuestas pueden guardarse en
caché 5 minutos, así que un cambio en HolyCRM aparece en tu sitio en pocos minutos.

### Qué devuelve cada fuente

Cada fuente devuelve JSON con la iglesia y una lista con el nombre de la fuente:

```json
{
  "church": { "name": "Iglesia Gracia", "timezone": "America/Argentina/Buenos_Aires" },
  "events": [ … ]
}
```

Un campo vacío es `null`.

**Eventos** (`events`), del más próximo al más lejano:

| Campo | |
|---|---|
| `id` | El id del evento |
| `title` | Nombre del evento |
| `starts_at` | Inicio, en UTC: `"2026-11-01 15:00:00.000Z"` |
| `location_place` | Dónde es, como se escribió en el evento |
| `description` | La descripción pública para el sitio web |
| `photo_url` | Dirección completa de la foto del sitio web |
| `registration` | `true` cuando las visitas pueden inscribirse en el sitio web de HolyCRM de la iglesia |

Acepta dos opciones: `?limit=` cuántos eventos (de 1 a 100, 20 por defecto) y `?days=`
cuántos días hacia adelante (de 1 a 365, 90 por defecto). Por ejemplo
`…/events?limit=5&days=30`.

**Células** (`groups`), por nombre:

| Campo | |
|---|---|
| `id` | El id de la célula |
| `name` | Nombre de la célula |
| `description` | La descripción pública para el sitio web |
| `meeting_day` | `mon`, `tue`, `wed`, `thu`, `fri`, `sat` o `sun` |
| `meeting_time` | Hora como se escribió, local de la iglesia, por ejemplo `"19:30"` |

**Horarios de reunión** (`services`), empezando por el domingo:

| Campo | |
|---|---|
| `title` | Nombre de la actividad, por ejemplo "Culto del domingo" |
| `weekday` | `sun` … `sat` |
| `time` | Hora local de la iglesia, por ejemplo `"10:00"` |
| `duration_minutes` | Duración, si está cargada |
| `place` | Nombre de la sede, si está cargada |

**Ministerios** (`ministries`), por nombre:

| Campo | |
|---|---|
| `id` | El id del ministerio |
| `name` | Nombre del ministerio |
| `min_age`, `max_age` | Rango de edad, si está cargado (para ministerios de niños y jóvenes) |
| `description` | La descripción pública para el sitio web |
| `photo_url` | Dirección completa de la foto del sitio web |

### Horarios y zonas horarias

El inicio de los eventos (`starts_at`) está en **UTC**. Los horarios de reuniones y
células son la hora **en la iglesia**. Mostrá los dos en la zona horaria de la iglesia,
que viene en `church.timezone` (por ejemplo `America/Argentina/Buenos_Aires`).

### Cuando una fuente responde "no encontrado"

Una fuente responde `404 {"error": "not found"}` cuando está desactivada, cuando la
dirección tiene un id equivocado o cuando la iglesia ya no está en el plan Pro. Si la
iglesia vuelve a Pro, recupera sus fuentes en las mismas direcciones.

### Ejemplo

Una lista de los próximos cinco eventos en cualquier página web:

```html
<ul id="events"></ul>
<script>
  const FEED = "https://pb.holycrm.app/backend/api/public/feeds/ID_DE_TU_IGLESIA/events?limit=5";
  fetch(FEED)
    .then((r) => r.json())
    .then(({ church, events }) => {
      const list = document.getElementById("events");
      for (const ev of events) {
        const when = new Date(ev.starts_at.replace(" ", "T")).toLocaleString("es", {
          timeZone: church.timezone || undefined,
          weekday: "long", day: "numeric", month: "long", hour: "2-digit", minute: "2-digit",
        });
        const li = document.createElement("li");
        li.textContent = `${ev.title} · ${when}${ev.location_place ? " · " + ev.location_place : ""}`;
        list.appendChild(li);
      }
    });
</script>
```

Usá `textContent` (no `innerHTML`) para todo lo que venga de una fuente, como arriba.

## Webhooks

Cómo configurar un webhook, elegir qué recibe y los formatos listos para ntfy y Telegram
está explicado en [Integraciones](#/integrations). Lo que le importa a un desarrollador:

- Cada envío es un `POST` con un cuerpo JSON (o tu formato a medida). `X-HolyCRM-Event`
  dice qué pasó, por ejemplo `visitor.created`.
- `X-HolyCRM-Delivery` no cambia cuando un envío se reintenta, así podés ignorar
  duplicados.
- **Cada pedido va firmado.** `X-HolyCRM-Signature` es `sha256=` seguido del HMAC-SHA256
  de `<X-HolyCRM-Timestamp>.<cuerpo sin procesar>`, con el secreto de firma de tu webhook
  como clave.
- Respondé con cualquier estado 2xx. Los tiempos agotados (15 segundos), 408, 429, 5xx y
  errores de red se reintentan (a los 30 segundos, 2, 8 y 32 minutos, y después cada hora,
  6 intentos en total). Cualquier otra respuesta, incluida una redirección, falla en el
  acto: las redirecciones nunca se siguen.

Con el formato por defecto el cuerpo se ve así (`data` depende del evento; `summary` es
una línea lista en el idioma de la iglesia):

```json
{
  "event": "visitor.created",
  "version": 1,
  "id": "k3j2h1g0f9e8d7c",
  "created_at": "2026-10-08T14:00:00.000Z",
  "church": { "id": "abc123def456ghi", "name": "Iglesia Gracia" },
  "data": { "first_name": "Juan", "last_name": "Pérez", "phone": "…", "summary": "Nueva visita: Juan Pérez" }
}
```

Cómo verificar la firma en Node.js:

```js
const crypto = require("crypto");
function verify(rawBody, headers, secret) {
  const ts = headers["x-holycrm-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // más de 5 minutos
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
  const got = String(headers["x-holycrm-signature"] || "");
  return got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}
```

Usá el cuerpo del pedido **sin procesar**, antes de interpretar el JSON.

## Todavía no disponible

No hay una API con claves para leer o cambiar datos de miembros. Si tu iglesia la
necesita, contanos para qué la usarías con el formulario de contacto de
[holycrm.app](https://holycrm.app).
