> ## Documentation Index
> Fetch the complete documentation index at: https://sorank.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Usar la API pública de SORANK

> Autenticación, cuotas, contenido y seguimiento de operaciones.

## Dirección de producción y primera llamada

Inicia sesión en la aplicación en `https://app.sorank.com`. Administra las claves en `https://app.sorank.com/settings/api` y envía las solicitudes a `https://app.sorank.com/api/public/v1`. No tienes que crear un servidor API.

1. Abre **Configuración → Acceso API** y crea una clave con nombre, al menos un sitio, **Ver sitios** (`profiles:read`) y fecha de vencimiento. Añade **Ver cuotas** (`quotas:read`) para la segunda llamada.
2. Copia ahora la clave completa: solo se muestra una vez. El prefijo visible después no sirve para autenticarte.
3. Elige un ejemplo. Con cURL, introduce la clave cuando se solicite. Para JavaScript o Go, configura `SORANK_API_KEY` en el entorno del proceso servidor. La pestaña HTML incluye un servidor Node.js local: ejecútalo con la clave en su entorno y abre `http://127.0.0.1:3100`. Nunca incluyas la clave en HTML o JavaScript del navegador. Añade la autenticación de tu aplicación antes de usar este patrón fuera de la demo local.

<Tabs>
  <Tab title="cURL">
    ```sh theme={null}
    printf 'SORANK API key: '; read -rs SORANK_API_KEY; echo
    curl --fail-with-body --silent --show-error \
      -H "Authorization: Bearer $SORANK_API_KEY" \
      'https://app.sorank.com/api/public/v1/profiles'

    # Copy an authorized data[].id from the response above.
    PROFILE_ID='<profile_id>'
    curl --fail-with-body --silent --show-error \
      -H "Authorization: Bearer $SORANK_API_KEY" \
      "https://app.sorank.com/api/public/v1/profiles/$PROFILE_ID/quotas"
    unset SORANK_API_KEY
    ```
  </Tab>

  <Tab title="HTML">
    ```html index.html theme={null}
    <button id="load">Load my SORANK websites</button>
    <pre id="result"></pre>
    <script>
      document.querySelector('#load').addEventListener('click', async () => {
        const output = document.querySelector('#result');
        try {
          // Your own backend authenticates to SORANK. Keep the key out of this page.
          const response = await fetch('/api/sorank/profiles');
          if (!response.ok) throw new Error(`HTTP ${response.status}`);
          const profiles = await response.json();
          output.textContent = JSON.stringify(profiles, null, 2);
        } catch (error) {
          output.textContent = String(error);
        }
      });
    </script>
    ```

    ```js server.mjs theme={null}
    // Local demo backend. Run with SORANK_API_KEY in the environment.
    import { createServer } from 'node:http';
    import { readFile } from 'node:fs/promises';

    const key = process.env.SORANK_API_KEY;
    if (!key) throw new Error('Set SORANK_API_KEY before running this file');
    const html = await readFile(new URL('./index.html', import.meta.url));

    createServer(async (request, response) => {
      if (request.method === 'GET' && request.url === '/') {
        response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
        response.end(html);
        return;
      }
      if (request.method === 'GET' && request.url === '/api/sorank/profiles') {
        try {
          const upstream = await fetch('https://app.sorank.com/api/public/v1/profiles', {
            headers: { Authorization: `Bearer ${key}` },
          });
          response.writeHead(upstream.status, {
            'Content-Type': 'application/json; charset=utf-8',
            'Cache-Control': 'no-store',
          });
          response.end(await upstream.text());
        } catch {
          response.writeHead(502).end('Upstream unavailable');
        }
        return;
      }
      response.writeHead(404).end('Not found');
    }).listen(3100, '127.0.0.1');
    // Open http://127.0.0.1:3100 after starting this server.
    ```
  </Tab>

  <Tab title="JavaScript">
    ```js profiles.mjs theme={null}
    // Run on Node.js 20+ with SORANK_API_KEY set in the process environment.
    const token = process.env.SORANK_API_KEY;
    if (!token) throw new Error('Set SORANK_API_KEY before running this file');
    const base = 'https://app.sorank.com/api/public/v1';

    async function get(path) {
      const response = await fetch(`${base}${path}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      const body = await response.json();
      if (!response.ok) throw new Error(`HTTP ${response.status}: ${JSON.stringify(body)}`);
      return body;
    }

    const profiles = await get('/profiles');
    console.log(profiles);
    const profileId = profiles.data[0]?.id;
    if (profileId) console.log(await get(`/profiles/${encodeURIComponent(profileId)}/quotas`));
    ```
  </Tab>

  <Tab title="Go">
    ```go main.go theme={null}
    package main

    import (
      "encoding/json"
      "fmt"
      "io"
      "net/http"
      "os"
      "time"
    )

    func main() {
      key := os.Getenv("SORANK_API_KEY")
      if key == "" { panic("set SORANK_API_KEY before running this file") }
      client := &http.Client{Timeout: 15 * time.Second}
      get := func(path string) []byte {
        req, err := http.NewRequest(http.MethodGet, "https://app.sorank.com/api/public/v1"+path, nil)
        if err != nil { panic(err) }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := client.Do(req)
        if err != nil { panic(err) }
        defer resp.Body.Close()
        body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
        if err != nil { panic(err) }
        if resp.StatusCode != http.StatusOK { panic(fmt.Sprintf("HTTP %d: %s", resp.StatusCode, body)) }
        return body
      }

      body := get("/profiles")
      fmt.Println(string(body))
      var profiles struct { Data []struct { ID string `json:"id"` } `json:"data"` }
      if err := json.Unmarshal(body, &profiles); err != nil { panic(err) }
      if len(profiles.Data) > 0 {
        fmt.Println(string(get("/profiles/"+profiles.Data[0].ID+"/quotas")))
      }
    }
    ```
  </Tab>
</Tabs>

La respuesta enumera los sitios autorizados en `data`. Usa un valor `data[].id` como `profile_id` en las siguientes llamadas. Si la lista está vacía, comprueba los sitios elegidos y tus permisos. Los ejemplos de cURL, JavaScript y Go también leen la cuota del primer perfil con `quotas:read`.

La **Referencia de la API pública** en la navegación para desarrolladores detalla rutas, scopes y respuestas.

## Autenticarse

Crea una clave personal en **Configuración → Acceso API** para tus scripts o autoriza un cliente OAuth con PKCE S256. Limita los perfiles y scopes a lo necesario. Envía el token como Bearer. Las cookies del navegador y los tokens de Supabase no sirven aquí.

Para una aplicación que actúe en nombre del usuario, empieza por `https://app.sorank.com/.well-known/oauth-protected-resource/api/public/v1` y `https://app.sorank.com/.well-known/oauth-authorization-server`. Descubre los endpoints OAuth anunciados, registra un cliente público si hace falta y usa authorization code con PKCE S256, los scopes necesarios y el recurso exacto `https://app.sorank.com/api/public/v1`. Un token OAuth de MCP no sirve para REST.

