---
title: "Cody Public API — Push Notifications"
section: "Cody Academy / Public API / Push Notifications"
language: es-MX
updated: 2026-09-23
source: https://docs.cody-tech.com/academy/public-api/push-notifications
markdown: https://docs.cody-tech.com/academy/public-api/push-notifications.md
---

# Cody Public API — Push Notifications

> **Guía de integración para desarrolladores y agentes de IA.** Este documento contiene todo lo necesario para construir una integración que envíe notificaciones push a los usuarios de Cody desde un sistema externo: contexto, reglas obligatorias, contrato de la API, ejemplos en cURL, JavaScript y Python, y una lista de verificación final.
>
> Versión web: https://docs.cody-tech.com/academy/public-api/push-notifications

## Cómo usar este documento con un agente de IA

1. Descarga este archivo y agrégalo al contexto de tu agente (adjúntalo en el chat o guárdalo en tu repositorio, por ejemplo en `docs/cody-push-notifications.md`). Si tu agente puede leer URLs, también puedes darle directamente: https://docs.cody-tech.com/academy/public-api/push-notifications.md
2. Pídele algo concreto, por ejemplo:

   > Usando docs/cody-push-notifications.md como especificación, implementa en nuestro backend un módulo que envíe una notificación push de Cody al vendedor cuando se apruebe un pedido. Respeta todas las reglas obligatorias y valida el resultado con la lista de verificación del final.

3. Revisa el código generado contra la sección **Lista de verificación de la implementación**.

## Contexto para el agente

- **Objetivo:** que un sistema externo (ERP, CRM, BI, automatización) envíe notificaciones push a usuarios de Cody a través de la app móvil Cody Companion.
- **Dónde corre el código:** siempre en un servidor o proceso de backend. Nunca en un navegador ni en una app móvil, porque requiere credenciales secretas.
- **Protocolo:** HTTPS con cuerpos JSON. Todas las rutas son relativas a la URL base de la Public API: `https://api.public.cody-tech.com` (en los ejemplos: `CODY_API_URL`).
- **Autenticación:** encabezado `Authorization: Bearer <access_token>`. El access token vence a la hora y se renueva con Client ID, Client Secret y Refresh Token.
- **Destinatarios:** se identifican por **ID de usuario de Cody**, no por correo ni por número de empleado.
- **Modelo de envío:** asíncrono. `POST /push-notification/send` responde `202` con un ID; la entrega ocurre en segundo plano y el resultado se consulta después con ese ID.

### Reglas obligatorias

1. Lee las credenciales de variables de entorno o de un gestor de secretos. Nunca las escribas en el código, en logs ni en el repositorio.
2. Reutiliza el access token mientras esté vigente y renuévalo unos minutos antes de que venza (por ejemplo, a los 55 minutos).
3. Si una solicitud responde `401`, renueva el token y reintenta **una sola vez**.
4. **No reintentes automáticamente** un `POST /push-notification/send` que ya respondió `202`: cada llamada crea un envío nuevo y los usuarios recibirían duplicados. Ante un error de red sin respuesta, consulta `GET /push-notification/list` antes de reintentar.
5. Elimina IDs duplicados de `target_user_ids` y nunca envíes la lista vacía.
6. Envía `scheduled_at` en ISO 8601 **con zona horaria** (por ejemplo `2026-10-01T09:00:00-06:00` o `2026-10-01T15:00:00Z`).
7. Usa en `deeplink` solo los valores de la tabla de destinos.
8. No envíes `tenant_id`: la organización se toma de la credencial.
9. Guarda el `id` de cada envío junto al registro de tu sistema que lo originó, para dar seguimiento.
10. No consultes el estado en bucle: una consulta a los pocos segundos y, si se miden aperturas, otra más tarde.

## 1. Qué es y para qué sirve

Una notificación push es el aviso que aparece en la pantalla del celular aunque la aplicación esté cerrada. Con esta funcionalidad, cualquier sistema de tu empresa puede pedirle a Cody que envíe uno de esos avisos a los usuarios que elijas, a través de la app **Cody Companion**. Cada notificación lleva un **título** y un **mensaje**, y opcionalmente una **imagen** y un **destino dentro de la app**. Tu sistema recibe un identificador del envío con el que después consulta el resultado.

### Capacidades

- **A una persona o a miles.** La misma solicitud sirve para avisar a un solo usuario o a toda una región: solo cambia la lista de destinatarios.
- **Inmediata o programada.** Envía en el momento o indica fecha y hora; mientras no salga, puedes cancelarla.
- **Lleva a la pantalla correcta.** Al tocar la notificación, la app puede abrir el chat, el coach, el inicio o un anuncio específico.
- **Seguimiento por usuario.** Sabes a quién le llegó, quién la abrió y quién todavía no tiene la app activa.
- **Todos sus dispositivos.** Si una persona usa Cody en más de un celular, la notificación llega a cada uno.
- **Reintentos automáticos.** Si un dispositivo no responde por una falla temporal, Cody vuelve a intentarlo por ti.

### Ejemplos de uso

- **ERP:** Se aprueba un pedido o una devolución → avisar al vendedor responsable.
- **CRM:** Un cliente clave queda sin visita en 15 días → recordar al ejecutivo de cuenta.
- **BI / reportes:** Una tienda cae por debajo de su meta diaria → alertar al gerente de zona.
- **Recursos humanos:** Se publica una capacitación obligatoria → notificar al área completa.
- **Automatización:** Cada lunes a las 8:00 a. m. → enviar el recordatorio de objetivos de la semana.

## 2. Cómo funciona

