Dáselo a tu agente de IA

Agregá Noti a tu sistema en minutos, con Claude Code, Cursor, Codex o el agente que uses.

Tu sistema tiene usuarios y pasan cosas en él (un pedido, un turno, un pago). Con Noti podés conectar el teléfono de cada persona con un botón y avisarle cuando algo cambia. Esta página es el prompt que le pasás a tu agente: le dice qué construir, qué no, y cómo comprobar que quedó bien. Es el mismo patrón que ya corre en producción en de confianza.

Cómo usarla

  1. Vos: creá tu empresa en /panel y un token (pestaña «Conectar»). Lo guardás en el gestor de secretos de tu plataforma: no lo pegues en el chat.
  2. Tu agente: copiá el prompt de abajo y pegáselo, o decile «leé https://noti-api-production.up.railway.app/empresas/integrar.md e integrá Noti en este proyecto».
  3. Los dos: revisen la checklist del final antes de dar la tarea por terminada.

El prompt

Es texto plano en Markdown: /empresas/integrar.md (y un índice para agentes en /llms.txt).

# Integrá Noti en tu sistema (guía para tu agente de IA)

> Pegale esta página entera a tu agente (Claude Code, Cursor, Codex…) o decile:
> «Leé https://noti-api-production.up.railway.app/empresas/integrar.md e integrá Noti en este proyecto».
> Está escrita para que la lea una máquina: dice qué construir, qué no, y cómo
> comprobar que quedó bien.

Noti manda notificaciones push al teléfono de la gente. **La app sólo recibe.**
Tu sistema (el de esta empresa) hace tres cosas con la API de Noti:

1. **Vincular** a una persona: le mostrás un botón «Conectar mi Noti» (un link y
   un QR de un solo uso). Cuando lo abre en su teléfono, esa persona queda
   vinculada a tu empresa bajo el `ref` que elegiste (tu id de usuario).
2. **Avisarle** a esa persona por su `ref` cuando pasa algo en tu sistema
   («tu pedido salió», «te asignaron un turno»).
3. **Difundir** a todos los suscriptos o a los de un QR (sucursal, tema).

## Lo que tenés que respetar (es el producto, no un detalle)

- **Un `202` NO significa «entregado».** `POST /send` contesta siempre
  `202 {"id"}`: le llegue el aviso a mil personas, a una o a ninguna (la persona
  no abrió su pase, silenció, se dio de baja, te pasaste de un límite, el `ref`
  no existe). Es a propósito: si la respuesta contara quién recibe, podrías
  deducir quién te cortó, y entonces darse de baja dejaría de ser gratis.
- **No hay estado «conectado» ni «avisos activados».** Noti no te dice si la
  persona abrió el pase ni si después se fue. Tu pantalla puede decir «pediste
  tu enlace · vence el …» y ofrecer «Mandame un aviso de prueba». Nunca
  «conectado», «activo», «recibió» ni «le llegó».
- **No esperes saber quién se dio de baja ni cuántos te siguen hoy.** Sólo
  existe cuántas personas se sumaron *en total* (número que sólo sube) y una
  «salud de la audiencia» en bandas. No construyas nada que dependa de más.
- **Avisar es accesorio.** Un fallo de Noti (caído, lento, sin token) jamás
  puede romper tu operación: crear el pedido, cobrar, cerrar el trabajo. Es
  «mejor esfuerzo»: se loguea el tipo de error y se sigue.
- **Noti es un tercero.** El título y el texto se leen en la pantalla bloqueada
  y pasan por servidores de Noti y de Apple/Google: **nunca** pongas teléfonos,
  direcciones, montos, nombres completos ni datos de salud. Un `#id`, un rubro y
  una zona alcanzan. El detalle vive detrás del botón (CTA) en tu sistema.

## Qué necesitás de un humano (el agente no puede hacerlo)

1. Que cree la empresa en https://noti-api-production.up.railway.app/panel (tarda un rato en aprobarse a mano;
   mientras está «en revisión» no se puede enviar).
