> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.client-p.pylote.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration guide

> Sync strategy, pagination and handling of deleted profiles

## Sync strategy

The recommended method to sync Pylote freelancers into your database:

### Initial sync

Use `modifiedTime=0` to fetch **all** freelancers, then paginate:

```javascript theme={null}
const axios = require('axios');

async function syncAllFreelances() {
  const API_URL = 'https://client-p.pylote.io/freelances';
  const API_KEY = process.env.PYLOTE_API_KEY;
  const limit = 1000;
  let page = 1;
  let totalPages = 1;
  let allFreelances = [];

  while (page <= totalPages) {
    const response = await axios.get(API_URL, {
      headers: { 'x-api-key': API_KEY },
      params: { modifiedTime: 0, page, limit, showDeleted: false }
    });

    allFreelances.push(...response.data.freelances);
    totalPages = response.data.infos.totalPages;
    page++;
  }

  console.log(`${allFreelances.length} freelancers synced`);
  return allFreelances;
}
```

### Incremental sync

Store the timestamp of your last sync, then fetch only the profiles
modified since then:

```javascript theme={null}
async function syncUpdatedFreelances(lastSyncTimestamp) {
  const response = await axios.get(API_URL, {
    headers: { 'x-api-key': API_KEY },
    params: {
      modifiedTime: lastSyncTimestamp,  // Unix timestamp in seconds
      page: 1,
      limit: 1000,
      showDeleted: true  // include deleted profiles
    }
  });

  for (const freelance of response.data.freelances) {
    if (freelance.meta.status === 'deleted') {
      // REQUIRED: delete from your database
      await deleteFromYourDB(freelance.meta.id);
    } else {
      await upsertInYourDB(freelance);
    }
  }

  // Store the new timestamp for the next sync
  return Math.floor(Date.now() / 1000);
}
```

<Warning>
  Freelancers with `status: deleted` **must be deleted** from your database
  within 30 days. This is a contractual commitment.
  The possible deletion statuses are:
  `account_deleted`, `account_banned`, `account_excluded`, `account_invisible`.
</Warning>

## Pagination

Responses are paginated with the `page` and `limit` parameters.
Each response includes an `infos` object:

```json theme={null}
{
  "freelances": [...],
  "infos": {
    "total": 1234,
    "page": 1,
    "limit": 50,
    "totalPages": 25
  }
}
```

| Parameter      | Type    | Required | Description                                |
| -------------- | ------- | -------- | ------------------------------------------ |
| `page`         | integer | Yes      | Page number (starts at 1)                  |
| `limit`        | integer | Yes      | Freelancers per page (max 1000)            |
| `modifiedTime` | integer | Yes      | Unix timestamp (seconds). `0` = all        |
| `showDeleted`  | boolean | No       | Include deleted profiles (default: `true`) |

<Note>
  Use `limit=1000` to minimize the number of requests.
  An initial sync of \~22,000 freelancers takes about 22 pages.
</Note>

## Deduplication

The same freelancer may be present on several platforms. The `meta.personalEmail`
field (SHA-256 hash of the personal email) lets you deduplicate:

```javascript theme={null}
// If two profiles share the same personalEmail, it's the same freelancer.
// Keep the one with the most recent updatedAt.
const seen = new Map();
for (const f of freelances) {
  const hash = f.meta.personalEmail;
  if (!seen.has(hash) || f.meta.updatedAt > seen.get(hash).meta.updatedAt) {
    seen.set(hash, f);
  }
}
```

## Tracked URLs (hive.pylote.io)

The `basics.url` field (LinkedIn) and the CV link in `basics.profiles` go through
the `hive.pylote.io` proxy. These URLs:

* **Track profile views** for the freelancer's stats
* **Contain a watermark** (the caller's IP encoded in base64)
* **Must not be shared** outside your platform

Format:

```
https://hive.pylote.io/linkedin/{encrypted_id}/{clientPk}?t={watermark}
https://hive.pylote.io/cv/{encrypted_id}/{clientPk}?t={watermark}
```

## Fetching specific freelancers

If you need to re-sync a subset of profiles
(for example after an incident), use `POST /freelances` with a list of IDs:

```bash theme={null}
curl -X POST "https://client-p.pylote.io/freelances?modifiedTime=0&page=1&limit=100" \
  -H "x-api-key: your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [
      "bc1d49cf-1cc7-4afe-92aa-0ebd545fcd12",
      "960a1c67-645d-4ac5-b8b2-875fb2f740d3"
    ]
  }'
```

## Recommended sync frequency

| Strategy           | Frequency    | Use case                              |
| ------------------ | ------------ | ------------------------------------- |
| **Classic cron**   | Every hour   | Enough for most clients               |
| **Near real-time** | Every 15 min | If availability freshness is critical |
| **Initial sync**   | Once         | First population of your database     |

<Info>
  The API cache is refreshed **every hour**. Syncing more often than
  hourly brings no additional data.
</Info>
