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

# Integracion Activities

> Envia actividades numeradas (001-999) desde cualquier CRM o sistema externo a SalesOS usando la integracion Activities y API Keys.

# Integracion Activities

<Tip>
  Prueba peticiones firmadas en el navegador en el [Sandbox de la API](https://play2sellsa.github.io/api-sandbox/) — pega tu API key y secret, y el playground firma las peticiones automáticamente.
</Tip>

<Note>
  Los ejemplos siguientes muestran el header en formato wire `Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE`. Para calcular la firma en tu código, usa el helper `signedRequest` en [Autenticación](/es/api/authentication).
</Note>

La integracion Activities te permite conectar **cualquier CRM o sistema externo** a SalesOS, incluso si no existe una integracion dedicada. Envias actividades numeradas (001-999) via API y las mapeas a misiones de SalesOS en el Dashboard.

## Como Funciona

1. **Obtienes una API Key** desde el Dashboard de SalesOS (Admin > Integraciones > API Keys)
2. **Sincronizas tu equipo** — registras colaboradores (vendedores) para que SalesOS los reconozca
3. **Envias actividades** numeradas 001-999 mediante un unico endpoint REST
4. **Tu administrador mapea** cada numero de actividad a una mision de SalesOS en el Dashboard
5. **SalesOS procesa** las actividades como completaciones de misiones, otorgando puntos y rastreando el progreso

<Note>
  Los codigos de actividad son simplemente numeros (001 a 999). El significado de cada codigo lo define tu equipo al mapearlos a misiones en el Dashboard. Por ejemplo, "001" podria significar "Llamada telefonica" y "002" podria significar "Reunion agendada".
</Note>

***

## Inicio Rapido

<Steps>
  <Step title="Obtener tu API Key">
    Ve a **Admin > Integraciones > API Keys** en el Dashboard de SalesOS. Crea una nueva clave con el scope `default:sync`. Copia la clave — solo se mostrara una vez.

    Tu clave luce asi: `sk_live_a1b2c3d4e5f6g7h8i9j0...`
  </Step>

  <Step title="Registrar tu equipo de ventas">
    Antes de enviar actividades, indica a SalesOS quienes son tus vendedores:

    ```bash theme={null}
    curl -X POST https://api.play2sell.com/functions/v1/default-integration \
      -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
      -H "Content-Type: application/json" \
      -d '{
        "action": "sync_collaborators",
        "collaborators": [
          {
            "external_id": "emp-101",
            "name": "Maria Santos",
            "email": "maria@yourcompany.com",
            "team": "Sales Team A",
            "role": "sales_rep"
          },
          {
            "external_id": "emp-102",
            "name": "Joao Silva",
            "email": "joao@yourcompany.com",
            "team": "Sales Team A",
            "role": "sales_rep"
          },
          {
            "external_id": "emp-103",
            "name": "Ana Oliveira",
            "email": "ana@yourcompany.com",
            "team": "Sales Team B",
            "role": "team_lead"
          }
        ]
      }'
    ```

    **Respuesta:**

    ```json theme={null}
    {
      "data": { "created": 3, "existing": 0, "errors": [], "total": 3 },
      "meta": { "request_id": "a1b2c3d4-...", "timestamp": "2026-03-17T10:00:00.000Z" }
    }
    ```
  </Step>

  <Step title="Enviar actividades desde tu CRM">
    Ahora envia las actividades que tus vendedores realizaron hoy:

    ```bash theme={null}
    curl -X POST https://api.play2sell.com/functions/v1/default-integration \
      -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
      -H "Content-Type: application/json" \
      -d '{
        "action": "sync_activities",
        "activities": [
          {
            "activity_code": "001",
            "external_id": "crm-evt-10001",
            "collaborator_email": "maria@yourcompany.com",
            "data": { "client": "Acme Corp", "duration_min": 12 },
            "occurred_at": "2026-03-17T09:15:00Z"
          },
          {
            "activity_code": "002",
            "external_id": "crm-evt-10002",
            "collaborator_email": "joao@yourcompany.com",
            "data": { "client": "Beta Inc", "type": "video_call" },
            "occurred_at": "2026-03-17T10:30:00Z"
          },
          {
            "activity_code": "001",
            "external_id": "crm-evt-10003",
            "collaborator_email": "maria@yourcompany.com",
            "data": { "client": "Gamma Ltd" },
            "occurred_at": "2026-03-17T11:00:00Z"
          }
        ]
      }'
    ```

    **Respuesta:**

    ```json theme={null}
    {
      "data": { "processed": 3, "skipped": 0, "duplicates": 0, "errors": [], "total": 3 },
      "meta": { "request_id": "d4e5f6g7-...", "timestamp": "2026-03-17T11:05:00.000Z" }
    }
    ```

    Maria obtuvo 2 actividades (dos llamadas telefonicas) y Joao obtuvo 1 (una reunion).
  </Step>

  <Step title="Mapear codigos a misiones en el Dashboard">
    En el Dashboard, ve a **Misiones > Configurar**. Selecciona **"Activities"** en el dropdown de CRM. Veras las actividades 001 a 999. Mapea las que utilices:

    | Actividad     | Mapear a Mision     |
    | ------------- | ------------------- |
    | Atividade 001 | Ligacoes Realizadas |
    | Atividade 002 | Reunioes Agendadas  |
    | Atividade 003 | Propostas Enviadas  |

    Haz clic en **Guardar**. A partir de ahora, cada actividad "001" enviada via API contara para la mision "Ligacoes Realizadas".
  </Step>
</Steps>

***

## Autenticacion

Todas las solicitudes requieren una API Key en el header `Authorization`:

```
Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE
```

Consulta la [pagina de Autenticacion](/es/api/authentication) para detalles sobre como crear y administrar API Keys.

| Propiedad                 | Detalles                                                    |
| ------------------------- | ----------------------------------------------------------- |
| **Header**                | `Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE`    |
| **Scope requerido**       | `default:sync`                                              |
| **Limite de solicitudes** | Configurable por clave (por defecto: 1000 solicitudes/hora) |
| **Formato de clave**      | `sk_live_` (produccion) o `sk_test_` (pruebas)              |

***

## Entornos

<Tabs>
  <Tab title="Production">
    **URL Base:** `https://api.play2sell.com`

    **Dashboard:** `https://dashboard.play2sell.com`

    **App:** `https://app.play2sell.com`
  </Tab>

  <Tab title="Staging">
    **URL Base:** `https://api-staging.play2sell.com`

    **Dashboard:** `https://dashboard-staging.play2sell.com`

    **App:** `https://app-staging.play2sell.com`
  </Tab>
</Tabs>

***

## Referencia del Endpoint

**URL Base:**

```
POST https://api.play2sell.com/functions/v1/default-integration
```

El endpoint acepta dos acciones mediante el campo `action`: `sync_collaborators` y `sync_activities`.

***

### Accion: sync\_collaborators

Registra o actualiza colaboradores (vendedores) en SalesOS. Los colaboradores deben existir antes de que puedas atribuirles actividades.

#### Esquema de Solicitud

<ParamField body="action" type="string" required>
  Debe ser `"sync_collaborators"`
</ParamField>

<ParamField body="collaborators" type="array" required>
  Array de objetos de colaborador (max 500)
</ParamField>

<ParamField body="collaborators[].external_id" type="string" required>
  ID unico en tu sistema (max 255 caracteres)
</ParamField>

<ParamField body="collaborators[].name" type="string" required>
  Nombre completo (max 255 caracteres)
</ParamField>

<ParamField body="collaborators[].email" type="string" required>
  Email valido — se usa para vincular actividades posteriormente
</ParamField>

<ParamField body="collaborators[].phone" type="string">
  Numero de telefono (cualquier formato)
</ParamField>

<ParamField body="collaborators[].document" type="string">
  Documento de identidad como CPF (max 20 caracteres)
</ParamField>

<ParamField body="collaborators[].team" type="string">
  Nombre del equipo (max 100 caracteres)
</ParamField>

<ParamField body="collaborators[].role" type="string">
  Rol en tu organizacion (max 50 caracteres)
</ParamField>

<ParamField body="collaborators[].metadata" type="object">
  Cualquier dato adicional clave-valor
</ParamField>

#### Ejemplo: Sincronizar un equipo completo

```bash theme={null}
curl -X POST https://api.play2sell.com/functions/v1/default-integration \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "sync_collaborators",
    "collaborators": [
      {
        "external_id": "emp-101",
        "name": "Maria Santos",
        "email": "maria@yourcompany.com",
        "phone": "+5511999001001",
        "document": "12345678901",
        "team": "Equipe Vendas SP",
        "role": "vendedor",
        "metadata": { "crm_id": "CRM-2001", "hire_date": "2024-06-15" }
      },
      {
        "external_id": "emp-102",
        "name": "Joao Silva",
        "email": "joao@yourcompany.com",
        "phone": "+5511999002002",
        "team": "Equipe Vendas SP",
        "role": "vendedor"
      },
      {
        "external_id": "emp-103",
        "name": "Ana Oliveira",
        "email": "ana@yourcompany.com",
        "team": "Equipe Vendas RJ",
        "role": "gerente",
        "metadata": { "crm_id": "CRM-2003", "is_manager": true }
      }
    ]
  }'
```

#### Respuesta (200 — Exitosa)

```json theme={null}
{
  "data": {
    "created": 2,
    "existing": 1,
    "errors": [],
    "total": 3
  },
  "meta": {
    "request_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "timestamp": "2026-03-17T14:30:00.000Z"
  }
}
```

* `created: 2` — Maria y Joao eran nuevos, por lo que fueron creados
* `existing: 1` — Ana ya existia (coincidio por email), por lo que fue actualizada
* `errors: []` — sin fallos en este lote

#### Ejemplo: Fallos parciales en un lote

Si algunos colaboradores tienen datos invalidos, fallan individualmente sin bloquear al resto:

```json theme={null}
{
  "data": {
    "created": 8,
    "existing": 1,
    "errors": [
      "Collaborator emp-110 (invalid-email): Invalid email format"
    ],
    "total": 10
  },
  "meta": {
    "request_id": "b2c3d4e5-...",
    "timestamp": "2026-03-17T14:30:00.000Z"
  }
}
```

<Tip>
  **Resincronizar es seguro.** Puedes enviar los mismos colaboradores multiples veces. Los existentes se actualizan (no se duplican) en base a su email. Esto facilita ejecutar una sincronizacion completa nocturna desde tu CRM.
</Tip>

***

### Accion: sync\_activities

Envia eventos de actividad atribuidos a colaboradores. Cada actividad usa un codigo de 3 digitos (001-999) que mapeas a misiones en el Dashboard.

#### Esquema de Solicitud

<ParamField body="action" type="string" required>
  Debe ser `"sync_activities"`
</ParamField>

<ParamField body="activities" type="array" required>
  Array de objetos de actividad (max 1000)
</ParamField>

<ParamField body="activities[].activity_code" type="string" required>
  Codigo de 3 digitos: `"001"` a `"999"`
</ParamField>

<ParamField body="activities[].external_id" type="string" required>
  ID unico para deduplicacion (max 255 caracteres)
</ParamField>

<ParamField body="activities[].collaborator_email" type="string" required>
  Email del vendedor que realizo la actividad
</ParamField>

<ParamField body="activities[].data" type="object">
  Cualquier contexto adicional (formato libre)
</ParamField>

<ParamField body="activities[].occurred_at" type="string">
  Cuando ocurrio (ISO 8601). Por defecto es ahora.
</ParamField>

#### Ejemplo: Enviar las actividades de un dia

```bash theme={null}
curl -X POST https://api.play2sell.com/functions/v1/default-integration \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "sync_activities",
    "activities": [
      {
        "activity_code": "001",
        "external_id": "crm-20260317-001",
        "collaborator_email": "maria@yourcompany.com",
        "data": {
          "client_name": "Acme Corp",
          "client_phone": "+5511988887777",
          "duration_minutes": 12,
          "outcome": "interested"
        },
        "occurred_at": "2026-03-17T09:15:00-03:00"
      },
      {
        "activity_code": "001",
        "external_id": "crm-20260317-002",
        "collaborator_email": "maria@yourcompany.com",
        "data": {
          "client_name": "Beta Inc",
          "duration_minutes": 8,
          "outcome": "no_answer"
        },
        "occurred_at": "2026-03-17T09:30:00-03:00"
      },
      {
        "activity_code": "002",
        "external_id": "crm-20260317-003",
        "collaborator_email": "joao@yourcompany.com",
        "data": {
          "client_name": "Gamma Ltd",
          "meeting_type": "video_call",
          "duration_minutes": 45
        },
        "occurred_at": "2026-03-17T10:00:00-03:00"
      },
      {
        "activity_code": "003",
        "external_id": "crm-20260317-004",
        "collaborator_email": "joao@yourcompany.com",
        "data": {
          "client_name": "Gamma Ltd",
          "proposal_value": 25000.00,
          "currency": "BRL"
        },
        "occurred_at": "2026-03-17T14:00:00-03:00"
      },
      {
        "activity_code": "001",
        "external_id": "crm-20260317-005",
        "collaborator_email": "ana@yourcompany.com",
        "data": {
          "client_name": "Delta SA",
          "duration_minutes": 20,
          "outcome": "scheduled_meeting"
        },
        "occurred_at": "2026-03-17T15:30:00-03:00"
      }
    ]
  }'
```

#### Respuesta (200 — Exitosa)

```json theme={null}
{
  "data": {
    "processed": 5,
    "skipped": 0,
    "duplicates": 0,
    "errors": [],
    "total": 5
  },
  "meta": {
    "request_id": "c3d4e5f6-7890-abcd-ef01-234567890abc",
    "timestamp": "2026-03-17T16:00:00.000Z"
  }
}
```

#### Campos de respuesta explicados

<ResponseField name="processed" type="number">
  Actividades registradas exitosamente
</ResponseField>

<ResponseField name="skipped" type="number">
  Actividades donde el email del colaborador no fue encontrado en SalesOS
</ResponseField>

<ResponseField name="duplicates" type="number">
  Actividades con un `external_id` que ya fue enviado anteriormente
</ResponseField>

<ResponseField name="errors" type="array">
  Array de mensajes de error para los elementos que fallaron
</ResponseField>

<ResponseField name="total" type="number">
  Total de elementos recibidos en la solicitud
</ResponseField>

#### Ejemplo: Resultados mixtos (algunos omitidos, algunos duplicados)

Si reenvias el mismo lote o incluyes emails desconocidos:

```json theme={null}
{
  "data": {
    "processed": 2,
    "skipped": 1,
    "duplicates": 2,
    "errors": [],
    "total": 5
  },
  "meta": {
    "request_id": "d4e5f6g7-...",
    "timestamp": "2026-03-17T16:05:00.000Z"
  }
}
```

* `skipped: 1` — un email no estaba registrado via `sync_collaborators`
* `duplicates: 2` — dos actividades tenian valores de `external_id` que ya estaban en el sistema

***

## Codigos de Actividad (001-999)

Los codigos de actividad son identificadores abstractos. Su significado depende completamente de ti.

**Ejemplo de mapeo para una empresa inmobiliaria:**

| Codigo | Actividad del CRM               | Mision en SalesOS   | Puntos |
| ------ | ------------------------------- | ------------------- | ------ |
| 001    | Llamada telefonica a prospecto  | Ligacoes Realizadas | 5 pts  |
| 002    | Reunion (presencial o video)    | Reunioes Agendadas  | 15 pts |
| 003    | Propuesta enviada               | Propostas Enviadas  | 20 pts |
| 004    | Contrato firmado                | Contratos Fechados  | 50 pts |
| 005    | Visita a propiedad con cliente  | Visitas Realizadas  | 25 pts |
| 006    | Email de seguimiento enviado    | Follow-ups Enviados | 3 pts  |
| 007    | Calificacion de lead completada | Leads Qualificados  | 10 pts |

**Ejemplo de mapeo para una empresa SaaS:**

| Codigo | Actividad del CRM         | Mision en SalesOS      | Puntos  |
| ------ | ------------------------- | ---------------------- | ------- |
| 001    | Llamada de descubrimiento | Ligacoes de Descoberta | 10 pts  |
| 002    | Demo de producto agendada | Demos Agendadas        | 20 pts  |
| 003    | Prueba gratuita iniciada  | Trials Iniciados       | 15 pts  |
| 004    | Propuesta enviada         | Propostas Enviadas     | 25 pts  |
| 005    | Negocio cerrado           | Deals Fechados         | 100 pts |

<Tip>
  No necesitas usar los 999 codigos. La mayoria de los equipos usan entre 5 y 20 codigos. Comienza con pocos y agrega mas segun sea necesario.
</Tip>

***

## Idempotencia

El campo `external_id` garantiza la idempotencia. Si envias la misma actividad dos veces con el mismo `external_id`, la segunda solicitud la reporta como duplicada — **no** se procesa nuevamente.

**Primera llamada** — la actividad se procesa:

```bash theme={null}
curl -X POST https://api.play2sell.com/functions/v1/default-integration \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "sync_activities",
    "activities": [{
      "activity_code": "001",
      "external_id": "crm-call-5001",
      "collaborator_email": "maria@yourcompany.com"
    }]
  }'
```

```json theme={null}
{ "data": { "processed": 1, "skipped": 0, "duplicates": 0, "errors": [], "total": 1 } }
```

**Segunda llamada** (mismo `external_id`) — deduplicada de forma segura:

```json theme={null}
{ "data": { "processed": 0, "skipped": 0, "duplicates": 1, "errors": [], "total": 1 } }
```

<Tip>
  **Usa el ID de evento de tu CRM como `external_id`.** Esto asegura que incluso si tu trabajo de sincronizacion se ejecuta dos veces (por ejemplo, despues de una falla y reintento), las actividades nunca se cuenten doble.
</Tip>

***

## Manejo de Errores

### Formato de Respuesta de Error

Todos los errores siguen una estructura consistente:

```json theme={null}
{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description",
    "details": []
  }
}
```

### Referencia de Codigos de Error

<AccordionGroup>
  <Accordion title="400 — VALIDATION_ERROR">
    **Cuando ocurre:** Body invalido, campos faltantes, codigo de actividad incorrecto.

    **Que hacer:** Corrige el payload de la solicitud — revisa el array `details` para detalles especificos.
  </Accordion>

  <Accordion title="401 — UNAUTHORIZED">
    **Cuando ocurre:** API key faltante, invalida o expirada.

    **Que hacer:** Verifica tu API key. Genera una nueva si expiro.
  </Accordion>

  <Accordion title="403 — FORBIDDEN">
    **Cuando ocurre:** La API key no tiene el scope `default:sync`.

    **Que hacer:** Edita la clave en el Dashboard y agrega el scope requerido.
  </Accordion>

  <Accordion title="405 — METHOD_NOT_ALLOWED">
    **Cuando ocurre:** Se uso GET, PUT, etc. en lugar de POST.

    **Que hacer:** Cambia a `POST`.
  </Accordion>

  <Accordion title="429 — RATE_LIMITED">
    **Cuando ocurre:** Demasiadas solicitudes en esta hora.

    **Que hacer:** Espera `retry_after` segundos, luego reintenta.
  </Accordion>

  <Accordion title="500 — SERVER_ERROR">
    **Cuando ocurre:** Error interno.

    **Que hacer:** Reintenta con backoff exponencial. Contacta a soporte si persiste.
  </Accordion>
</AccordionGroup>

### Ejemplo: Error de validacion con detalles

```bash theme={null}
# Enviando un activity_code invalido y un email incorrecto
curl -X POST https://api.play2sell.com/functions/v1/default-integration \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "sync_activities",
    "activities": [
      { "activity_code": "abc", "external_id": "e1", "collaborator_email": "maria@co.com" },
      { "activity_code": "001", "external_id": "e2", "collaborator_email": "not-an-email" },
      { "activity_code": "001", "external_id": "e3", "collaborator_email": "joao@co.com" }
    ]
  }'
```

**Respuesta (400):**

```json theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid activities payload",
    "details": [
      { "index": 0, "field": "activity_code", "message": "Required 3-digit string (001-999), e.g. \"001\"" },
      { "index": 1, "field": "collaborator_email", "message": "Required valid email" }
    ]
  }
}
```

El campo `index` te indica cual elemento del array tiene el problema. El elemento en el indice 2 (joao) era valido — solo se listan los elementos invalidos.

### Ejemplo: Limite de solicitudes excedido

```json theme={null}
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded",
    "retry_after": 1847
  }
}
```

Espera 1847 segundos (\~31 minutos) antes de reintentar. El limite de solicitudes se reinicia cada hora.

### Ejemplo: API key invalida

```json theme={null}
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or expired API key"
  }
}
```

***

## Ejemplos de Codigo Completos

<CodeGroup>
  ```bash cURL theme={null}
  # Step 1: Set your API key
  # Define las credenciales (ver /es/api/authentication para el helper de firma)
  export SALESOS_API_KEY="sk_live_YOUR_API_KEY"
  export SALESOS_API_SECRET="YOUR_API_KEY_SECRET"
  export SALESOS_URL="https://api.play2sell.com/functions/v1/default-integration"

  # Step 2: Register your team (run once, or nightly to keep in sync)
  curl -s -X POST "$SALESOS_URL" \
    -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
    -H "Content-Type: application/json" \
    -d '{
      "action": "sync_collaborators",
      "collaborators": [
        { "external_id": "emp-101", "name": "Maria Santos", "email": "maria@co.com", "team": "SP" },
        { "external_id": "emp-102", "name": "Joao Silva", "email": "joao@co.com", "team": "SP" },
        { "external_id": "emp-103", "name": "Ana Oliveira", "email": "ana@co.com", "team": "RJ" }
      ]
    }' | jq .

  # Step 3: Send today's activities
  curl -s -X POST "$SALESOS_URL" \
    -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
    -H "Content-Type: application/json" \
    -d '{
      "action": "sync_activities",
      "activities": [
        { "activity_code": "001", "external_id": "call-1001", "collaborator_email": "maria@co.com", "data": {"client": "Acme"} },
        { "activity_code": "001", "external_id": "call-1002", "collaborator_email": "maria@co.com", "data": {"client": "Beta"} },
        { "activity_code": "002", "external_id": "meet-2001", "collaborator_email": "joao@co.com", "data": {"client": "Gamma", "type": "video"} },
        { "activity_code": "003", "external_id": "prop-3001", "collaborator_email": "joao@co.com", "data": {"value": 50000} },
        { "activity_code": "001", "external_id": "call-1003", "collaborator_email": "ana@co.com", "data": {"client": "Delta"} }
      ]
    }' | jq .
  ```

  ```javascript Node.js theme={null}
  const API_URL = 'https://api.play2sell.com/functions/v1/default-integration';
  const API_KEY = process.env.SALESOS_API_KEY;

  async function callSalesOS(payload, retries = 3) {
    for (let attempt = 1; attempt <= retries; attempt++) {
      const response = await fetch(API_URL, {
        method: 'POST',
        headers: {
          // P2S-SIGN-V1 — ver signedRequest() en /es/api/authentication
          'Authorization': await signedHeader('POST', '/functions/v1/default-integration', payload),
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(payload),
      });

      // Success
      if (response.ok) {
        return await response.json();
      }

      const error = await response.json();

      // Rate limited — wait and retry
      if (response.status === 429) {
        const waitSeconds = error.error?.retry_after || 60;
        console.log(`Rate limited. Waiting ${waitSeconds}s before retry...`);
        await new Promise(r => setTimeout(r, waitSeconds * 1000));
        continue;
      }

      // Server error — retry with backoff
      if (response.status >= 500) {
        const backoff = Math.pow(2, attempt) * 1000;
        console.log(`Server error (${response.status}). Retrying in ${backoff}ms...`);
        await new Promise(r => setTimeout(r, backoff));
        continue;
      }

      // Client error (400, 401, 403) — don't retry
      throw new Error(`SalesOS API error ${response.status}: ${error.error?.message}`);
    }

    throw new Error('Max retries exceeded');
  }

  // ---- Usage ----

  // Sync your team
  const teamResult = await callSalesOS({
    action: 'sync_collaborators',
    collaborators: [
      { external_id: 'emp-101', name: 'Maria Santos', email: 'maria@co.com', team: 'SP' },
      { external_id: 'emp-102', name: 'Joao Silva', email: 'joao@co.com', team: 'SP' },
    ],
  });
  console.log(`Team sync: ${teamResult.data.created} created, ${teamResult.data.existing} existing`);

  // Send activities in batches of 1000
  const allActivities = getActivitiesFromYourCRM(); // your function

  for (let i = 0; i < allActivities.length; i += 1000) {
    const batch = allActivities.slice(i, i + 1000);
    const result = await callSalesOS({
      action: 'sync_activities',
      activities: batch.map(a => ({
        activity_code: a.type_code,        // "001", "002", etc.
        external_id: a.crm_event_id,       // unique ID from your CRM
        collaborator_email: a.agent_email,
        data: { client: a.client_name, notes: a.notes },
        occurred_at: a.timestamp,
      })),
    });
    console.log(`Batch ${i/1000 + 1}: ${result.data.processed} processed, ${result.data.duplicates} duplicates`);
  }
  ```

  ```python Python theme={null}
  import requests
  import os
  import logging
  from datetime import datetime, timedelta
  import time

  logging.basicConfig(level=logging.INFO)
  logger = logging.getLogger('salesos_sync')

  API_URL = 'https://api.play2sell.com/functions/v1/default-integration'
  API_KEY = os.environ['SALESOS_API_KEY']

  HEADERS = {
      'Authorization': f'Bearer {API_KEY}',
      'Content-Type': 'application/json',
  }

  def call_salesos(payload: dict, max_retries: int = 3) -> dict:
      """Call SalesOS API with retry logic for rate limits and server errors."""
      for attempt in range(1, max_retries + 1):
          response = requests.post(API_URL, json=payload, headers=HEADERS)

          if response.ok:
              return response.json()

          error = response.json()
          error_code = error.get('error', {}).get('code', 'UNKNOWN')

          if response.status_code == 429:
              wait = error.get('error', {}).get('retry_after', 60)
              logger.warning(f'Rate limited. Waiting {wait}s...')
              time.sleep(wait)
              continue

          if response.status_code >= 500:
              backoff = 2 ** attempt
              logger.warning(f'Server error ({response.status_code}). Retry in {backoff}s...')
              time.sleep(backoff)
              continue

          raise Exception(f'SalesOS API error {response.status_code}: {error_code} - {error.get("error", {}).get("message")}')

      raise Exception('Max retries exceeded')


  def sync_team(collaborators: list[dict]):
      """Sync collaborators to SalesOS. Safe to run multiple times."""
      result = call_salesos({
          'action': 'sync_collaborators',
          'collaborators': collaborators,
      })
      data = result['data']
      logger.info(f'Team sync: {data["created"]} created, {data["existing"]} existing, {len(data["errors"])} errors')
      if data['errors']:
          for err in data['errors']:
              logger.error(f'  - {err}')
      return data


  def sync_activities(activities: list[dict]):
      """Send activities in batches of 1000."""
      total_processed = 0
      total_duplicates = 0
      total_skipped = 0

      for i in range(0, len(activities), 1000):
          batch = activities[i:i+1000]
          result = call_salesos({
              'action': 'sync_activities',
              'activities': batch,
          })
          data = result['data']
          total_processed += data['processed']
          total_duplicates += data['duplicates']
          total_skipped += data['skipped']
          logger.info(f'Batch {i//1000 + 1}: {data["processed"]} ok, {data["duplicates"]} dup, {data["skipped"]} skip')

      logger.info(f'Total: {total_processed} processed, {total_duplicates} duplicates, {total_skipped} skipped')
      return {'processed': total_processed, 'duplicates': total_duplicates, 'skipped': total_skipped}


  # ---- Example: nightly sync job ----

  if __name__ == '__main__':
      # 1. Sync team (idempotent)
      sync_team([
          {'external_id': 'emp-101', 'name': 'Maria Santos', 'email': 'maria@co.com', 'team': 'SP'},
          {'external_id': 'emp-102', 'name': 'Joao Silva', 'email': 'joao@co.com', 'team': 'SP'},
          {'external_id': 'emp-103', 'name': 'Ana Oliveira', 'email': 'ana@co.com', 'team': 'RJ'},
      ])

      # 2. Sync today's activities from your CRM
      today = datetime.now().strftime('%Y-%m-%d')
      crm_events = fetch_crm_events_for_date(today)  # your function

      activities = [{
          'activity_code': evt['type_code'],
          'external_id': evt['crm_id'],
          'collaborator_email': evt['agent_email'],
          'data': {'client': evt['client'], 'notes': evt.get('notes', '')},
          'occurred_at': evt['timestamp'],
      } for evt in crm_events]

      sync_activities(activities)
  ```

  ```php PHP theme={null}
  <?php

  $api_url        = 'https://api.play2sell.com/functions/v1/default-integration';
  $api_key        = getenv('SALESOS_API_KEY');
  $api_key_secret = getenv('SALESOS_API_SECRET');

  // Construye el header Authorization P2S-SIGN-V1. Aviso: hash_hmac() de
  // PHP recibe (datos, clave) — al revés de Node/Python.
  function p2sSignedHeader(string $method, string $path, string $body): string {
      global $api_key, $api_key_secret;
      $ts          = (string) time();
      $payloadHash = hash('sha256', $body);
      $k1  = hash_hmac('sha256', $api_key,     $api_key_secret, true);
      $k2  = hash_hmac('sha256', $ts,          $k1,             true);
      $k3  = hash_hmac('sha256', $method,      $k2,             true);
      $k4  = hash_hmac('sha256', $path,        $k3,             true);
      $sig = hash_hmac('sha256', $payloadHash, $k4);  // hex
      return "P2S-SIGN-V1 {$api_key}:{$ts}:{$sig}";
  }

  function callSalesOS(array $payload): array {
      global $api_url;
      $body = json_encode($payload);
      $path = parse_url($api_url, PHP_URL_PATH);

      $ch = curl_init($api_url);
      curl_setopt_array($ch, [
          CURLOPT_POST => true,
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_HTTPHEADER => [
              'Authorization: ' . p2sSignedHeader('POST', $path, $body),
              'Content-Type: application/json',
          ],
          CURLOPT_POSTFIELDS => $body,
          CURLOPT_TIMEOUT => 30,
      ]);

      $response = curl_exec($ch);
      $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
      curl_close($ch);

      $result = json_decode($response, true);

      if ($httpCode !== 200) {
          $errorMsg = $result['error']['message'] ?? 'Unknown error';
          throw new Exception("SalesOS API error ({$httpCode}): {$errorMsg}");
      }

      return $result;
  }

  // Sync a collaborator
  $result = callSalesOS([
      'action' => 'sync_collaborators',
      'collaborators' => [[
          'external_id' => 'wp-user-42',
          'name' => 'Carlos Lima',
          'email' => 'carlos@company.com',
          'team' => 'Inside Sales',
      ]],
  ]);
  echo "Created: {$result['data']['created']}\n";

  // Send an activity
  $result = callSalesOS([
      'action' => 'sync_activities',
      'activities' => [[
          'activity_code' => '001',
          'external_id' => 'wp-form-' . uniqid(),
          'collaborator_email' => 'carlos@company.com',
          'data' => ['source' => 'wordpress_form', 'page' => '/contact'],
          'occurred_at' => date('c'),
      ]],
  ]);
  echo "Processed: {$result['data']['processed']}\n";
  ```
</CodeGroup>

***

## Mejores Practicas

### Estrategia de Sincronizacion

* **Colaboradores:** Sincroniza tu equipo completo cada noche. La API es idempotente — los usuarios existentes se actualizan, no se duplican.
* **Actividades:** Sincroniza cada 5-15 minutos, o en tiempo real via webhooks desde tu CRM. Siempre usa el ID de evento de tu CRM como `external_id`.
* **Tamano de lote:** Envia hasta 1000 actividades por solicitud. Para grandes volumenes, divide en lotes secuenciales.

### Elegir el external\_id

El `external_id` es tu clave de deduplicacion. Elige algo **estable y unico** de tu sistema de origen:

| Origen        | Buen `external_id`     | Mal `external_id`          |
| ------------- | ---------------------- | -------------------------- |
| CRM           | `crm-event-12345`      | `random-uuid-each-time`    |
| Base de datos | `db-row-id-789`        | `timestamp-only`           |
| Webhook       | `webhook-delivery-abc` | `user-email` (no es unico) |

### Manejo de fallos

* **Errores 400:** Corrige los datos y reenvia. Revisa el array `details` para errores a nivel de campo.
* **Errores 429:** Espera `retry_after` segundos, luego reintenta. Considera reducir la frecuencia de sincronizacion.
* **Errores 500:** Reintenta con backoff exponencial (2s, 4s, 8s). Contacta a soporte si persiste.
* **Errores de red:** Reintenta de forma segura — la idempotencia via `external_id` previene duplicados.

***

## Limites de Solicitudes

Cada API key tiene un limite de solicitudes configurable (por defecto: 1000 solicitudes por hora). El contador se reinicia cada hora.

| Limite                           | Valor        |
| -------------------------------- | ------------ |
| Solicitudes por hora por defecto | 1000         |
| Max colaboradores por solicitud  | 500          |
| Max actividades por solicitud    | 1000         |
| Timeout por solicitud            | 120 segundos |

***

## Seguridad

* Las API keys se hashean con bcrypt — nunca se almacenan en texto plano
* Cada clave esta limitada a un unico tenant — sin acceso entre tenants
* Se pueden configurar listas de IPs permitidas por clave
* Todas las solicitudes se registran con fines de auditoria
* Las claves pueden revocarse instantaneamente desde el Dashboard

<Warning>
  Nunca expongas tu API key en codigo del lado del cliente (JavaScript ejecutandose en el navegador). La API solo debe ser llamada desde tu servidor backend.
</Warning>

***

## Proximos Pasos

<CardGroup cols={2}>
  <Card title="Autenticacion" icon="key" href="/es/api/authentication">
    Aprende a crear y administrar API Keys
  </Card>

  <Card title="Soporte" icon="headset" href="mailto:suporte@play2sell.com">
    Necesitas ayuda? Contacta a nuestro equipo de soporte
  </Card>
</CardGroup>