2. Que cree un **token** en el panel (pestaña «Conectar») y lo guarde en el
   gestor de secretos de tu plataforma. Empieza con `ntf_` y se muestra una sola
   vez. **No lo pegues en el chat, en el código ni en un archivo versionado.**

El token es de la empresa: con él se manda y se piden pases, no se crean empresas
ni tokens nuevos.

## Variables de entorno

```
NOTI_ENABLED=false            # apagado por defecto: sin esto no se manda nada
NOTI_URL=https://noti-api-production.up.railway.app
NOTI_TOKEN=ntf_...            # secreto
NOTI_APP_BASE_URL=https://tu-sistema.com   # para armar los botones (CTA) de los avisos
```

`/send` y `/company/*` van con `Authorization: Bearer $NOTI_TOKEN`. Timeout corto
(3 s): Noti nunca tiene que frenar tu operación.

## Cliente mínimo (copiá uno, no escribas el tuyo)

Un solo archivo, sin dependencias, con reintentos y modo de pruebas:

- Python 3.9+: https://noti-api-production.up.railway.app/sdk/noti.py
- Node 18+ / TypeScript: https://noti-api-production.up.railway.app/sdk/noti.ts

```python
from noti import Noti, NotiError            # Noti.from_env() lee NOTI_ENABLED / NOTI_URL / NOTI_TOKEN
noti = Noti.from_env()                      # apagado o sin token => no hace nada, no rompe nada
envio = noti.send(ref="usuario-42", title="Tu pedido salió", body="Llega hoy antes de las 18.", emoji="📦",
                  cta_label="Ver el pedido", cta_url="https://tu-sistema.com/pedidos/7", key="pedido-7-salio")
# envio.accepted == "Noti lo aceptó (202)", NO "le llegó"
pase = noti.vincular_persona("usuario-42")  # pase.url (link con QR) / pase.qr_text; levanta NotiError
```

```ts
import { Noti } from "./noti.ts";
const noti = Noti.fromEnv();
await noti.send({ ref: "usuario-42", title: "Tu pedido salió", body: "Llega hoy antes de las 18.", emoji: "📦",
                  ctaLabel: "Ver el pedido", ctaUrl: "https://tu-sistema.com/pedidos/7", key: "pedido-7-salio" });
const pase = await noti.vincularPersona("usuario-42"); // pase.url / pase.qrText
```

Si preferís no copiar el archivo, son dos llamadas HTTP (curl, `fetch`, `requests`):

```
POST https://noti-api-production.up.railway.app/company/connect-links   {"ref": "usuario-42"}            -> 201 {"url", "qr_text", "expires_at"}
POST https://noti-api-production.up.railway.app/send                    {"ref": "usuario-42", "title": "…", "body": "…", "emoji": "📦",
                                       "cta": {"label": "Ver el pedido", "url": "https://…"}}   -> 202 {"id"}
```

Topes: `title` ≤ 100, `body` ≤ 1000, `emoji` exactamente uno (opcional), `cta.label` ≤ 40,
`cta.url` ≤ 2048 con `https://`, `http://`, `mailto:`, `tel:`, `sms:` o `whatsapp:`;
`ref` ≤ 128. Se puede poner uno solo de `ref`, `group`, `to`. Un campo mal armado
da `400`; el `401` es el token. **Pasarse de un límite no da error** (el aviso no
sale): 60 llamadas por minuto por token, 20 difusiones por hora, 5.000 personas por
difusión y un aviso cada 10 s a la misma persona.

## Qué construir

### 1. Un puerto con tres implementaciones

Tu dominio pide «el puerto de Noti» y no sabe quién lo implementa:

- **Http**: el cliente de arriba (el real).
- **Fake**: guarda lo «enviado» en una lista; lo usan tus tests.
- **Disabled**: no hace nada (Noti apagado o sin token). Es lo que se construye si
  falta `NOTI_TOKEN`: **no se levanta nunca una excepción por esto**.

`send` devuelve «Noti lo aceptó» (booleano) y **nunca levanta**. `vincular_persona`
sí puede fallar: hay una persona esperando un link y hay que decirle el motivo.

### 2. El botón «Conectar mi Noti»

En el perfil/ajustes de tu usuario:

1. Un endpoint propio (por ejemplo `POST /noti/me/link`) que llama a
   `vincular_persona(ref)` con el **`ref` sacado de la sesión**, nunca del cuerpo
   del pedido (si no, alguien pide el pase de otro).
2. Muestra el **link** (botón «Abrir en Noti») y el **QR** (`pase.qr_text` dibujado
   con cualquier librería de QR, o simplemente la página `pase.url`, que ya lo trae).
3. Guarda **sólo** cuándo lo pidió y cuándo vence (`expires_at`), para decir
   «pediste tu enlace · vence el …». Un botón «Generar enlace nuevo» sirve para
   cambiar de teléfono (pase nuevo, mismo `ref`; el viejo vence solo).
4. Un botón «Mandame un aviso de prueba» (un `send` al mismo `ref`).
5. Un texto que lo explique: «Noti no nos dice si lo abriste ni si te diste de baja;
   si no te llega el aviso de prueba, generá un enlace nuevo».

**`ref`**: tu id estable y opaco (`usuario-42`), no un mail ni un teléfono si podés
evitarlo: Noti lo guarda. Tiene que ser siempre el mismo para la misma persona.

### 3. Avisos por transición (el patrón que funcionó en producción)

Avisá cuando algo **cambia de estado** (pedido creado → técnico elegido → trabajo
cerrado), no por cada cambio:

- **Dos pasos.** `preparar` dentro de tu transacción, ANTES del commit: decide si
  corresponde avisar, anota la marca de idempotencia y arma los mensajes.
  `despachar` DESPUÉS del commit, en un hilo o cola aparte con timeout corto. Si el
  commit se cae, la marca se cae con él y no se avisa de algo que no pasó.
- **Idempotente: un evento = un aviso.** Tabla `noti_avisos` con
  `UNIQUE (entidad_id, tipo)`; la marca se inserta con `ON CONFLICT DO NOTHING`. Si
  la transición se reintenta (webhook duplicado, usuario que toca dos veces), no sale
  un segundo aviso. `key=` del cliente sólo cubre un proceso: **la garantía es tu UNIQUE**.
- **Best-effort, nunca toca la operación.** El hilo no usa tu base: los destinatarios y
  el texto se resuelven en `preparar`. De un error se loguea el **tipo** (el texto de una
  excepción de red puede traer la URL), nunca el mensaje ni el token.
- **Sin credencial o apagado, `preparar` ni consulta la base.**
- **Textos desde cero** (nada de armar un aviso interpolando campos libres): un emoji,
  voseo o el tono de tu marca, `#id` + rubro + zona, y un botón (CTA) con `https://` a la
  pantalla de tu sistema que muestra el detalle.
- **Un aviso cada 10 s por persona.** Si tu evento puede disparar dos avisos seguidos a la
  misma persona, el segundo se descarta sin error: juntalos o espaciá.

### 4. Difusión y QR por sucursal (opcional)

Para avisos a todos o a un grupo: `send(title=…, body=…)` o `send(group="palermo", …)`.
Los QR se crean con `POST /company/groups {"name": "Sucursal Palermo", "code": "palermo"}`;
la página imprimible de cada uno es `https://noti-api-production.up.railway.app/e/<código>`. Antes de difundir,
**confirmá con una persona el título y el texto**: un envío no se deshace.

## Saber si un aviso se vio (sin romper la privacidad)

`send` devuelve el `id` del aviso. `GET /company/avisos/{id}` (y `GET /company/avisos`)
te dicen **sólo señales positivas**:

- **Aviso a una persona (`ref`)**: `seen_on` con la **fecha** (sin hora) si lo abrió. Si
  es `null`, **no significa nada**: no lo vio todavía, lo silenció, te cortó o nunca se
  vinculó, y Noti no distingue entre eso. No muestres «no lo vio» ni reintentes a
  ciegas: tratá `null` como «sin señal».
- **Aviso a muchos (todos o un QR)**: `seen_count` y `click_count` (clics en el botón),
  **sólo cuando hay al menos `min_audience` (30) vistos**; si no, `null`. No hay
  denominador a propósito: ni «a cuántos se mandó», ni «cuántos faltan».
