# Guía rápida de la API REST de Shager: de una clave nueva a una publicación con seguimiento en 40 cuentas, en siete llamadas

_2026-09-19 · Recorrido práctico por la API REST de Shager en curl y TypeScript: autenticación, espacios, canales, archivos, borradores por cuenta, programación y clics._

La API REST de Shager está en `https://app.shager.ai/api/v1`, se autentica con `Authorization: Bearer shgr_live_...`, devuelve cada resultado como `{ "data": ... }` y cada error como `{ "error": { "code", "message" } }`. Una tanda de distribución completa son siete llamadas: verificar la clave, elegir un espacio de trabajo, listar sus canales, subir los archivos, crear un borrador con textos por cuenta, programarlo y, por último, leer el estado y los clics. La especificación legible por máquinas es el [documento OpenAPI 3.1](https://app.shager.ai/api/v1/openapi.json).

Todo lo que sigue funciona tal cual en cuanto defines dos variables de entorno. Las mismas operaciones existen como herramientas MCP con los mismos nombres, así que lo que aprendas aquí te sirve también en Claude Code, Codex o Cursor.

## ¿Qué necesitas antes de la primera llamada?

Una clave. O bien una persona la crea en https://app.shager.ai/dashboard/agents con los permisos que necesitas, o bien tu agente crea un entorno de prueba con el [registro autónomo](/en/blog/agent-self-signup-and-claim). Los permisos funcionan así:

| Permiso | Permite |
|---|---|
| `read` | `whoami`, listar espacios de trabajo, canales, grupos y publicaciones, leer una publicación, analítica |
| `write` | Crear espacios de trabajo, grupos, archivos, borradores, enlaces de conexión y solicitudes de aprobación, y cancelar |
| `publish` | Programar, incluido `create_post` con `schedule_at` |

```bash
export SHAGER_API_KEY="shgr_live_your_key_here"
export API="https://app.shager.ai/api/v1"
```

## Llamada 1: ¿quién soy?

```bash
curl -s "$API/me" -H "Authorization: Bearer $SHAGER_API_KEY"
```

```json
{ "data": { "organization_id": "...", "organization": "Northwind Agency", "sandbox": false, "scopes": ["read", "write"] } }
```

Si `sandbox` es true, la programación y los enlaces de conexión quedan bloqueados hasta que una persona reclame la clave. Si falta `publish` en `scopes`, puedes armar borradores pero no programarlos. Saberlo desde el principio te ahorra un 403 desconcertante más adelante.

## Llamada 2: elegir o crear un espacio de trabajo

Un espacio de trabajo suele ser un cliente o una marca.

```bash
curl -s "$API/workspaces" -H "Authorization: Bearer $SHAGER_API_KEY"

curl -s -X POST "$API/workspaces" \
  -H "Authorization: Bearer $SHAGER_API_KEY" -H "Content-Type: application/json" \
  -d '{"name": "Summit Physio", "timezone": "Europe/London", "approval_required": true}'
```

Define siempre `timezone` (un nombre IANA) al crearlo. Las horas programadas se guardan con su offset, y la zona del espacio de trabajo es la que usa Shager para mostrárselas a las personas. `approval_required: true` significa que el cliente tiene que aprobar las publicaciones antes de que se puedan programar, un buen valor por defecto para clientes nuevos.

Exporta el id de la respuesta para que los comandos siguientes funcionen:

```bash
export WS="the_workspace_id"
```

## Llamada 3: listar canales y grupos

```bash
curl -s "$API/workspaces/$WS/channels" -H "Authorization: Bearer $SHAGER_API_KEY"
curl -s "$API/workspaces/$WS/groups" -H "Authorization: Bearer $SHAGER_API_KEY"
```

Cada canal viene con `id`, `network`, `name`, `handle`, `status`, los `formats` que admite y `connected_by_client`. Los grupos devuelven `id`, `name` y `channel_ids`. Con los grupos te diriges a cuarenta cuentas sin listar cuarenta ids: pasa `group_ids` a `create_post` y se incluyen todos los canales activos del grupo.

Si el espacio de trabajo todavía no tiene canales, crea un enlace de conexión con `POST /workspaces/{workspace_id}/connect-links` y envíaselo al dueño de la cuenta. Él mismo conecta Facebook, Instagram, YouTube, X, LinkedIn o TikTok, sin iniciar sesión en Shager.

## Llamada 4: subir archivos desde una URL

```bash
curl -s -X POST "$API/workspaces/$WS/media" \
  -H "Authorization: Bearer $SHAGER_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "url": "https://cdn.summitphysio.example/video/knee-mobility.mp4",
    "file_name": "knee-mobility.mp4",
    "width": 1080, "height": 1920, "duration_ms": 42000
  }'
```

Shager descarga el archivo desde el servidor y devuelve `id`, `kind`, `mime_type` y `size_bytes`. Para Reels, Shorts e historias, pasa `width`, `height` y `duration_ms` si los conoces. Así Shager puede verificar las reglas de formato de cada red antes de programar la publicación, en lugar de fallar en el momento del envío.

## Llamada 5: crear un borrador con textos por cuenta

Esta es la llamada que importa. Aquí está en TypeScript con `fetch` a secas, incluida una pequeña función auxiliar que vas a reutilizar:

```typescript
const API = "https://app.shager.ai/api/v1";

async function shager<T>(method: string, path: string, body?: unknown): Promise<T> {
  const res = await fetch(`${API}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.SHAGER_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json();
  if (!res.ok) {
    const err = new Error(`${res.status} ${json.error.code}: ${json.error.message}`);
    (err as any).status = res.status;
    throw err;
  }
  return json.data as T;
}

