> ## 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.

# Activities Integration

> Send numbered activities (001-999) from any CRM or external system to SalesOS using the Activities integration and API Keys.

# Activities Integration

<Tip>
  Try signed requests in your browser at the [API Sandbox](https://play2sellsa.github.io/api-sandbox/) — paste your API key and secret, and the playground signs requests automatically.
</Tip>

<Note>
  Examples below show the wire-format `Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE` header. To compute the signature in your code, use the `signedRequest` helper in [Authentication](/api/authentication).
</Note>

The Activities integration allows you to connect **any CRM or external system** to SalesOS — even if there is no dedicated integration available. You send numbered activities (001-999) via API, and map them to SalesOS missions in the Dashboard.

## How It Works

1. **You get an API Key** from the SalesOS Dashboard (Admin > Integrations > API Keys)
2. **You sync your team** — register collaborators (salespeople) so SalesOS knows who they are
3. **You send activities** numbered 001-999 via a single REST endpoint
4. **Your admin maps** each activity number to a SalesOS mission in the Dashboard
5. **SalesOS processes** the activities as mission completions, awarding points and tracking progress

<Note>
  Activity codes are just numbers (001 through 999). The meaning of each code is defined by your team when mapping them to missions in the Dashboard. For example, "001" could mean "Phone call" and "002" could mean "Meeting scheduled".
</Note>

***

## Quick Start

<Steps>
  <Step title="Get your API Key">
    Go to **Admin > Integrations > API Keys** in the SalesOS Dashboard. Create a new key with the `default:sync` scope. Copy the key — it will only be shown once.

    Your key looks like: `sk_live_a1b2c3d4e5f6g7h8i9j0...`
  </Step>

  <Step title="Register your sales team">
    Before sending activities, tell SalesOS who your salespeople are:

    ```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"
          }
        ]
      }'
    ```

    **Response:**

    ```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="Send activities from your CRM">
    Now send the activities your salespeople performed today:

    ```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"
          }
        ]
      }'
    ```

    **Response:**

    ```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 got 2 activities (two phone calls) and Joao got 1 (a meeting).
  </Step>

  <Step title="Map codes to missions in the Dashboard">
    In the Dashboard, go to **Missions > Configure**. Select **"Activities"** from the CRM dropdown. You will see activities 001 through 999. Map the ones you use:

    | Activity      | Map to Mission      |
    | ------------- | ------------------- |
    | Atividade 001 | Ligacoes Realizadas |
    | Atividade 002 | Reunioes Agendadas  |
    | Atividade 003 | Propostas Enviadas  |

    Click **Save**. From now on, every activity "001" sent via API will count toward the "Ligacoes Realizadas" mission.
  </Step>
</Steps>

***

## Authentication

All requests require an API Key in the `Authorization` header:

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

See the [Authentication page](/api/authentication) for details on creating and managing API Keys.

| Property           | Details                                                  |
| ------------------ | -------------------------------------------------------- |
| **Header**         | `Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE` |
| **Scope required** | `default:sync`                                           |
| **Rate limit**     | Configurable per key (default: 1000 requests/hour)       |
| **Key format**     | `sk_live_` (production) or `sk_test_` (testing)          |

***

## Environments

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

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

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

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

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

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

***

## Endpoint Reference

**Base URL:**

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

The endpoint accepts two actions via the `action` field: `sync_collaborators` and `sync_activities`.

***

### Action: sync\_collaborators

Register or update collaborators (salespeople) in SalesOS. Collaborators must exist before you can attribute activities to them.

#### Request Schema

<ParamField body="action" type="string" required>
  Must be `"sync_collaborators"`
</ParamField>

<ParamField body="collaborators" type="array" required>
  Array of collaborator objects (max 500)
</ParamField>

<ParamField body="collaborators[].external_id" type="string" required>
  Unique ID in your system (max 255 chars)
</ParamField>

<ParamField body="collaborators[].name" type="string" required>
  Full name (max 255 chars)
</ParamField>

<ParamField body="collaborators[].email" type="string" required>
  Valid email — used to match activities later
</ParamField>

<ParamField body="collaborators[].phone" type="string">
  Phone number (any format)
</ParamField>

<ParamField body="collaborators[].document" type="string">
  ID document like CPF (max 20 chars)
</ParamField>

<ParamField body="collaborators[].team" type="string">
  Team name (max 100 chars)
</ParamField>

