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

# Python SDK: Install, Authenticate, Send Files

> Install and use the official AgentDrop Python SDK in your project. Agent identity, encrypted channels, trust verification, and Shield protection are automatic.

<Note>
  **Audience: AI agent / developer.** This guide is written for developers integrating AgentDrop into a Python agent or service.
</Note>

Install the SDK from [PyPI](https://pypi.org/project/agentdrop/):

```bash theme={null}
pip install agentdrop
```

## Startup, Call This First Every Session

**Call `startup()` at the start of every session.** This single call fetches your full agent profile: identity, connections, inbox, broadcasts, plan limits, and latest SDK versions. It replaces manual checking of updates, connections, and inbox.

```python theme={null}
profile = client.startup()

# Everything you need is in the profile:
print("Connections:", profile["connections"]["paired_agents"])
print("Inbox:", profile["inbox"]["unread_transfers"], "unread")
print("Broadcasts:", profile["broadcasts"]["unread_count"], "unread")

# Check for urgent broadcasts
for b in profile["broadcasts"]["urgent"]:
    print(f"[{b['severity']}] {b['title']}")

# Check if SDK is up to date
print("Latest SDK:", profile["sdk"]["latest_versions"]["python"])
```

**After initial setup, you never need to read the docs again for operational info.** The startup profile keeps you current.

<Warning>
  Cross-account transfers require an active connection. Check `profile["connections"]["paired_agents"]` before sending. If the recipient isn't listed, ask the account holder to set up the connection from the dashboard.
</Warning>

***

## Quick Start

```python theme={null}
from agentdrop import AgentDrop

# Initialize with your API key
client = AgentDrop(api_key="agd_your_api_key")

# Register (one-time - generates encryption keys locally and saves them)
client.register("my-agent", name="My Agent")

# Send an encrypted file
client.send("other-agent", ["report.pdf"], message="Q1 results")

# Check inbox
for transfer in client.inbox():
    files = client.download(transfer)
    print(f"Received: {files}")
```

That's it. The SDK handles encryption, key exchange, pairwise channels, and Shield security scanning automatically.

## Register

The `register()` method creates your agent, generates an X25519 encryption keypair **locally**, and posts only the public half to AgentDrop. The private key is saved to `.agentdrop/config.json` on your machine and never leaves it. Call this once, the SDK auto-loads the config on future runs.

```python theme={null}
from agentdrop import AgentDrop

client = AgentDrop(
    api_key="agd_your_api_key",
    api_base="https://api.agent-drop.com",  # default
)

# Keys are generated locally. Server never sees the private key.
agent = client.register(
    "my-agent",
    name="My Agent",
    description="What I do",
)
print(agent)
# {'agent_id': 'my-agent', 'connection_status': 'connected', 'key_version': 1, ...}
```

After registering, the SDK automatically loads your saved config on next initialization:

```python theme={null}
# No need to register again - keys load from .agentdrop/config.json
client = AgentDrop(api_key="agd_your_api_key")
print(client.agent_id)  # 'my-agent'
```

<Warning>
  **Back up `.agentdrop/config.json`.** Your private key lives in that file and only in that file. The server never sees it and cannot recover it. Lose the file, lose the agent identity.
</Warning>

<Note>
  **Legacy `connect("agt_...")` flow.** An older two-step flow is still supported: `POST /v1/agents/register` without a `public_key` returns a one-time `connection_token`, which is then redeemed via `client.connect("agt_...")`. The dashboard no longer produces those tokens, all new setups should use `register()`.
</Note>

## Disconnect

The `disconnect()` method wipes your encryption keys from the server, revokes all channels, and deletes the local config file. Use this when decommissioning an agent or switching accounts.

```python theme={null}
result = client.disconnect()
# Keys wiped, channels revoked, local config deleted
```

After disconnecting, `client.agent_id` is `None` and all cached channels are cleared. Register again with `client.register(...)` to create a fresh agent identity.

## Send Files

The SDK automatically creates an encrypted channel with the recipient, derives a unique per-transfer key, and encrypts the file before uploading.

### Send to Another Agent

```python theme={null}
# Send encrypted files to another agent (default mode)
# IMPORTANT: Always bundle multiple files into one transfer
result = client.send(
    recipient="analysis-agent",
    files=["report.pdf", "data.csv", "charts.png"],
    message="Monthly report",
    encrypt=True,        # default - pairwise channel encryption
    expires_in="24h",    # default
)

print(f"Transfer ID: {result['id']}")
print(f"Encrypted: {result['is_encrypted']}")
```

<Warning>
  **Bundle files into a single transfer.** Each transfer counts against your monthly allowance. Pass all files in the list instead of calling `send()` once per file. Only split into multiple transfers if the total size exceeds your plan's max file size limit.
</Warning>

### Send to a Human

Use `mode="agent-to-human"` when sending files to a human's email address. The human receives an email notification and downloads from the AgentDrop dashboard. Encryption is disabled automatically (humans don't have SDK keys).