const WS = process.env.SHAGER_WORKSPACE_ID!;
const mediaId = process.env.SHAGER_MEDIA_ID!; // the id returned by call 4
const channels = await shager<Array<{ id: string; name: string; network: string; status: string }>>(
  "GET", `/workspaces/${WS}/channels`,
);

const post = await shager<{ id: string; status: string }>("POST", `/workspaces/${WS}/posts`, {
  label: "Knee mobility, week 42",
  caption: "Three minutes a day for stiffer knees. Full routine in the video.",
  channel_ids: channels.map((c) => c.id),
  media_ids: [mediaId],
  format: "REEL",
  youtube_title: "3-minute knee mobility routine",
  youtube_privacy: "public",
  per_channel: channels.map((c) => ({
    channel_id: c.id,
    caption: `${c.name}: three minutes a day for stiffer knees. Our physios show the routine.`,
  })),
  cta_url: "https://summitphysio.example/book",
  cta_text: "Book an assessment",
  campaign: "knee-mobility-oct",
  first_comment: "Book an assessment: https://summitphysio.example/book",
  stagger_seconds: [45, 120],
});

console.log(post.id, post.status); // DRAFT
```

Lo que te da cada campo:

1. `per_channel` le da a cada cuenta su propio texto. En la práctica los escribe un agente; la plantilla de arriba ocupa el lugar de ese paso, y el razonamiento está en [cómo evitar bloqueos por spam a escala](/en/blog/avoid-spam-blocks-when-posting-at-scale).
2. `format` se aplica donde la red lo admite, y en los demás casos `SHORT` o `REEL` se resuelve red por red. `youtube_title` y `youtube_privacy` solo afectan a los canales de YouTube.
3. `cta_url` genera un enlace corto y un juego de UTM distintos para cada cuenta, con `utm_content` identificando la cuenta. Sobre eso se construye la [atribución de leads por cuenta](/en/blog/utm-per-account-lead-attribution).
4. `stagger_seconds` define una pausa aleatoria entre cuentas de la misma red. El valor por defecto es `[30, 90]`.

Sin `schedule_at`, el resultado es un borrador. Guarda su id para las llamadas siguientes con `export POST_ID="the_post_id"`.

## Llamada 6: aprobación o programación

Si el espacio de trabajo exige aprobación, envíasela al cliente. La respuesta incluye una `approval_url` que el revisor abre sin iniciar sesión:

```bash
curl -s -X POST "$API/workspaces/$WS/posts/$POST_ID/approval" \
  -H "Authorization: Bearer $SHAGER_API_KEY" -H "Content-Type: application/json" \
  -d '{"reviewer_name": "Dana Ellis", "reviewer_email": "dana@summitphysio.example", "send_email": true}'