<ParamField body="collaborators[].role" type="string">
  Role in your org (max 50 chars)
</ParamField>

<ParamField body="collaborators[].metadata" type="object">
  Any extra key-value data
</ParamField>

#### Example: Sync a full team

```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 }
      }
    ]
  }'
```

#### Response (200 — Success)

```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 and Joao were new, so they were created
* `existing: 1` — Ana already existed (matched by email), so she was updated
* `errors: []` — no failures in this batch

#### Example: Partial failures in a batch

If some collaborators have invalid data, they fail individually without blocking the rest:

```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>
  **Re-syncing is safe.** You can send the same collaborators multiple times. Existing ones are updated (not duplicated) based on their email. This makes it easy to run a nightly full sync from your CRM.
</Tip>

***

### Action: sync\_activities

Send activity events attributed to collaborators. Each activity uses a 3-digit code (001-999) that you map to missions in the Dashboard.

#### Request Schema

<ParamField body="action" type="string" required>
  Must be `"sync_activities"`
</ParamField>

<ParamField body="activities" type="array" required>
  Array of activity objects (max 1000)
</ParamField>

<ParamField body="activities[].activity_code" type="string" required>
  3-digit code: `"001"` through `"999"`
</ParamField>

<ParamField body="activities[].external_id" type="string" required>
  Unique ID for deduplication (max 255 chars)
</ParamField>

<ParamField body="activities[].collaborator_email" type="string" required>
  Email of the salesperson who did the activity
</ParamField>

<ParamField body="activities[].data" type="object">
  Any additional context (free-form)
</ParamField>

<ParamField body="activities[].occurred_at" type="string">
  When it happened (ISO 8601). Defaults to now.
</ParamField>

#### Example: Send a day's worth of activities

```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"
      }
    ]
  }'
```

#### Response (200 — Success)

```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"
  }
}
```

#### Response fields explained

<ResponseField name="processed" type="integer">
  Activities successfully recorded
</ResponseField>

<ResponseField name="skipped" type="integer">
  Activities where the collaborator email was not found in SalesOS
</ResponseField>

<ResponseField name="duplicates" type="integer">
  Activities with an `external_id` that was already sent before
</ResponseField>

<ResponseField name="errors" type="array">
  Array of error messages for items that failed
</ResponseField>

<ResponseField name="total" type="integer">
  Total items received in the request
</ResponseField>

#### Example: Mixed results (some skipped, some duplicates)

If you re-send the same batch or include unknown emails:

```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` — one email was not registered via `sync_collaborators`
* `duplicates: 2` — two activities had `external_id` values already in the system

***

## Activity Codes (001-999)

Activity codes are abstract identifiers. Their meaning is entirely up to you.

**Example mapping for a real estate company:**

| Code | CRM Activity                 | SalesOS Mission     | Points |
| ---- | ---------------------------- | ------------------- | ------ |
| 001  | Phone call to prospect       | Ligacoes Realizadas | 5 pts  |
| 002  | Meeting (in-person or video) | Reunioes Agendadas  | 15 pts |
| 003  | Proposal sent                | Propostas Enviadas  | 20 pts |
| 004  | Contract signed              | Contratos Fechados  | 50 pts |
| 005  | Property visit with client   | Visitas Realizadas  | 25 pts |
| 006  | Follow-up email sent         | Follow-ups Enviados | 3 pts  |
| 007  | Lead qualification completed | Leads Qualificados  | 10 pts |

**Example mapping for a SaaS company:**

| Code | CRM Activity           | SalesOS Mission        | Points  |
| ---- | ---------------------- | ---------------------- | ------- |
| 001  | Discovery call         | Ligacoes de Descoberta | 10 pts  |
| 002  | Product demo scheduled | Demos Agendadas        | 20 pts  |
| 003  | Trial started          | Trials Iniciados       | 15 pts  |
| 004  | Proposal sent          | Propostas Enviadas     | 25 pts  |
| 005  | Deal closed            | Deals Fechados         | 100 pts |

<Tip>
  You don't need to use all 999 codes. Most teams use 5-20 codes. Start small and add more as needed.
</Tip>

***

## Idempotency

The `external_id` field guarantees idempotency. If you send the same activity twice with the same `external_id`, the second request reports it as a duplicate — it is **not** processed again.

**First call** — activity is processed:

```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 } }
```

**Second call** (same `external_id`) — safely deduplicated:

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

