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

# AI Agent Integration

> Build AI agents that autonomously operate on Leonar CRM data using the API.

This guide explains how to connect an AI agent (Claude, GPT, LangChain, CrewAI, etc.) to the Leonar API so it can search candidates, manage contacts, enroll in sequences, and track deals on behalf of your users.

## How it works

The Leonar API is a standard REST API with an OpenAPI 3.0 spec. AI agent frameworks consume this spec to generate tool definitions automatically.

```
Your Agent  →  OpenAPI Spec  →  Tool Definitions  →  Leonar API
```

The OpenAPI spec at `openapi.yaml` is the single source of truth for REST-based agents. If your client supports MCP, you can also connect directly to Leonar's native [MCP server](/guides/mcp-server).

## Setup

### 1. Create a scoped API key

Go to **Settings > API** and create a key with only the scopes your agent needs.

| Agent use case      | Recommended scopes                                                      |
| ------------------- | ----------------------------------------------------------------------- |
| Read-only research  | `contacts:read`, `projects:read`, `companies:read`                      |
| Sourcing automation | `sourcing:read`, `sourcing:write`, `contacts:read`, `projects:read`     |
| Outreach automation | `sequences:read`, `sequences:write`, `contacts:read`, `messaging:write` |
| Full CRM agent      | `read_only` + `contacts:write`, `projects:write`, `deals:write`         |

<Warning>
  Never give an agent `admin` scope. Always use the minimum scopes required.
</Warning>

### 2. Load the OpenAPI spec

<CodeGroup>
  ```python Claude (tool_use) theme={null}
  import anthropic
  import yaml

  # Load spec and convert operations to tools
  with open("openapi.yaml") as f:
      spec = yaml.safe_load(f)

  # Claude's tool_use format accepts OpenAPI-style schemas directly
  tools = []
  for path, methods in spec["paths"].items():
      for method, operation in methods.items():
          if isinstance(operation, dict) and "operationId" in operation:
              tools.append({
                  "name": operation["operationId"],
                  "description": operation.get("description", operation.get("summary", "")),
                  "input_schema": extract_schema(operation),  # your conversion logic
              })
  ```

  ```python LangChain / OpenAPI toolkit theme={null}
  from langchain_community.agent_toolkits.openapi import planner
  from langchain_community.utilities.requests import TextRequestsWrapper

  headers = {"Authorization": "Bearer leo_your_api_key"}
  toolkit = planner.create_openapi_agent(
      spec="openapi.yaml",
      requests_wrapper=TextRequestsWrapper(headers=headers),
  )
  ```

  ```typescript Vercel AI SDK theme={null}
  import { openapi } from "@ai-sdk/openapi";

  const leonar = openapi({
    url: "https://app.leonar.app/api/v1",
    apiKey: "leo_your_api_key",
    spec: "./openapi.yaml",
  });
  ```
</CodeGroup>

### 3. Set the base URL and auth header

```
Base URL: https://app.leonar.app/api/v1
Authorization: Bearer leo_your_api_key
Content-Type: application/json
```

## Rate limiting strategy

The API allows **2000 requests per hour**. For autonomous agents:

* **Check headers**: Every response includes `X-RateLimit-Remaining` and `X-RateLimit-Reset`
* **Batch operations**: Use bulk endpoints (e.g., enroll up to 500 contacts at once) instead of individual calls
* **Exponential backoff**: On `429` responses, wait `2^attempt` seconds before retrying
* **Pagination**: Default `limit=50`. Use `offset` to paginate. Don't fetch all pages unless needed.

```python theme={null}
import time

def api_call_with_retry(url, **kwargs):
    for attempt in range(5):
        response = requests.request(url=url, **kwargs)
        if response.status_code == 429:
            wait = 2 ** attempt
            time.sleep(wait)
            continue
        return response
    raise Exception("Rate limit exceeded after 5 retries")
```

## Common agent workflows

### Workflow 1: Source and add candidates to a project

```
1. GET /projects                      → Pick target project
2. GET /connected-accounts            → Get LinkedIn account ID
3. GET /sourcing/linkedin/locations   → Resolve location names to IDs
4. POST /sourcing/linkedin/search     → Search with filters
5. POST /sourcing/add-to-project      → Add selected profiles
```

<Warning>
  **`location_ids` requires LinkedIn geo IDs, not plain-text names.** You must call `GET /sourcing/linkedin/locations?q=Paris&account_id=ACCOUNT_ID` first to resolve a location name to its LinkedIn numeric ID. Passing plain text like `{"Paris": "Paris"}` will silently return zero results.
</Warning>

<Warning>
  **LinkedIn search pagination is cursor-based.** After `POST /sourcing/linkedin/search`, store `data.cursor` and send it on the next request. Do not request page 2 by sending only `page: 2`; it can return the same profiles as page 1.
</Warning>