```mermaid
sequenceDiagram
    autonumber
    participant SYSTEM as Tu sistema
    participant API as Cody Public API
    participant CODY as Plataforma Cody
    participant APP as App Cody Companion
    Note over SYSTEM,APP: Fase 1 · Autenticación
    SYSTEM->>API: Renueva su access token
    API-->>SYSTEM: Access token (vigente 1 hora)
    Note over SYSTEM,APP: Fase 2 · Destinatarios (opcional)
    SYSTEM->>API: ¿Quién tiene la app activa?
    API-->>SYSTEM: Lista de IDs de usuario
    Note over SYSTEM,APP: Fase 3 · Envío
    SYSTEM->>API: Envía título, mensaje y destinatarios
    API->>CODY: Registra el envío
    API-->>SYSTEM: 202 Aceptada + ID del envío
    CODY-)APP: Entrega a cada dispositivo
    Note over SYSTEM,APP: Fase 4 · Interacción del usuario
    APP-)CODY: El usuario la toca y se marca abierta
    Note over SYSTEM,APP: Fase 5 · Seguimiento
    SYSTEM->>API: Consulta el estado con el ID
    API-->>SYSTEM: Estado y resultado por usuario
```

1. **Autenticación.** Tu sistema presenta sus credenciales y obtiene un **access token**: una llave temporal que acompaña cada solicitud y dura una hora.
2. **Respuesta con el token.** Tu sistema lo guarda y lo reutiliza hasta que esté por vencer.
3. **Consulta de destinatarios (opcional).** Puedes preguntar qué usuarios tienen la app lista para recibir notificaciones.
4. **Lista de usuarios.** Cody responde con sus identificadores.
5. **Envío.** Tu sistema manda el título, el mensaje y la lista de destinatarios.
6. **Registro.** Cody guarda el envío y prepara la entrega.
7. **Confirmación inmediata.** Tu sistema recibe la respuesta `202 Aceptada` con el ID del envío, sin esperar a que lleguen todas las notificaciones.
8. **Entrega en segundo plano.** Cody hace llegar la notificación a cada celular de cada destinatario, normalmente en segundos.
9. **Apertura.** Cuando la persona toca la notificación, la app la lleva a la pantalla indicada y Cody registra que la abrió.
10. **Consulta de resultado.** Con el ID del envío, tu sistema pregunta cómo le fue.
11. **Resultado.** Cody devuelve el estado general y el detalle por usuario: entregada, abierta, fallida o sin dispositivo.

> **Enviar no es lo mismo que entregar.** La respuesta `202` confirma que Cody aceptó la solicitud, no que ya llegó a todos. Para conocer el resultado final, consulta el envío con su ID.

## 3. Antes de empezar (precondiciones)

1. **Tu organización está activa en Cody.** Con usuarios dados de alta. Las notificaciones solo pueden enviarse a usuarios de **tu propia organización**.
2. **Un administrador con acceso a Cody AMS.** Es quien crea la credencial de integración desde la consola de administración, en **Integraciones API**.
3. **Una credencial de servicio asociada a un administrador.** La credencial actúa en nombre del usuario que elijas al crearla, y **ese usuario debe tener rol de administrador**. Si no lo tiene, la API rechaza el envío.
4. **La URL base de la Public API.** Es `https://api.public.cody-tech.com`. Todas las rutas de esta guía son relativas a esa URL; por ejemplo, el envío se hace a `https://api.public.cody-tech.com/push-notification/send`.
5. **Usuarios con la app Cody Companion lista.** Para que una persona reciba notificaciones debe **tener la app instalada, haber iniciado sesión y aceptado el permiso de notificaciones** en su celular. Si cierra sesión, deja de recibirlas en ese dispositivo hasta que vuelva a entrar.
6. **Un sistema capaz de hacer solicitudes HTTPS.** Cualquier lenguaje o herramienta que pueda enviar solicitudes HTTPS con JSON y guardar secretos de forma segura: un backend propio, un ERP con conectores, o plataformas como Power Automate, Make, Zapier o n8n.

> Una vez que tengas tu credencial, `GET /user-device/users` te dice cuántos y cuáles usuarios pueden recibir notificaciones hoy. Es la mejor prueba antes del primer envío.

## 4. Integración con tus sistemas

Concentra toda la comunicación con Cody en un solo componente (un **conector**) que se encargue de las credenciales, de renovar el token, de traducir tus usuarios a IDs de Cody, de enviar y de guardar el resultado. El resto de tus procesos solo le pide “avisa a estas personas”.

### Paso 1. Crear la credencial de servicio en Cody AMS

Un administrador de tu organización:

1. Entra a **Cody AMS** y abre **Integraciones API** en el menú lateral.
2. Hace clic en **Nueva credencial de servicio**.
3. Escribe un nombre que identifique la integración, por ejemplo “ERP – Notificaciones”.
4. Elige el usuario en cuyo nombre actuará la integración. **Debe tener rol de administrador.**
5. Hace clic en **Crear credencial** y copia los cuatro valores: **Client ID**, **Client Secret**, **Access Token** y **Refresh Token**. Solo se muestran una vez; si se pierden, se crea una credencial nueva y se elimina la anterior.

### Paso 2. Guardar las credenciales de forma segura

- Gestor de secretos o variables de entorno del servidor; nunca en el código ni en un repositorio.
- Solo desde un servidor; nunca en páginas web, apps móviles ni hojas de cálculo compartidas.
- Una credencial distinta por sistema integrado, para poder eliminar una sin afectar a las demás.