## Permisos y cuota

Consulta `getQuotas` antes de generar contenido. El ejemplo requiere `profiles:read`, `quotas:read`, `calendar:read`, `calendar:write`, `articles:read`, `articles:generate` y `articles:publish`. Crear un calendario también requiere permisos de generación y publicación. La aplicación, REST y MCP comparten la cuota; más claves no la amplían.

Los permisos de lectura **Ver productos** (`products:read`), **Ver analíticas** (`analytics:read`), **Ver visibilidad IA** (`geo:read`) y **Ver ajustes** (`settings:read`) nunca consumen cuota. **Editar ajustes** (`settings:write`) cambia los mismos ajustes que la aplicación; concédalo solo si una integración debe modificarlos.

## Comprobar la elegibilidad

La API está disponible cuando ha completado el onboarding en Sorank y el sitio tiene una suscripción válida. Un colaborador de agencia usa la suscripción del propietario del sitio. Sorank lo comprueba al crear una clave o aprobar una conexión OAuth, y de nuevo en cada solicitud.

* `listProfiles` solo devuelve los sitios elegibles.
* Un onboarding sin terminar devuelve `403 onboarding_required`.
* Un sitio sin suscripción válida devuelve `402 subscription_required`, con `details.reason` (`paused`, `past_due`, `incomplete` o `inactive`) cuando se conoce.

Termine el onboarding o actualice la suscripción en **Configuración → Facturación**. Su clave actual vuelve a funcionar en la siguiente solicitud.

## Leer productos, analíticas, visibilidad IA y ajustes

Estas lecturas devuelven lo que la aplicación Sorank le muestra. Nunca inician un análisis, una sincronización ni una generación.

* **Productos** (`products:read`): `listProducts` recorre el catálogo de productos con un cursor. Tras `409 product_catalog_cursor_stale`, vuelva a empezar desde la primera página.
* **Analíticas** (`analytics:read`): rendimiento, consultas, páginas, países y dispositivos de Search Console, y tráfico de Google Analytics procedente de asistentes de IA. `start` y `end` (`YYYY-MM-DD`) son obligatorios. Cada llamada consulta Google en directo y tiene un límite de frecuencia más bajo. Conecte primero Google en Sorank; si no, recibirá `409 gsc_not_connected` o `409 ga_not_connected`, y `403 google_access_lost` si se perdió el acceso.
* **Visibilidad IA** (`geo:read`): estadísticas GEO, evolución de las menciones, cronología de citas, fuentes que citan el sitio y competidores citados en un análisis.
* **Ajustes** (`settings:read`): `getSettings` devuelve el sitio, el brief de negocio con su `revision`, los competidores, los valores predeterminados de artículos, los visuales y el canal de YouTube.