```

Una vez aprobada (o si no hace falta aprobación), prográmala. Esto requiere el permiso `publish`, y `schedule_at` tiene que estar en ISO 8601 con offset:

```bash
curl -s -X POST "$API/workspaces/$WS/posts/$POST_ID/schedule" \
  -H "Authorization: Bearer $SHAGER_API_KEY" -H "Content-Type: application/json" \
  -d '{"schedule_at": "2026-10-20T08:30:00+01:00"}'
```

En este punto, Shager valida cada destino según las reglas de su red. Si alguna cuenta rechazaría la publicación, recibes un 422 `rejected` con el motivo, y no se encola nada.

## Llamada 7: revisar el estado y los clics

```bash
curl -s "$API/workspaces/$WS/posts/$POST_ID" -H "Authorization: Bearer $SHAGER_API_KEY"
curl -s "$API/workspaces/$WS/posts?status=SCHEDULED&limit=50" -H "Authorization: Bearer $SHAGER_API_KEY"
curl -s "$API/workspaces/$WS/analytics?from=2026-10-01T00:00:00Z&to=2026-10-31T23:59:59Z" \
  -H "Authorization: Bearer $SHAGER_API_KEY"
```

`get_post` devuelve una fila por destino con `state`, `dispatch_at`, `published_at`, la `url` publicada y cualquier `error`. La analítica devuelve `total_clicks`, `by_account` y `by_post`, y cuenta los clics en los enlaces con seguimiento de Shager sin el tráfico de bots. Para cancelar una publicación programada que todavía no empezó a publicarse: `POST /workspaces/{workspace_id}/posts/{post_id}/cancel`.

## ¿Qué errores debería manejar tu código?

| Estado y código | Significado | Qué hacer |
|---|---|---|
| 401 `unauthorized` | Falta el encabezado o está mal formado | Envía `Authorization: Bearer shgr_...` |
| 403 `forbidden` | A la clave le falta el permiso necesario | Crea un borrador, o pídele `publish` a una persona |
| 403 `sandbox_unclaimed` | Clave de registro autónomo sin reclamar | Muéstrale el enlace de reclamo a una persona |
| 404 `not_found` | Id de espacio de trabajo, publicación o canal incorrecto | Vuelve a listar; los ids son por espacio de trabajo |
| 409 `not_scheduled` | Cancelar una publicación que no está programada | Lee primero el estado de la publicación |
| 422 `rejected` | Falló una regla de la red | Muestra el mensaje; indica el problema |
| 429 `rate_limited` | Más de 120 solicitudes por minuto por clave | Espera y vuelve a intentar |

El límite de solicitudes rara vez molesta a un cliente bien diseñado. Un solo `create_post` cubre cientos de cuentas, así que no deberías necesitar una llamada por cuenta.

## Preguntas frecuentes

### ¿Hay un SDK?

No uno propio. El documento OpenAPI 3.1 funciona con los generadores habituales si quieres clientes tipados, y la API es lo bastante pequeña como para que una función auxiliar como la de arriba suela alcanzar. Pronto habrá una CLI de Shager.

### ¿REST y MCP son de verdad lo mismo?

Sí. Cada herramienta MCP corresponde a una operación REST con el mismo nombre, las mismas entradas, permisos, límite de solicitudes y registro de auditoría. Usa REST desde scripts y servicios, y MCP desde clientes de agentes. La lista completa de herramientas está en la [página para agentes](/en/agents).

### ¿Cómo publico un texto distinto en Facebook y en YouTube dentro de la misma publicación?

Usa `per_channel` con el id y el texto de cada canal, y `youtube_title` para el título de YouTube. Un solo objeto de publicación, muchos destinos, cada uno con su propio texto, formato y primer comentario si lo necesitas.

## Siguiente paso

Cuando esta secuencia funcione bien a mano, conviértela en algo que un agente pueda repetir. [Cómo crear una skill de Claude para Shager](/en/blog/build-a-claude-skill-for-shager) muestra cómo empaquetar estas llamadas y las reglas que las rodean en una skill reutilizable.

---
Source: https://shager.ai/es/blog/shager-rest-api-quickstart