```bash
# URL base de la Public API de Cody.
CODY_API_URL="https://api.public.cody-tech.com"

# Valores entregados al crear la credencial de servicio en Cody AMS.
# Guárdalos en tu gestor de secretos; nunca en el código fuente.
CODY_CLIENT_ID="<client-id>"
CODY_CLIENT_SECRET="<client-secret>"
CODY_REFRESH_TOKEN="<refresh-token>"
CODY_ACCESS_TOKEN="<access-token>"   # vigente por 1 hora
```

### Paso 3. Autenticarse y renovar el token

Cada solicitud lleva el access token en el encabezado:

```http
Authorization: Bearer <access_token>
Content-Type: application/json
```

El access token vence a la hora. Para obtener uno nuevo usa `POST /auth/oauth/v2/refresh`. El refresh token no cambia: consérvalo junto con tus credenciales.

#### cURL

```bash
curl -X POST "$CODY_API_URL/auth/oauth/v2/refresh" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "'"$CODY_CLIENT_ID"'",
    "client_secret": "'"$CODY_CLIENT_SECRET"'",
    "refresh_token": "'"$CODY_REFRESH_TOKEN"'"
  }'
```

#### JavaScript

```javascript
const response = await fetch(`${process.env.CODY_API_URL}/auth/oauth/v2/refresh`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    client_id: process.env.CODY_CLIENT_ID,
    client_secret: process.env.CODY_CLIENT_SECRET,
    refresh_token: process.env.CODY_REFRESH_TOKEN,
  }),
})
const { access_token, expires_in } = await response.json()
```

#### Python

```python
import os
import requests

response = requests.post(
    f"{os.environ['CODY_API_URL']}/auth/oauth/v2/refresh",
    json={
        "client_id": os.environ["CODY_CLIENT_ID"],
        "client_secret": os.environ["CODY_CLIENT_SECRET"],
        "refresh_token": os.environ["CODY_REFRESH_TOKEN"],
    },
    timeout=30,
)
response.raise_for_status()
access_token = response.json()["access_token"]
```

Respuesta 200:

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

### Paso 4. Relacionar tus usuarios con los de Cody

Las notificaciones se dirigen por ID de usuario de Cody. Arma una tabla de equivalencias (por ejemplo, por correo electrónico) y actualízala periódicamente; una vez al día suele bastar.

#### cURL

```bash
curl "$CODY_API_URL/user/list?skip=0&limit=1000" \
  -H "Authorization: Bearer $CODY_ACCESS_TOKEN"
```

#### JavaScript

```javascript
const response = await fetch(`${process.env.CODY_API_URL}/user/list?skip=0&limit=1000`, {
  headers: { Authorization: `Bearer ${accessToken}` },
})
const users = await response.json()

// Tabla de equivalencias: correo en tu sistema → ID de usuario en Cody
const codyIdByEmail = new Map(users.map((user) => [user.email.toLowerCase(), user.id]))
```

#### Python

```python
response = requests.get(
    f"{CODY_API_URL}/user/list",
    params={"skip": 0, "limit": 1000},
    headers={"Authorization": f"Bearer {access_token}"},
    timeout=30,
)
response.raise_for_status()
users = response.json()

# Tabla de equivalencias: correo en tu sistema → ID de usuario en Cody
cody_id_by_email = {user["email"].lower(): user["id"] for user in users}
```

Respuesta 200 (extracto):

```json
[
  {
    "id": "664f5f3c2f4f5e3b1c2a9d10",
    "name": "Ana",
    "lastname": "López",
    "username": "ana.lopez",
    "email": "ana.lopez@tuempresa.com"
  },
  {
    "id": "664f5f3c2f4f5e3b1c2a9d11",
    "name": "Carlos",
    "lastname": "Méndez",
    "username": "carlos.mendez",
    "email": "carlos.mendez@tuempresa.com"
  }
]
```

El listado se entrega por páginas de hasta 1,000 usuarios: si hay más, repite la consulta aumentando `skip` (0, 1000, 2000…) hasta que la respuesta llegue vacía.

Para saber quiénes pueden recibir notificaciones hoy:

#### cURL

```bash
curl "$CODY_API_URL/user-device/users" \
  -H "Authorization: Bearer $CODY_ACCESS_TOKEN"
```

#### JavaScript

```javascript
const response = await fetch(`${process.env.CODY_API_URL}/user-device/users`, {
  headers: { Authorization: `Bearer ${accessToken}` },
})
const { data } = await response.json()
console.log(`${data.count} usuarios pueden recibir notificaciones`)
```

#### Python

```python
response = requests.get(
    f"{CODY_API_URL}/user-device/users",
    headers={"Authorization": f"Bearer {access_token}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
print(f"{data['count']} usuarios pueden recibir notificaciones")
```

Respuesta 200:

```json
{
  "ok": true,
  "data": {
    "user_ids": [
      "664f5f3c2f4f5e3b1c2a9d10",
      "664f5f3c2f4f5e3b1c2a9d11"
    ],
    "count": 2
  }
}
```

### Paso 5. Enviar la notificación

Con el token vigente y la lista de IDs, llama a `POST /push-notification/send` (campos en la sección Referencia; casos completos en Ejemplos). Guarda el `id` de la respuesta.

### Paso 6. Dar seguimiento al resultado

Unos segundos después, consulta `GET /push-notification/{id}`. Los usuarios con estado `no_device` todavía no tienen la app lista: puedes avisarles por otro canal e invitarlos a instalarla. Para medir aperturas, vuelve a consultar más tarde.

### Conector de referencia

Resuelve la renovación del token, el reintento único ante `401`, la eliminación de duplicados y el formato de fechas. Tómalo como punto de partida.