## Cambiar los ajustes

Con `settings:write` puede cambiar el nombre de la empresa, el color de marca y el mercado, sustituir el brief de negocio y la lista de competidores, cambiar los valores predeterminados de los artículos del calendario, el modo de imagen y las portadas, y conectar, pausar o desconectar el canal de YouTube. La URL del sitio no se puede cambiar mediante la API (`400 field_not_writable`).

Cada cambio requiere un `Idempotency-Key`. La respuesta contiene los ajustes resultantes, un recibo y una lista `effects` con los procesos iniciados en segundo plano, como en la aplicación: `geo_questions_regeneration_queued`, `backlink_refresh_queued`, `calendar_slots_shifted`, `image_gallery_analysis_queued`, `youtube_sync_queued` o `youtube_purge_queued`.

Para sustituir el brief de negocio, envíe la `expected_revision` leída en `getSettings`. Si el brief cambió entretanto, recibirá `409 business_brief_revision_conflict`: vuelva a leer los ajustes antes de reintentar.

## Probar un flujo REST completo

Este ejemplo avanzado crea contenido y puede consumir cuota. Sustituye los identificadores, fechas futuras, claves de idempotencia y credencial de ejemplo antes de ejecutarlo. Las fechas del calendario usan la zona horaria IANA indicada. Consulta `getCapabilities` antes de elegir destino o categoría del CMS.

```sh theme={null}
# Sustituye los UUID de ejemplo por los de tu cuenta.
API_ORIGIN="https://app.sorank.com"
PROFILE_ID="00000000-0000-4000-8000-000000000000"
ARTICLE_ID="11111111-1111-4111-8111-111111111111"
TOKEN="<PERSONAL_API_KEY>"

curl -H "Authorization: Bearer $TOKEN" "$API_ORIGIN/api/public/v1/profiles/$PROFILE_ID/quotas"
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Idempotency-Key: 22222222-2222-4222-8222-222222222222" \
  -d '{"settings":{"timezone":"Europe/Paris","local_time":"09:30"},"start_date":"2026-11-01","end_date":"2026-12-31","subjects":[{"subject":"Sustainable travel checklist"}]}' \
  "$API_ORIGIN/api/public/v1/profiles/$PROFILE_ID/calendar"
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Idempotency-Key: 33333333-3333-4333-8333-333333333333" \
  -d '{"seeds":[{"keyword":"sustainable travel tips","title":"Practical sustainable travel"}]}' \
  "$API_ORIGIN/api/public/v1/profiles/$PROFILE_ID/article-generations"
curl -H "Authorization: Bearer $TOKEN" "$API_ORIGIN/api/public/v1/operations/<OPERATION_UUID>"
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Idempotency-Key: 44444444-4444-4444-8444-444444444444" \
  -d '{"target":"draft"}' "$API_ORIGIN/api/public/v1/profiles/$PROFILE_ID/articles/$ARTICLE_ID/publications"
```

La referencia de la API pública detalla operaciones, campos, respuestas y errores.

## Seguir y reintentar

Cada cambio requiere un `Idempotency-Key` único. Use la misma clave al repetir el mismo comando. Lea `data.receipt.operation_id` y consulte `getOperation` hasta `succeeded`, `failed` o `verification_pending`. Un recibo aceptado no significa que la publicación haya terminado. Un lote de generación de artículos devuelve `202` con un recibo ya `succeeded`; siga cada artículo de `selection.accepted_ids` con `getArticle`. La autorización se comprueba en cada consulta, así que una revocación surte efecto de inmediato.

## Gestionar errores

Todos los errores tienen la misma forma en REST y en MCP: `{"error": {"code", "message", "request_id", "retryable", "details"}}`. Base su lógica en `code`, no en el mensaje. Reintente solo si `retryable` es `true`, después de `Retry-After` cuando esté presente.

* `401 invalid_credential`: credencial no válida, caducada o revocada.
* `403 missing_scope` o `403 onboarding_required`; `402 subscription_required` o `402 insufficient_quota`.
* `404 not_found`: el recurso no existe o no es accesible con esta credencial.
* `409`: un conflicto como `idempotency_conflict`; `429 rate_limited`: espere `Retry-After`.
* `503 auth_state_unavailable`: el acceso se deniega hasta que se recupere el estado de autorización; reintente más tarde.

La referencia de la API pública enumera los códigos de cada operación. Comparta `code` y `request_id` con soporte, nunca su token.

Para conectar un asistente, lee la guía MCP.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.