- **Nunca** hay «entregado», «falló», «silenciado» ni «dado de baja», ni un conteo de
  destinatarios: eso delataría quién cortó. Lo que sí podés es medir interés: cuánta
  gente abre y cuánta toca el botón.

## Recetas cortas

**Aviso de pedido por `ref`** (la receta de de confianza): al crear el pedido,
`preparar` → commit → `despachar`; título «📦 Pedido #7», texto «Gasista · Palermo»,
CTA «Ver el pedido» a tu pantalla. El `ref` es el id del usuario que debe enterarse.

**Difusión**: `noti.send(title="Hoy abrimos hasta las 22", body="…", emoji="🕙")`. Máximo 20
por hora por empresa.

**QR por sucursal**: crear el QR de cada sucursal (`POST /company/groups`), imprimir
`https://noti-api-production.up.railway.app/e/<código>` en el local, y avisar con `group="<código>"`.

## Checklist de verificación (hacela antes de dar la tarea por terminada)

- [ ] Con `NOTI_ENABLED=false` (o sin `NOTI_TOKEN`) todo tu sistema anda igual y nada se manda.
- [ ] Tests con el **Fake**: la transición dispara el aviso exacto una vez; repetirla no
      dispara otro; con `fail_with` / Noti caído la operación termina bien.
- [ ] Un test recorre los textos de los avisos con datos personales cargados (teléfono,
      dirección, nombre) y comprueba que **ninguno** aparece.
- [ ] El `ref` del pase sale de la sesión; hay un test que intenta pedir el de otro usuario.
- [ ] Ninguna pantalla ni log dice «conectado», «activo», «entregado», «recibió» o «le llegó»
      (buscalo con grep).
- [ ] El token no está en el repo ni en logs (`git grep ntf_` sin resultados).
- [ ] Prueba real: pedí un pase, ábrilo en la app Noti de un teléfono, mandá el aviso de
      prueba y mirá que suene. (Si tu empresa sigue «en revisión», no va a salir.)
- [ ] Un `400` de Noti en los logs significa un campo que no cumple los topes: arreglá el
      texto, no reintentes.

## Otras formas de operar Noti

- **MCP** (`https://noti-api-production.up.railway.app/mcp`, con el token): `ver_empresa`, `listar_qrs`, `crear_qr`,
  `vincular_persona`, `enviar_aviso`, `listar_avisos`, `ver_aviso`, `ver_audiencia`. Útil para que tu
  agente haga las pruebas y las altas de QR desde el chat. No crea empresas ni tokens.
- **OpenAPI**: https://noti-api-production.up.railway.app/empresas/openapi.json. **Guía completa para humanos**: https://noti-api-production.up.railway.app/empresas.

Clientes sin dependencias

Un solo archivo por lenguaje, con reintentos, tope de tiempo y un modo de pruebas (FakeNoti) que valida igual que Noti. Copialos a tu proyecto:

curl -O https://noti-api-production.up.railway.app/sdk/noti.py
curl -O https://noti-api-production.up.railway.app/sdk/noti.ts   # Node 23.6+ lo corre directo; si no, tsx o tu compilador

Con Python: from noti import Noti; noti = Noti.from_env() y después noti.vincular_persona("usuario-42") / noti.send(ref="usuario-42", title=…, body=…). Con Node: Noti.fromEnv(), await noti.vincularPersona(…), await noti.send({…}).

Lo que Noti no dice (y tu sistema tampoco tiene que decir)

  • Un 202 es «lo recibí», no «entregado». Nunca vas a saber si le llegó, si lo silenció o si se dio de baja.
  • No hay estado «conectado»: sólo «pediste tu enlace · vence el …».
  • De un aviso sabés lo positivo: a una persona, la fecha en que lo abrió; a muchos, cuántos lo abrieron y cuántos tocaron el botón, si son 30 o más. La ausencia de señal nunca se distingue de «silenció» o «cortó».

Es lo que hace que decir que sí sea fácil: la persona corta sin que nadie se entere. Si tu agente propone algo que dependa de saber más, está equivocado: la guía para humanos explica el porqué.