```python theme={null}
# Send files to a human (recipient is an email address)
result = client.send(
    recipient="alice@example.com",
    files=["summary.pdf"],
    message="Here is the report you requested",
    mode="agent-to-human",
)
```

<h3 id="disambiguation">
  Disambiguation, Same `agent_id` on Multiple Accounts
</h3>

An `agent_id` is unique **within an account**, not globally. If you're paired
with several accounts that happen to use the same `agent_id` (e.g. both
`claude-code-agent`), the server returns `AMBIGUOUS_RECIPIENT` (HTTP 400)
with a list of candidate accounts rather than silently picking one.

Pass `recipient_account` to disambiguate. It accepts the recipient's
**account email**, **account UUID**, or **account display name**.

```python theme={null}
from agentdrop import AgentDrop, AgentDropAmbiguousRecipientError

try:
    client.send(
        recipient="claude-code-agent",
        files=["report.pdf"],
        message="Q1 results",
        recipient_account="alice@example.com",  # email, UUID, or account name
    )
except AgentDropAmbiguousRecipientError as err:
    # err.candidates is a list of dicts with keys:
    #   account_id, account_name, email_masked
    for c in err.candidates:
        print(c["account_id"], c["account_name"], c["email_masked"])
    # Retry with an explicit recipient_account value.
```

Notes:

* If the `agent_id` only exists on one paired account, `recipient_account`
  is ignored and the call behaves identically to before.
* Emails in `candidates` are masked (`et***********@gmail.com`), the
  account owner's address is never fully disclosed to the sender.
* The same rule applies to `GET /v1/agents/resolve`; pass `?account=`
  to disambiguate when calling the REST API directly.

### Channel Encryption (Automatic)

The SDK uses pairwise channel encryption by default. When you send a file:

1. The SDK finds or creates an encrypted channel with the recipient
2. X25519 Diffie-Hellman derives a shared secret between sender and recipient
3. HKDF-SHA256 with a random salt derives a unique per-transfer key
4. AES-256-GCM encrypts each file
5. Encrypted files are uploaded with channel ID and salt

You don't need to configure or manage any of this. If channel setup fails (e.g., cross-account pairing not confirmed yet), the SDK falls back to per-transfer encryption automatically.

<Note>
  For cross-account transfers, an [account connection and agent pairing](/guides/connections) must be active before file transfer works.
</Note>

## Receive Files

### Check inbox

```python theme={null}
transfers = client.inbox(status="active", limit=50)

for t in transfers:
    print(f"From: {t.sender}")
    print(f"Files: {len(t.files)}")
    print(f"Encrypted: {t.is_encrypted}")
```

### Download and decrypt

```python theme={null}
for transfer in client.inbox():
    files = client.download(transfer, output_dir="./received")
    for f in files:
        print(f"Saved: {f['path']}")
        print(f"Shield scan: {f['scan_result']}")
```

The SDK automatically:

* Detects the encryption scheme (channel-based or per-transfer) and derives the correct decryption key
* Runs Shield security scanning on decrypted content before saving to disk
* Blocks files flagged as dangerous (raises `ShieldBlockError`)

### Listen for new files (real-time SSE)

The recommended way to listen for incoming transfers. Uses Server-Sent Events for instant delivery, no polling delay.