<CodeGroup>
  ```bash Step 1 - List projects theme={null}
  curl -X GET "https://app.leonar.app/api/v1/projects?status=active&limit=10" \
    -H "Authorization: Bearer leo_your_api_key"
  ```

  ```bash Step 2 - Get connected accounts theme={null}
  curl -X GET "https://app.leonar.app/api/v1/connected-accounts" \
    -H "Authorization: Bearer leo_your_api_key"
  ```

  ```bash Step 3 - Resolve locations theme={null}
  curl -X GET "https://app.leonar.app/api/v1/sourcing/linkedin/locations?q=Paris&account_id=ACCOUNT_ID" \
    -H "Authorization: Bearer leo_your_api_key"
  # Response: { "data": [{ "id": "105015875", "title": "France" }, { "id": "102927018", "title": "Paris, Île-de-France, France" }] }
  # Use the id and title in location_ids: {"105015875": "France"}
  ```

  ```bash Step 4 - Search LinkedIn theme={null}
  curl -X POST "https://app.leonar.app/api/v1/sourcing/linkedin/search" \
    -H "Authorization: Bearer leo_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "project_id": "PROJECT_ID",
      "account_id": "ACCOUNT_ID",
      "job_titles": ["Software Engineer", "Backend Developer"],
      "companies": ["Doctolib", "BlaBlaCar"],
      "location_ids": {"105015875": "France"},
      "years_experience": {"min": 3, "max": 10},
      "page": 1,
      "page_size": 25
    }'
  ```

  ```bash Step 5 - Add to project theme={null}
  curl -X POST "https://app.leonar.app/api/v1/sourcing/add-to-project" \
    -H "Authorization: Bearer leo_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "project_id": "PROJECT_ID",
      "account_id": "ACCOUNT_ID",
      "resolve_names": true,
      "profiles": [PROFILES_FROM_SEARCH_RESULTS]
    }'
  ```
</CodeGroup>

<Tip>
  **LinkedIn Classic search may return null `first_name` and `last_name`.** Set `resolve_names: true` and provide `account_id` in the add-to-project request to automatically resolve missing names via LinkedIn profile lookup. Without this, contacts are created with "Unknown" as their first name.
</Tip>

### Workflow 2: Enrich and enroll in sequence

```
1. POST /contacts/search     → Find contacts matching criteria
2. POST /contacts/{id}/enrich → Find email (async)
3. GET  /enrichment/{id}      → Poll until completed
4. GET  /sequences            → Pick target sequence
5. POST /sequences/{id}/enroll → Enroll contacts
```

### Workflow 3: Deal pipeline management

```
1. GET  /deal-pipelines       → Get pipeline stages
2. POST /deals                → Create deal
3. POST /deals/{id}/contacts  → Link contacts to deal
4. PUT  /deals/{id}/stage     → Move through stages
5. POST /deals/{id}/close     → Close as won/lost
```

## Error handling for agents

Your agent should handle these error patterns:

| HTTP code | Error code              | Agent action                                  |
| --------- | ----------------------- | --------------------------------------------- |
| `400`     | `validation_error`      | Fix the request parameters and retry          |
| `401`     | `invalid_api_key`       | Stop — API key is invalid or revoked          |
| `401`     | `insufficient_scope`    | Stop — need a key with more scopes            |
| `403`     | `billing_required`      | Stop — workspace needs an active subscription |
| `403`     | `plan_upgrade_required` | Stop — feature not available on current plan  |
| `404`     | `not_found`             | The resource ID is wrong — verify and retry   |
| `429`     | `rate_limit_exceeded`   | Wait and retry with exponential backoff       |
| `500`     | `internal_error`        | Retry once, then stop and report              |

```python theme={null}
def handle_response(response):
    if response.ok:
        return response.json()

    error = response.json().get("error", {})
    code = error.get("code", "unknown")

    if code == "rate_limit_exceeded":
        return "RETRY"
    elif code in ("invalid_api_key", "insufficient_scope", "billing_required"):
        return f"STOP: {error['message']}"
    elif code == "validation_error":
        return f"FIX_INPUT: {error['message']}"
    elif code == "not_found":
        return "RESOURCE_NOT_FOUND"
    else:
        return f"ERROR: {error['message']}"
```

## Best practices for autonomous agents

<CardGroup cols={2}>
  <Card title="Start with read-only" icon="eye">
    Let the agent explore data before writing. Most mistakes come from creating or updating with wrong data.
  </Card>

  <Card title="Confirm before bulk actions" icon="shield-check">
    Enrolling 500 contacts in a sequence is hard to undo. Have the agent confirm with the user before bulk writes.
  </Card>

  <Card title="Use source-specific endpoints" icon="route">
    Use `/sourcing/linkedin/search` instead of the generic `/sourcing/search`. The flat schemas are easier for agents to construct.
  </Card>

  <Card title="Check existing data first" icon="magnifying-glass">
    Before creating a contact, search by email or LinkedIn URL to avoid duplicates.
  </Card>
</CardGroup>

## Testing your agent

1. **Create a test API key** with `read_only` scopes only
2. **Run your agent** on a read-only task (e.g., "list all active projects and their candidate counts")
3. **Verify outputs** — check that the agent correctly interprets the API responses
4. **Upgrade scopes** once read-only works, add write scopes one at a time
5. **Test write operations** on a test project before going live