#### JavaScript (Node.js 18+)

```javascript
// cody-push.js — conector mínimo para enviar notificaciones desde tu sistema.
const API = process.env.CODY_API_URL

let accessToken = process.env.CODY_ACCESS_TOKEN ?? null
let expiresAt = 0 // se fuerza la primera renovación

async function renewToken() {
  const response = await fetch(`${API}/auth/oauth/v2/refresh`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      client_id: process.env.CODY_CLIENT_ID,
      client_secret: process.env.CODY_CLIENT_SECRET,
      refresh_token: process.env.CODY_REFRESH_TOKEN,
    }),
  })
  if (!response.ok) throw new Error(`No se pudo renovar el token (${response.status})`)
  const body = await response.json()
  accessToken = body.access_token
  // Renovamos 5 minutos antes de que expire
  expiresAt = Date.now() + (body.expires_in - 300) * 1000
}

async function codyRequest(path, options = {}, retried = false) {
  if (!accessToken || Date.now() >= expiresAt) await renewToken()

  const response = await fetch(`${API}${path}`, {
    ...options,
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
      ...options.headers,
    },
  })

  // Token vencido o revocado a mitad del camino: renueva y reintenta una sola vez
  if (response.status === 401 && !retried) {
    await renewToken()
    return codyRequest(path, options, true)
  }
  if (!response.ok) {
    throw new Error(`Cody respondió ${response.status}: ${await response.text()}`)
  }
  return response.json()
}

export async function sendPush({ title, body, userIds, imageUrl, deeplink, scheduledAt }) {
  const { data } = await codyRequest("/push-notification/send", {
    method: "POST",
    body: JSON.stringify({
      title,
      body,
      target_user_ids: [...new Set(userIds)],
      image_url: imageUrl,
      deeplink,
      scheduled_at: scheduledAt?.toISOString(),
    }),
  })
  return data // { id, status }
}

export async function getPushStatus(pushId) {
  const { data } = await codyRequest(`/push-notification/${pushId}`)
  return data // { notification, deliveries }
}

export async function usersWithApp() {
  const { data } = await codyRequest("/user-device/users")
  return data.user_ids
}
```

#### Python 3.9+

```python
# cody_push.py — conector mínimo para enviar notificaciones desde tu sistema.
import os
import time
from datetime import datetime
from typing import Optional

import requests

API = os.environ["CODY_API_URL"]


class CodyPush:
    def __init__(self) -> None:
        self._access_token: Optional[str] = None
        self._expires_at = 0.0  # se fuerza la primera renovación

    def _renew_token(self) -> None:
        response = requests.post(
            f"{API}/auth/oauth/v2/refresh",
            json={
                "client_id": os.environ["CODY_CLIENT_ID"],
                "client_secret": os.environ["CODY_CLIENT_SECRET"],
                "refresh_token": os.environ["CODY_REFRESH_TOKEN"],
            },
            timeout=30,
        )
        response.raise_for_status()
        body = response.json()
        self._access_token = body["access_token"]
        # Renovamos 5 minutos antes de que expire
        self._expires_at = time.time() + body["expires_in"] - 300

    def _request(self, method: str, path: str, retried: bool = False, **kwargs):
        if not self._access_token or time.time() >= self._expires_at:
            self._renew_token()

        response = requests.request(
            method,
            f"{API}{path}",
            headers={"Authorization": f"Bearer {self._access_token}"},
            timeout=30,
            **kwargs,
        )
        # Token vencido o revocado a mitad del camino: renueva y reintenta una sola vez
        if response.status_code == 401 and not retried:
            self._renew_token()
            return self._request(method, path, retried=True, **kwargs)
        response.raise_for_status()
        return response.json()

    def send(
        self,
        title: str,
        body: str,
        user_ids: list[str],
        image_url: Optional[str] = None,
        deeplink: Optional[str] = None,
        scheduled_at: Optional[datetime] = None,  # siempre con zona horaria
    ) -> dict:
        payload = {
            "title": title,
            "body": body,
            "target_user_ids": list(dict.fromkeys(user_ids)),
            "image_url": image_url,
            "deeplink": deeplink,
            "scheduled_at": scheduled_at.isoformat() if scheduled_at else None,
        }
        payload = {k: v for k, v in payload.items() if v is not None}
        return self._request("POST", "/push-notification/send", json=payload)["data"]

    def status(self, push_id: str) -> dict:
        return self._request("GET", f"/push-notification/{push_id}")["data"]

    def users_with_app(self) -> list[str]:
        return self._request("GET", "/user-device/users")["data"]["user_ids"]


# Uso
# cody = CodyPush()
# envio = cody.send("Tu pedido fue aprobado", "El pedido #4821 ya está en ruta.", ["664f5f3c2f4f5e3b1c2a9d10"])
# print(cody.status(envio["id"]))
```

### Patrones de integración

- **Disparada por un evento:** algo pasa en tu sistema y se llama al conector con los destinatarios afectados. Ideal para avisos individuales y operativos.
- **Campañas programadas:** un proceso calendarizado arma la lista y envía una notificación a muchos usuarios, o la programa con `scheduled_at`.
- **Herramientas sin código:** en Power Automate, Make, Zapier o n8n, usa el módulo de solicitud HTTP con los datos de los ejemplos cURL y guarda las credenciales en el almacén de secretos de la herramienta.

## 5. Ejemplos de envío

Todos asumen un access token vigente en `CODY_ACCESS_TOKEN` y la URL base (`https://api.public.cody-tech.com`) en `CODY_API_URL`.