<Tip>
  **Use your CRM's event ID as the `external_id`.** This ensures that even if your sync job runs twice (e.g., after a crash and retry), activities are never double-counted.
</Tip>

***

## Error Handling

### Error Response Format

All errors follow a consistent structure:

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

### Error Codes Reference

<AccordionGroup>
  <Accordion title="400 — VALIDATION_ERROR">
    Invalid body, missing fields, or bad activity code. Fix the request payload — check the `details` array for field-level specifics.
  </Accordion>

  <Accordion title="401 — UNAUTHORIZED">
    API key is missing, invalid, or expired. Check your `Authorization` header and verify the key in the Dashboard. Generate a new one if expired.
  </Accordion>

  <Accordion title="403 — FORBIDDEN">
    API key is valid but lacks the `default:sync` scope. Edit the key in Dashboard and add the required scope.
  </Accordion>

  <Accordion title="405 — METHOD_NOT_ALLOWED">
    Used GET, PUT, etc. instead of POST. Change to `POST`.
  </Accordion>

  <Accordion title="429 — RATE_LIMITED">
    Too many requests this hour. Wait `retry_after` seconds, then retry. Consider reducing your sync frequency.
  </Accordion>

  <Accordion title="500 — SERVER_ERROR">
    Internal server error. Retry with exponential backoff (2s, 4s, 8s). Contact support if persistent.
  </Accordion>
</AccordionGroup>

### Example: Validation error with details

```bash theme={null}
# Sending an invalid activity_code and a bad email
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" }
    ]
  }'
```

**Response (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" }
    ]
  }
}
```

The `index` field tells you which item in the array has the problem. Item at index 2 (joao) was valid — only the invalid items are listed.

### Example: Rate limit exceeded

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

Wait 1847 seconds (\~31 minutes) before retrying. The rate limit resets every hour.

### Example: Invalid API key

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

***

## Complete Code Examples

<CodeGroup>
  ```bash cURL theme={null}
  # Step 1: Set your credentials (see /api/authentication for the signing helper)
  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 — see signedRequest() in /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');

  // Build the P2S-SIGN-V1 Authorization header. Note: PHP's hash_hmac()
  // takes (data, key) — opposite of Node/Python — so the args look reversed.
  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>

***

## Best Practices

### Sync Strategy

* **Collaborators:** Sync your full team nightly. The API is idempotent — existing users are updated, not duplicated.
* **Activities:** Sync every 5-15 minutes, or in real-time via webhooks from your CRM. Always use your CRM's event ID as `external_id`.
* **Batch size:** Send up to 1000 activities per request. For large volumes, split into sequential batches.

### Choosing external\_id

The `external_id` is your deduplication key. Pick something **stable and unique** from your source system:

| Source   | Good `external_id`     | Bad `external_id`         |
| -------- | ---------------------- | ------------------------- |
| CRM      | `crm-event-12345`      | `random-uuid-each-time`   |
| Database | `db-row-id-789`        | `timestamp-only`          |
| Webhook  | `webhook-delivery-abc` | `user-email` (not unique) |

### Handling failures

* **400 errors:** Fix the data and resend. Check the `details` array for field-level errors.
* **429 errors:** Wait `retry_after` seconds, then retry. Consider reducing your sync frequency.
* **500 errors:** Retry with exponential backoff (2s, 4s, 8s). Contact support if it persists.
* **Network errors:** Retry safely — idempotency via `external_id` prevents duplicates.

***

## Rate Limits

Each API key has a configurable rate limit (default: 1000 requests per hour). The counter resets every hour.

| Limit                         | Value       |
| ----------------------------- | ----------- |
| Default requests per hour     | 1000        |
| Max collaborators per request | 500         |
| Max activities per request    | 1000        |
| Timeout per request           | 120 seconds |

***

## Security

* API keys are hashed with bcrypt — never stored in plaintext
* Each key is scoped to a single tenant — no cross-tenant access
* IP allowlists can be configured per key
* All requests are logged for audit purposes
* Keys can be revoked instantly from the Dashboard

<Warning>
  Never expose your API key in client-side code (JavaScript running in the browser). The API should only be called from your backend server.
</Warning>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/api/authentication">
    Learn how to create and manage API Keys
  </Card>

  <Card title="Support" icon="headset" href="mailto:suporte@play2sell.com">
    Need help? Contact our support team
  </Card>
</CardGroup>