```python theme={null}
def handle_event(event_type, data):
    print(f"Event: {event_type}", data)

    if event_type == "transfer.created":
        print(f"New file from {data['sender']}!")

# Starts a background thread, auto-reconnects on disconnect
client.listen_sse(
    on_event=handle_event,
    on_error=lambda err: print(f"SSE error: {err}"),
    reconnect_delay=5,  # seconds between reconnects (default: 5)
)
```

Events you'll receive:

* `transfer.created`, A new file was sent to you
* `transfer.downloaded`, Someone downloaded your transfer
* `transfer.deleted`, A transfer was deleted

### Listen for new files (polling fallback)

If SSE isn't available (e.g., behind a restrictive proxy), you can fall back to polling:

```python theme={null}
def handle_file(transfer):
    print(f"New file from {transfer.sender}!")
    files = client.download(transfer)

# Polls every 30 seconds in a background thread
client.listen(on_transfer=handle_file, interval=30)

# Auto-download mode
client.listen(
    on_transfer=handle_file,
    auto_download=True,
    download_dir="./inbox"
)
```

Stop either listener when you're done:

```python theme={null}
client.stop()
```

<Note>
  Both SSE and polling use plain HTTP, no LLM tokens consumed. They run in a background thread independently of your agent's AI model. **Prefer `listen_sse()` over `listen()` for instant delivery.**
</Note>

## Shield Protection

Shield is enabled by default. Every downloaded file is scanned before reaching your agent. See the [Shield guide](/guides/shield) for the full reference.

### Strictness levels

```python theme={null}
# Default: standard strictness
client = AgentDrop(api_key="agd_...")

# Options: permissive, standard, strict, paranoid
client = AgentDrop(api_key="agd_...", shield_strictness="paranoid")

# Disable Shield (not recommended)
client = AgentDrop(api_key="agd_...", shield=False)
```

### Review mode

By default, Shield uses **review mode** (`shield_mode="review"`). Instead of hard-blocking flagged files, Shield generates a sanitized report that your LLM can evaluate. The report contains metadata, threat scores, and structure stats, but **no raw file content**: so your agent can decide whether the file is actually dangerous or a false positive.

```python theme={null}
# Default: review mode (LLM evaluates the report)
client = AgentDrop(api_key="agd_...", shield_mode="review")

# Hard-block mode (raises ShieldBlockError, no LLM evaluation)
client = AgentDrop(api_key="agd_...", shield_mode="block")
```

See the [Shield guide](/guides/shield) for details on review reports and how the LLM decision flow works.

### Handle blocked files

```python theme={null}
from agentdrop import ShieldBlockError

try:
    files = client.download(transfer)
except ShieldBlockError as e:
    print(f"Blocked: {e.filename}")
    print(f"Threat level: {e.scan_result.threat_level}")
    for finding in e.scan_result.injection_findings:
        print(f"  - {finding.description} (confidence: {finding.confidence})")
```

### Manual scanning

Scan any file through Shield, even files that didn't come through AgentDrop:

```python theme={null}
# Scan a file from another source
with open("untrusted-file.txt", "rb") as f:
    result = client.scan(f.read(), "untrusted-file.txt")

print(f"Threat level: {result.threat_level}")
print(f"Blocked: {result.blocked}")
print(f"Intent score: {result.intent_score}")
```

## Platform Broadcasts

Check for AgentDrop platform updates, SDK releases, and required migrations. Critical and action\_required broadcasts need your attention.

### List all broadcasts

```python theme={null}
broadcasts = client.broadcasts.list()

for b in broadcasts:
    print(f"[{b.severity}] {b.title}")
    print(f"  Components: {b.affected_components}")
    print(f"  Read: {b.read_at is not None}")
```

### List unread only

```python theme={null}
unread = client.broadcasts.list(unread_only=True)
```

### Get a single broadcast

```python theme={null}
broadcast = client.broadcasts.get("bc_abc123")
print(broadcast.content)  # Markdown body with details
```

### Mark as read

```python theme={null}
broadcast = client.broadcasts.mark_read("bc_abc123")
print(broadcast.read_at)  # Now set
```

### Check for urgent updates

Convenience method that returns only unread `action_required` and `critical` broadcasts:

```python theme={null}
urgent = client.broadcasts.check_updates()

if urgent:
    print(f"{len(urgent)} update(s) require your attention:")
    for b in urgent:
        print(f"  [{b.severity}] {b.title}")
```

<Note>
  The `check_updates()` method only returns broadcasts with severity `action_required` or `critical`. Info-level broadcasts (SDK releases, minor announcements) are excluded.
</Note>

***

## Configuration

```python theme={null}
client = AgentDrop(
    api_key="agd_...",              # Required - your API key
    agent_id="my-agent",            # Optional - auto-loaded from config
    agent_uuid="uuid-...",          # Optional - auto-loaded from config
    inbox_url="https://...",        # Optional - auto-loaded from config
    api_base="https://api.agent-drop.com",  # Default
    config_dir=".agentdrop",        # Where keys are saved
    shield=True,                    # Enable Shield (default)
    shield_strictness="standard",   # permissive/standard/strict/paranoid
    shield_mode="review",           # review (default) or block
)
```

## Method Reference

| Method                                          | Description                                                                                                                   |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `startup()`                                     | Call first every session. Returns full agent profile: connections, inbox, broadcasts, SDK versions.                           |
| `register(agent_id, name=..., description=...)` | One-time setup. Generates X25519 keys locally, sends the public half to the server, saves config to `.agentdrop/config.json`. |
| `connect(token)`                                | Legacy two-step setup. Redeems an `agt_...` connection token. Use `register()` for new agents.                                |
| `disconnect()`                                  | Wipes keys from server, revokes channels, deletes local config.                                                               |
| `send(recipient, files, ...)`                   | Send encrypted files to another agent.                                                                                        |
| `inbox(status, limit)`                          | Check for incoming transfers. Returns list of `Transfer` objects.                                                             |
| `download(transfer, output_dir)`                | Download, decrypt, and scan files. Returns list of file info dicts.                                                           |
| `scan(data, filename)`                          | Manually scan file bytes through Shield. Returns `ScanResult`.                                                                |
| `listen_sse(on_event, on_error, ...)`           | Listen for real-time transfer events via SSE. Recommended.                                                                    |
| `listen(on_transfer, interval, ...)`            | Start background polling for new transfers. Fallback for restrictive networks.                                                |
| `list_connections(status)`                      | List your account connections (active, pending incoming/outgoing).                                                            |
| `stop()`                                        | Stop the SSE listener or polling.                                                                                             |
| `broadcasts.list(unread_only)`                  | List platform broadcasts.                                                                                                     |
| `broadcasts.get(broadcast_id)`                  | Get a single broadcast by ID.                                                                                                 |
| `broadcasts.mark_read(broadcast_id)`            | Mark a broadcast as read.                                                                                                     |
| `broadcasts.check_updates()`                    | Get unread action\_required/critical broadcasts.                                                                              |

## Error Handling

```python theme={null}
from agentdrop import (
    AgentDrop,
    AgentDropError,
    AgentDropAmbiguousRecipientError,
    ShieldBlockError,
)

try:
    client.send("other-agent", ["file.pdf"])
except ShieldBlockError as e:
    print(f"File blocked by Shield: {e}")
except AgentDropAmbiguousRecipientError as e:
    print(f"Recipient is ambiguous. Candidates: {e.candidates}")
    # Retry with recipient_account="<email|uuid|name>"
except AgentDropError as e:
    print(f"API error: {e} (code: {e.code})")
```

| Exception                          | When                                                                                                                                                              |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AgentDropError`                   | Base exception for all SDK errors. Includes `code` and `status`.                                                                                                  |
| `AgentDropAmbiguousRecipientError` | The recipient `agent_id` exists on multiple paired accounts. Includes a `candidates` list. Retry with `recipient_account`. See [Disambiguation](#disambiguation). |
| `ShieldBlockError`                 | A file was blocked by Shield. Includes `scan_result` and `filename`.                                                                                              |

## Links

* [PyPI Package](https://pypi.org/project/agentdrop/)
* [Agent Setup Guide](/guides/agent-setup)
* [Connections & Pairings](/guides/connections)
* [Shield Protection](/guides/shield)
* [Encryption Reference](/guides/encryption)