### Ejemplo 1. Enviar a un usuario

#### cURL

```bash
curl -X POST "$CODY_API_URL/push-notification/send" \
  -H "Authorization: Bearer $CODY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Tu pedido fue aprobado",
    "body": "El pedido #4821 ya está en ruta. Revisa los detalles con tu coach.",
    "target_user_ids": ["664f5f3c2f4f5e3b1c2a9d10"]
  }'
```

#### JavaScript

```javascript
const response = await fetch(`${process.env.CODY_API_URL}/push-notification/send`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    title: "Tu pedido fue aprobado",
    body: "El pedido #4821 ya está en ruta. Revisa los detalles con tu coach.",
    target_user_ids: ["664f5f3c2f4f5e3b1c2a9d10"],
  }),
})

if (response.status !== 202) {
  throw new Error(`Cody respondió ${response.status}: ${await response.text()}`)
}
const { data } = await response.json()
console.log("Envío registrado:", data.id) // guarda este ID para dar seguimiento
```

#### Python

```python
response = requests.post(
    f"{CODY_API_URL}/push-notification/send",
    headers={"Authorization": f"Bearer {access_token}"},
    json={
        "title": "Tu pedido fue aprobado",
        "body": "El pedido #4821 ya está en ruta. Revisa los detalles con tu coach.",
        "target_user_ids": ["664f5f3c2f4f5e3b1c2a9d10"],
    },
    timeout=30,
)
if response.status_code != 202:
    raise RuntimeError(f"Cody respondió {response.status_code}: {response.text}")

push_id = response.json()["data"]["id"]  # guarda este ID para dar seguimiento
print("Envío registrado:", push_id)
```

Respuesta 202:

```json
{
  "ok": true,
  "data": {
    "id": "6710a2c4e8b1f23d4c5a6b7e",
    "status": "sending"
  }
}
```

### Ejemplo 2. Enviar a un grupo de usuarios

La solicitud es idéntica: solo cambia la lista de IDs. Este ejemplo agrega imagen y destino dentro de la app. Los usuarios sin la app no bloquean el envío: aparecen como `no_device` en el seguimiento.

#### cURL

```bash
curl -X POST "$CODY_API_URL/push-notification/send" \
  -H "Authorization: Bearer $CODY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Nuevo reto de la semana",
    "body": "Completa el reto de exhibición antes del viernes y suma puntos para tu equipo.",
    "image_url": "https://cdn.tuempresa.com/campanas/reto-exhibicion.png",
    "deeplink": "codycompanion://coach",
    "target_user_ids": [
      "664f5f3c2f4f5e3b1c2a9d10",
      "664f5f3c2f4f5e3b1c2a9d11",
      "664f5f3c2f4f5e3b1c2a9d12"
    ]
  }'
```

#### JavaScript

```javascript
// Los IDs salen de tu propio sistema: una región, un equipo, una lista de campaña…
const equipoNorte = ["664f5f3c2f4f5e3b1c2a9d10", "664f5f3c2f4f5e3b1c2a9d11", "664f5f3c2f4f5e3b1c2a9d12"]

const response = await fetch(`${process.env.CODY_API_URL}/push-notification/send`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    title: "Nuevo reto de la semana",
    body: "Completa el reto de exhibición antes del viernes y suma puntos para tu equipo.",
    image_url: "https://cdn.tuempresa.com/campanas/reto-exhibicion.png",
    deeplink: "codycompanion://coach",
    target_user_ids: [...new Set(equipoNorte)], // sin duplicados
  }),
})
const { data } = await response.json()
console.log("Envío registrado:", data.id)
```

#### Python

```python
# Los IDs salen de tu propio sistema: una región, un equipo, una lista de campaña…
equipo_norte = ["664f5f3c2f4f5e3b1c2a9d10", "664f5f3c2f4f5e3b1c2a9d11", "664f5f3c2f4f5e3b1c2a9d12"]

response = requests.post(
    f"{CODY_API_URL}/push-notification/send",
    headers={"Authorization": f"Bearer {access_token}"},
    json={
        "title": "Nuevo reto de la semana",
        "body": "Completa el reto de exhibición antes del viernes y suma puntos para tu equipo.",
        "image_url": "https://cdn.tuempresa.com/campanas/reto-exhibicion.png",
        "deeplink": "codycompanion://coach",
        "target_user_ids": list(dict.fromkeys(equipo_norte)),  # sin duplicados
    },
    timeout=30,
)
response.raise_for_status()
print("Envío registrado:", response.json()["data"]["id"])
```

### Ejemplo 3. Enviar a todos los usuarios con la app

Primero se consulta quién tiene la app activa y después se envía a esa lista en una sola solicitud. Para varios miles de personas, conviene dividir la lista en lotes (por ejemplo, de 1,000 usuarios); cada lote es un envío con su propio ID.

#### JavaScript

```javascript
const api = process.env.CODY_API_URL
const headers = {
  Authorization: `Bearer ${accessToken}`,
  "Content-Type": "application/json",
}

// 1. ¿Quién puede recibir notificaciones hoy?
const devices = await fetch(`${api}/user-device/users`, { headers }).then((r) => r.json())
const recipients = devices.data.user_ids

if (recipients.length === 0) {
  console.log("Ningún usuario tiene la app activa todavía.")
} else {
  // 2. Enviar a todos ellos en una sola solicitud
  const response = await fetch(`${api}/push-notification/send`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      title: "Cierre de mes",
      body: "Hoy es el último día para registrar tus ventas del mes.",
      deeplink: "codycompanion://chat",
      target_user_ids: recipients,
    }),
  })
  const { data } = await response.json()
  console.log(`Envío ${data.id} registrado para ${recipients.length} usuarios`)
}
```

