# Python SDK: API Reference

## Client Classes

### Keito

```python
from keito import Keito

client = Keito(
    api_key: str = None,        # defaults to KEITO_API_KEY env var
    account_id: str = None,     # defaults to KEITO_ACCOUNT_ID env var
    base_url: str = "https://app.keito.ai/api/v2",
    max_retries: int = 2,
    timeout: float = 30.0,
)
```

### AsyncKeito

```python
from keito import AsyncKeito

client = AsyncKeito(
    # Same parameters as Keito
)
```

## Time Entries

### client.time_entries.create()

```python
entry = client.time_entries.create(
    project_id: str,            # required
    spent_date: str,            # required, YYYY-MM-DD
    hours: float = 0,           # required unless is_running=True
    task_id: str,                 # required by API v2
    notes: str = None,
    is_running: bool = False,
    billable: bool = None,      # defaults from project
    source: str = "api",
    metadata: dict = None,      # max 4KB
) -> TimeEntry
```

### client.time_entries.list()

```python
entries = client.time_entries.list(
    source: str = None,
    project_id: str = None,
    user_id: str = None,
    from_date: str = None,      # YYYY-MM-DD
    to_date: str = None,        # YYYY-MM-DD
    is_running: bool = None,
    page: int = 1,
    per_page: int = 100,
) -> PaginatedResponse[TimeEntry]
```

### client.time_entries.get()

```python
entry = client.time_entries.get(id: str) -> TimeEntry
```

### client.time_entries.update()

```python
entry = client.time_entries.update(
    id: str,
    hours: float = None,
    notes: str = None,
    is_running: bool = None,
    is_billable: bool = None,
    task_id: str = None,
    metadata: dict = None,
) -> TimeEntry
```

### client.time_entries.delete()

```python
client.time_entries.delete(id: str) -> None
```

## Expenses

### client.expenses.create()

```python
expense = client.expenses.create(
    project_id: str,            # required
    expense_category_id: str,   # required
    spent_date: str,            # required, YYYY-MM-DD
    total_cost: float = None,   # auto-calculated if units + unit_price
    units: float = None,
    unit_price: float = None,
    notes: str = None,
    source: str = "api",
    metadata: dict = None,
) -> Expense
```

### client.expenses.list()

```python
expenses = client.expenses.list(
    source: str = None,
    project_id: str = None,
    user_id: str = None,
    from_date: str = None,
    to_date: str = None,
    cursor: str = None,
    limit: int = 50,
) -> PaginatedResponse[Expense]
```

### client.expenses.get()

```python
expense = client.expenses.get(id: str) -> Expense
```

## Projects

### client.projects.list()

```python
projects = client.projects.list(
    cursor: str = None,
    limit: int = 50,
) -> PaginatedResponse[Project]
```

### client.projects.get()

```python
project = client.projects.get(id: str) -> Project
```

## Types

### TimeEntry

| Field | Type | Description |
|---|---|---|
| `id` | str | Unique identifier |
| `project_id` | str | Project ID |
| `task_id` | str or None | Task ID |
| `user_id` | str | User ID |
| `spent_date` | str | Date (YYYY-MM-DD) |
| `hours` | float | Duration in hours |
| `notes` | str or None | Description |
| `is_running` | bool | Timer active |
| `is_billable` | bool | Billable status |
| `source` | str | Origin |
| `metadata` | dict or None | Agent context |
| `created_at` | str | ISO timestamp |
| `updated_at` | str | ISO timestamp |

### Expense

| Field | Type | Description |
|---|---|---|
| `id` | str | Unique identifier |
| `project_id` | str | Project ID |
| `expense_category_id` | str | Category ID |
| `spent_date` | str | Date (YYYY-MM-DD) |
| `units` | float or None | Quantity |
| `unit_price` | float or None | Price per unit |
| `total_cost` | float | Total amount |
| `notes` | str or None | Description |
| `source` | str | Origin |
| `metadata` | dict or None | Agent context |
| `created_at` | str | ISO timestamp |

### PaginatedResponse

| Field | Type | Description |
|---|---|---|
| `data` | list | Array of results |
| `next_cursor` | str or None | Cursor for next page |
| `has_more` | bool | Whether more results exist |