#### Python

```python
headers = {"Authorization": f"Bearer {access_token}"}

# 1. ¿Quién puede recibir notificaciones hoy?
devices = requests.get(f"{CODY_API_URL}/user-device/users", headers=headers, timeout=30)
devices.raise_for_status()
recipients = devices.json()["data"]["user_ids"]

if not recipients:
    print("Ningún usuario tiene la app activa todavía.")
else:
    # 2. Enviar a todos ellos en una sola solicitud
    response = requests.post(
        f"{CODY_API_URL}/push-notification/send",
        headers=headers,
        json={
            "title": "Cierre de mes",
            "body": "Hoy es el último día para registrar tus ventas del mes.",
            "deeplink": "codycompanion://chat",
            "target_user_ids": recipients,
        },
        timeout=30,
    )
    response.raise_for_status()
    print(f"Envío {response.json()['data']['id']} registrado para {len(recipients)} usuarios")
```

### Ejemplo 4. Programar un envío y cancelarlo

Con `scheduled_at` la respuesta llega con estado `scheduled` y Cody la envía al llegar la hora (puede tardar hasta un par de minutos después). Una fecha pasada se envía de inmediato.

#### cURL

```bash
# Programada para el 1 de octubre de 2026 a las 9:00 a. m., hora del centro de México (UTC-6)
curl -X POST "$CODY_API_URL/push-notification/send" \
  -H "Authorization: Bearer $CODY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "¡Arranca la temporada de octubre!",
    "body": "Ya están disponibles los nuevos retos. Entra y conoce las metas del mes.",
    "deeplink": "codycompanion://home",
    "target_user_ids": ["664f5f3c2f4f5e3b1c2a9d10", "664f5f3c2f4f5e3b1c2a9d11"],
    "scheduled_at": "2026-10-01T09:00:00-06:00"
  }'
```

#### JavaScript

```javascript
const response = await fetch(`${process.env.CODY_API_URL}/push-notification/send`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    title: "¡Arranca la temporada de octubre!",
    body: "Ya están disponibles los nuevos retos. Entra y conoce las metas del mes.",
    deeplink: "codycompanion://home",
    target_user_ids: ["664f5f3c2f4f5e3b1c2a9d10", "664f5f3c2f4f5e3b1c2a9d11"],
    // Siempre con zona horaria. toISOString() la expresa en UTC ("Z").
    scheduled_at: new Date("2026-10-01T09:00:00-06:00").toISOString(),
  }),
})
const { data } = await response.json()
console.log(data) // { id: "...", status: "scheduled" }
```

#### Python

```python
from datetime import datetime
from zoneinfo import ZoneInfo

envio = datetime(2026, 10, 1, 9, 0, tzinfo=ZoneInfo("America/Mexico_City"))

response = requests.post(
    f"{CODY_API_URL}/push-notification/send",
    headers={"Authorization": f"Bearer {access_token}"},
    json={
        "title": "¡Arranca la temporada de octubre!",
        "body": "Ya están disponibles los nuevos retos. Entra y conoce las metas del mes.",
        "deeplink": "codycompanion://home",
        "target_user_ids": ["664f5f3c2f4f5e3b1c2a9d10", "664f5f3c2f4f5e3b1c2a9d11"],
        "scheduled_at": envio.isoformat(),  # "2026-10-01T09:00:00-06:00"
    },
    timeout=30,
)
response.raise_for_status()
print(response.json()["data"])  # {'id': '...', 'status': 'scheduled'}
```

Mientras siga en `scheduled`, se puede cancelar:

#### cURL

```bash
curl -X POST "$CODY_API_URL/push-notification/6710a2c4e8b1f23d4c5a6b7e/cancel" \
  -H "Authorization: Bearer $CODY_ACCESS_TOKEN"
```

#### JavaScript

```javascript
const response = await fetch(
  `${process.env.CODY_API_URL}/push-notification/${pushId}/cancel`,
  { method: "POST", headers: { Authorization: `Bearer ${accessToken}` } }
)
if (response.status === 409) {
  console.log("Ya no se puede cancelar: la notificación ya se envió.")
}
```

#### Python

```python
response = requests.post(
    f"{CODY_API_URL}/push-notification/{push_id}/cancel",
    headers={"Authorization": f"Bearer {access_token}"},
    timeout=30,
)
if response.status_code == 409:
    print("Ya no se puede cancelar: la notificación ya se envió.")
```

Respuesta 200:

```json
{
  "ok": true,
  "message": "Push notification canceled"
}
```

### Ejemplo 5. Consultar el resultado de un envío

#### cURL

```bash
curl "$CODY_API_URL/push-notification/6710a2c4e8b1f23d4c5a6b7e" \
  -H "Authorization: Bearer $CODY_ACCESS_TOKEN"
```

#### JavaScript

```javascript
const response = await fetch(`${process.env.CODY_API_URL}/push-notification/${pushId}`, {
  headers: { Authorization: `Bearer ${accessToken}` },
})
const { data } = await response.json()
const { status, stats } = data.notification

console.log(`Estado: ${status}`)
console.log(`Recibieron: ${stats.sent} dispositivos · Abrieron: ${stats.opened}`)

const sinApp = data.deliveries.filter((d) => d.status === "no_device").map((d) => d.user_id)
if (sinApp.length) console.log("Usuarios sin la app activa:", sinApp)
```

#### Python

```python
response = requests.get(
    f"{CODY_API_URL}/push-notification/{push_id}",
    headers={"Authorization": f"Bearer {access_token}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]
notification = data["notification"]

print(f"Estado: {notification['status']}")
print(f"Recibieron: {notification['stats']['sent']} dispositivos · Abrieron: {notification['stats']['opened']}")

sin_app = [d["user_id"] for d in data["deliveries"] if d["status"] == "no_device"]
if sin_app:
    print("Usuarios sin la app activa:", sin_app)
```

Respuesta 200 (simplificada):

```json
{
  "ok": true,
  "data": {
    "notification": {
      "id": "6710a2c4e8b1f23d4c5a6b7e",
      "title": "Nuevo reto de la semana",
      "body": "Completa el reto de exhibición antes del viernes y suma puntos para tu equipo.",
      "image_url": "https://cdn.tuempresa.com/campanas/reto-exhibicion.png",
      "deeplink": "codycompanion://coach",
      "status": "sent",
      "scheduled_at": null,
      "sent_at": "2026-09-23T16:02:11Z",
      "stats": {
        "targeted_users": 3,
        "devices": 3,
        "sent": 3,
        "failed": 0,
        "opened": 1,
        "no_device": 1
      },
      "created_at": "2026-09-23T16:02:10Z"
    },
    "deliveries": [
      { "user_id": "664f5f3c2f4f5e3b1c2a9d10", "status": "opened",    "sent_at": "2026-09-23T16:02:11Z", "opened_at": "2026-09-23T16:05:40Z" },
      { "user_id": "664f5f3c2f4f5e3b1c2a9d10", "status": "sent",      "sent_at": "2026-09-23T16:02:11Z", "opened_at": null },
      { "user_id": "664f5f3c2f4f5e3b1c2a9d11", "status": "sent",      "sent_at": "2026-09-23T16:02:11Z", "opened_at": null },
      { "user_id": "664f5f3c2f4f5e3b1c2a9d12", "status": "no_device", "sent_at": "2026-09-23T16:02:11Z", "opened_at": null }
    ]
  }
}
```

En este resultado se dirigió a 3 usuarios: el primero tiene dos celulares (aparece dos veces) y ya abrió la notificación en uno; el segundo la recibió sin abrirla; el tercero no tiene la app activa. Para listar todos los envíos, del más reciente al más antiguo:

```bash
curl "$CODY_API_URL/push-notification/list" \
  -H "Authorization: Bearer $CODY_ACCESS_TOKEN"
```

## 6. Referencia

### Rutas

| Método y ruta | Para qué sirve |
| --- | --- |
| `POST /auth/oauth/v2/refresh` | Obtener un access token nuevo. |
| `GET /user/list` | Listar los usuarios de tu organización (para armar tu tabla de equivalencias). |
| `GET /user-device/users` | Listar los usuarios que pueden recibir notificaciones hoy. |
| `POST /push-notification/send` | Crear un envío, inmediato o programado. |
| `GET /push-notification/list` | Listar los envíos de tu organización, del más reciente al más antiguo. |
| `GET /push-notification/{id}` | Consultar el estado y el detalle por usuario de un envío. |
| `POST /push-notification/{id}/cancel` | Cancelar un envío programado que todavía no sale. |

### Campos de `POST /push-notification/send`

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `title` | Texto | Sí | Título visible de la notificación. Recomendado: hasta 50 caracteres para que no se corte en pantalla. |
| `body` | Texto | Sí | Mensaje de la notificación. Recomendado: hasta 150 caracteres. |
| `target_user_ids` | Lista de textos | Sí | IDs de usuario de Cody que recibirán la notificación. Mínimo uno; evita duplicados. Los IDs que no pertenezcan a tu organización no la reciben. |
| `image_url` | Texto (URL) | No | Dirección pública (https) de una imagen JPG o PNG. Es un complemento: el mensaje debe entenderse sin ella, porque algunos teléfonos no la muestran. |
| `deeplink` | Texto | No | Pantalla que se abre al tocar la notificación. Usa uno de los valores de la tabla de destinos. Sin este campo, la app abre en su pantalla habitual. |
| `scheduled_at` | Fecha ISO 8601 | No | Fecha y hora del envío, con zona horaria. Sin este campo (o con una fecha pasada), se envía de inmediato. |
| `announcement_id` | Texto | No | Relaciona el envío con un anuncio publicado en Cody. Déjalo vacío si tu notificación no corresponde a un anuncio. |
| `tenant_id` | Texto | No | No lo envíes: Cody toma tu organización automáticamente de tu credencial. |

### Destinos dentro de la app (`deeplink`)

| Valor | Qué abre | Úsalo para |
| --- | --- | --- |
| `codycompanion://home` | Pantalla de inicio de la app. | Avisos generales, arranque de temporada. |
| `codycompanion://chat` | Conversación con el agente. | Invitar a registrar algo o a pedir ayuda. |
| `codycompanion://coach` | Sección del coach. | Retos, metas y seguimiento de desempeño. |
| `codycompanion://announcement/{id}` | Inicio, abriendo el anuncio indicado. | Reforzar un anuncio ya publicado en Cody. |

Un valor distinto a los de esta tabla no produce error, pero la notificación solo abrirá la app sin llevar a ninguna pantalla en particular.

### Estados de un envío

| Estado | Qué significa |
| --- | --- |
| `scheduled` | Programada; espera la fecha y hora indicadas. Es el único estado que se puede cancelar. |
| `sending` | La entrega está en curso. |
| `sent` | Entregada a todos los dispositivos disponibles. Puede incluir usuarios sin dispositivo, que no cuentan como falla. |
| `partially_failed` | Llegó a una parte de los dispositivos y otros fallaron. Cody reintenta automáticamente las fallas temporales. |
| `failed` | No llegó a ningún dispositivo. Si la falla es temporal, Cody la reintenta automáticamente. |
| `canceled` | Cancelada antes de enviarse. |

### Estados por destinatario (`deliveries[].status`)

| Estado | Qué significa | Qué hacer |
| --- | --- | --- |
| `sent` | Llegó al celular. | Nada; puede pasar a abierta. |
| `opened` | La persona tocó la notificación. | Nada; es el mejor resultado. |
| `failed` | No se pudo entregar a ese dispositivo (por ejemplo, la app se desinstaló). | Si persiste, pide al usuario reinstalar la app e iniciar sesión. |
| `no_device` | El usuario no tiene la app activa en ningún celular. | Invítalo a instalar Cody Companion, iniciar sesión y permitir notificaciones. |

### Contadores (`notification.stats`)

| Campo | Qué significa |
| --- | --- |
| `targeted_users` | Usuarios en la lista de destinatarios. |
| `devices` | Celulares alcanzados. |
| `sent` | Entregas exitosas. |
| `failed` | Entregas fallidas. |
| `opened` | Notificaciones que la persona tocó. |
| `no_device` | Usuarios sin la app activa. |

### Códigos de respuesta

| Código | Qué significa | Cómo resolverlo |
| --- | --- | --- |
| `200 OK` | Consulta o cancelación exitosa. | — |
| `202 Accepted` | Envío aceptado; la entrega continúa en segundo plano. | Guarda el ID y consulta el resultado. |
| `400 Bad Request` | La solicitud tiene un dato inválido. | Revisa el formato de los campos contra la tabla de esta guía. |
| `401 Unauthorized` | Falta el token, es inválido o ya venció. | Renueva el access token y reintenta una vez. |
| `403 Forbidden` | Tu credencial no tiene permiso para esta acción, o el envío pertenece a otra organización. | Verifica que la credencial esté asociada a un usuario administrador. |
| `404 Not Found` | El envío no existe. | Revisa el ID. |
| `409 Conflict` | Intentaste cancelar un envío que ya no está programado. | Solo se cancelan envíos en estado scheduled. |
| `422 Unprocessable Entity` | Falta un campo obligatorio o tiene un formato incorrecto (por ejemplo, la lista de destinatarios vacía). | El detalle de la respuesta indica qué campo corregir. |
| `5xx` | Error temporal de la plataforma. | Espera unos momentos y reintenta. En envíos, confirma primero con la lista que no se haya registrado. |

## 7. Lista de verificación de la implementación

- [ ] Las credenciales se leen de variables de entorno o de un gestor de secretos, y no aparecen en código, logs ni repositorio.
- [ ] El código corre solo en backend.
- [ ] El access token se reutiliza y se renueva antes de vencer.
- [ ] Ante un `401` se renueva el token y se reintenta una sola vez.
- [ ] Un envío que respondió `202` nunca se reintenta automáticamente.
- [ ] `target_user_ids` no va vacío y no tiene duplicados.
- [ ] Los usuarios de tu sistema se traducen a IDs de Cody con una tabla de equivalencias actualizada.
- [ ] `scheduled_at`, si se usa, lleva zona horaria.
- [ ] `deeplink`, si se usa, es uno de los valores documentados.
- [ ] No se envía `tenant_id`.
- [ ] Se guarda el `id` de cada envío junto al registro que lo originó.
- [ ] Se consulta el resultado y se atienden los usuarios `no_device`.
- [ ] Los errores `403`, `409` y `422` se registran con el detalle de la respuesta.
- [ ] Se probó primero enviando solo al ID de un usuario de prueba propio.

## 8. Preguntas frecuentes

**¿Cuánto tarda en llegar una notificación?**

Normalmente unos segundos después de recibir la respuesta 202. Si el celular está apagado o sin conexión, se entrega cuando vuelve a conectarse, según las reglas del sistema operativo del teléfono.

**¿Puedo enviar a usuarios de otra organización?**

No. Cada credencial solo puede enviar a los usuarios de su propia organización. Los IDs ajenos simplemente no reciben la notificación.

**¿Qué pasa si un usuario tiene la app en dos celulares?**

Recibe la notificación en ambos. En el seguimiento verás una entrada por cada celular.

**¿Puedo borrar una notificación que ya se envió?**

No. Una vez entregada, la notificación ya está en el celular de la persona. Lo único que se puede cancelar es un envío programado que todavía no sale.

**¿Cuándo se marca una notificación como abierta?**

Cuando la persona la toca y se abre la app. Si la descarta sin tocarla, queda como entregada.

**¿Necesito reintentar si algunos dispositivos fallan?**

No. Cody reintenta automáticamente las fallas temporales. Las fallas permanentes (por ejemplo, una app desinstalada) no se reintentan; el usuario debe volver a instalar la app e iniciar sesión.

**¿Cómo pruebo sin molestar a mis usuarios?**

Instala la app Cody Companion en tu propio celular con un usuario de tu organización y haz tus primeros envíos solo a tu ID. Cuando veas la notificación y el seguimiento correcto, pasa a tus listas reales.

**¿Qué hago si mi credencial se expone por error?**

Elimínala de inmediato desde Integraciones API en Cody AMS, crea una nueva y actualiza tu sistema. Eliminar una credencial no afecta a las demás integraciones.

---

Guía de Cody Academy · Public API. Última actualización: 2026-09-23. Versión web: https://docs.cody-tech.com/academy/public-api/push-notifications
