Metadata-Version: 2.5
Name: layrcake
Version: 0.1.0
Summary: Official Python SDK for the Layrcake API. Go-to-market infrastructure for AI agents.
Project-URL: Homepage, https://layrcake.dev
Project-URL: Documentation, https://docs.layrcake.dev
Project-URL: Source, https://github.com/Layrcake-dev/layrcake-python
Project-URL: Issues, https://github.com/Layrcake-dev/layrcake-python/issues
Project-URL: Changelog, https://github.com/Layrcake-dev/layrcake-python/blob/main/CHANGELOG.md
Author: Layrcake
License-Expression: MIT
License-File: LICENSE
Keywords: agents,ai,chatgpt,claude,email-verification,layrcake,lead-generation,mcp,prospecting,sales
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.23.0
Description-Content-Type: text/markdown

# Layrcake Python SDK

Go-to-market infrastructure for AI agents: find leads, enrich and verify emails, read buying signals, research companies, and run email campaigns that a person approves before anything sends.

This is the official Python client for the [Layrcake API](https://docs.layrcake.dev). It has one dependency, [httpx](https://www.python-httpx.org). It works on Python 3.9 or later, with a sync client and an asyncio client. Every request and response is typed.

## Install

```sh
pip install layrcake
```

## Quickstart

Get an API key at [layrcake.dev](https://layrcake.dev) and set `LAYRCAKE_API_KEY`.

```python
from layrcake import Layrcake, approval_required

client = Layrcake()  # reads LAYRCAKE_API_KEY

found = client.find_leads(
    audience="Heads of sales at US software companies with 50 to 500 staff",
    titles=["Head of Sales"],
    locations=["United States"],
    industries=["Software Development"],
    company_headcount={"min": 50, "max": 500},
    limit=5,
)
lead = found.result["leads"][0]
enriched = client.enrich(
    person={"full_name": lead["full_name"] or "", "company_domain": lead["company_domain"] or ""},
    want=["work_email"],
)
contact = enriched.result["contact"]
email = (contact["work_email"] if contact else None) or lead["work_email"]
if not email:
    raise SystemExit("No work email found for this lead.")
check = client.verify_email(email=email)
if check.result["status"] != "valid":
    raise SystemExit(f"{email} is {check.result['status']}")
draft = client.create_campaign(
    name="Q4 sales leaders",
    leads=[{"email": email}],
    steps=[{"subject": "Your outbound this quarter", "body": "Hi {{first_name}}, ..."}],
)
preview = client.launch_campaign(campaign_id=draft.result["campaign_id"])
approval = approval_required(preview)
if approval is not None:
    print("Ask a person to approve:", approval.approval_url)
```

The last call sends nothing. It returns a preview and asks a person to approve. Emails go out only after a signed-in person approves in the Layrcake console.

### Async

`AsyncLayrcake` has the same methods. Each one is a coroutine.

```python
import asyncio
from layrcake import AsyncLayrcake


async def main() -> None:
    async with AsyncLayrcake() as client:
        me = await client.get_me()
        print(me.result["workspace"]["name"])


asyncio.run(main())
```

## Configuration

```python
client = Layrcake(
    api_key="lc_...",  # default: the LAYRCAKE_API_KEY environment variable
    base_url="https://api.layrcake.dev",  # default: LAYRCAKE_BASE_URL, then this
    max_retries=2,  # default
    timeout=60.0,  # seconds per attempt, default. An httpx.Timeout also works.
    http_client=None,  # your own httpx.Client, for proxies or custom transports
    default_headers=None,  # headers sent with every request
)
```

The constructor raises `LayrcakeError` if it finds no key. Keep the key on a server. Do not ship it to a browser or commit it.

Every method also takes per-call options as keyword arguments: `timeout`, `extra_headers`, `extra_query` and, for calls with a body, `extra_body`. `with_options` gives a copy of the client with other settings. It shares the connection pool.

```python
client.with_options(max_retries=0, timeout=5.0).get_me()
```

Use the client as a context manager, or call `close()`, to release connections. A client you pass in as `http_client` is never closed for you.

## Methods

Each method is named after the API operation. Body fields are keyword arguments. Path parameters come first, as positional arguments. Every method returns a `LayrcakeResponse`:

- `result` is the outcome, typed from the OpenAPI spec. Calls that answer with a bare object, such as `get_me`, return that object.
- `layrcake` holds billing facts on metered calls: `request_id`, `credits_charged` and `credits_available`. It is `None` on endpoints that return none.
- `request_id` is the request id. Quote it to support.

Credit amounts are `decimal.Decimal`, parsed from the decimal strings the API sends. The SDK never turns them into floats. `layrcake.raw` keeps the original strings.

```python
response = client.verify_email(email="ada@example.com")
print(response.result["status"], response.credits_charged, response.request_id)
```

Arguments you leave out are not sent. To send a field this version does not know yet, use `extra_body`.

`find_leads` needs an `audience` and at least one of `titles`, `industries`, `locations` or `company_domains`. `find_companies` needs at least one of `query`, `hiring`, `technologies` or `similar_to`. The spec marks these filters optional, but the API refuses a search without one.

<!-- methods:start (generated by scripts/generate.py; do not edit) -->

| Method | Endpoint | What it does |
| --- | --- | --- |
| `verify_email(**params)` | `POST /v1/verify_email` | Check whether one email address can receive mail, including catch-all analysis. |
| `enrich(**params)` | `POST /v1/enrich` | Find a person's work email, mobile or profile from an email, LinkedIn URL, or name and company. |
| `find_leads(**params)` | `POST /v1/find_leads` | Find people who match an audience description and filters. |
| `find_intent(**params)` | `POST /v1/find_intent` | Find hiring, funding, news and job-change signals for a list of company domains. |
| `check_companies(**params)` | `POST /v1/check_companies` | Answer one yes or no question about each company in a list, with evidence. |
| `find_companies(**params)` | `POST /v1/find_companies` | Find companies that match a description and filters. |
| `enrich_company(**params)` | `POST /v1/enrich_company` | Get profile, technology, funding or relationship data for one company domain. |
| `find_posts(**params)` | `POST /v1/find_posts` | Find social posts by topic, author or company, or the people engaging with given posts. |
| `setup_sending(**params)` | `POST /v1/setup_sending` | Quote, then buy after human approval, warmed domains and mailboxes for sending. |
| `create_campaign(**params)` | `POST /v1/campaigns` | Create a draft email campaign with leads and sequence steps. Nothing sends yet. |
| `add_leads(**params)` | `POST /v1/add_leads` | Add leads to a campaign. Adding to an active campaign needs human approval. |
| `launch_campaign(**params)` | `POST /v1/launch_campaign` | Launch a draft campaign. Returns a preview first. A person approves before anything sends. |
| `get_replies(**params)` | `POST /v1/get_replies` | List campaign replies, newest first. Free. |
| `get_reply(**params)` | `POST /v1/get_reply` | Read one reply in full, including its body and lead state. Free. |
| `set_lead_status(**params)` | `POST /v1/set_lead_status` | Record a lead's interest status, such as interested or meeting_booked. Free. |
| `reply_to_thread(**params)` | `POST /v1/reply_to_thread` | Reply in an existing thread. Returns a preview first. A person approves before it sends. |
| `research_company(**params)` | `POST /v1/research_company` | Run source-backed research on one company in the background. |
| `prospect_picture(**params)` | `POST /v1/prospect_picture` | Build one picture of a person, a company, or both, in the background. |
| `llm(**params)` | `POST /v1/llm/chat` | Chat with a hosted model through Layrcake. |
| `get_me()` | `GET /v1/me` | Who this key is and what it may do. |
| `get_trail(**params)` | `GET /v1/trail` | Your workspace's outcome record, newest first, typed per kind. |
| `export_trail_csv(**params)` | `GET /v1/trail/export.csv` | Complete bounded trail CSV. |
| `get_campaign_proof(campaign_id)` | `GET /v1/campaigns/{id}/proof` | Whole retained campaign proof as safe JSON. |
| `tighten_key_policy(key_id, **params)` | `PATCH /v1/keys/{id}` | Tighten another key's policy (lower caps/limits, drop scopes). |
| `import_do_not_contact(**params)` | `POST /v1/do_not_contact` | Add up to 5,000 emails to the workspace do-not-contact list. |
| `kill_sending(**params)` | `POST /v1/kill` | Stop sending for the whole workspace: pauses every launching/active campaign (provider-verified) and blocks new launches, purchases and replies until an owner clears it in the console. |
| `pause_campaign(campaign_id)` | `POST /v1/campaigns/{id}/pause` | Pause one campaign through verified containment. |
| `get_operation(operation_id, **params)` | `GET /v1/operations/{id}` | Read the state, progress and result of a background operation. |

<!-- methods:end -->

The types live in `layrcake.types`, for example `FindLeadsResultData` and `LaunchCampaignParams`. They are `TypedDict`s, so results are plain dicts at runtime.

## Dry runs

Most paid calls accept `dry_run=True`. The call returns a plan instead: the most it can cost, how many sources could answer, and any limit it would trip. Nothing is looked up, reserved or charged. The return type follows the flag.

```python
plan = client.find_leads(
    audience="CFOs at UK fintechs",
    titles=["CFO"],
    locations=["United Kingdom"],
    industries=["Financial Services"],
    dry_run=True,
)
print(plan.result["plan"]["would"]["maximum_credits"])
```

`budget_credits` sets a spend ceiling for one call. A call that could cost more is refused before anything is charged.

## Idempotency

Paid and external calls take an `Idempotency-Key`. If you do not pass one, the SDK generates a UUID for each call and reuses it across its automatic retries. The API then returns the first outcome instead of running or charging twice.

Pass your own key when you may retry later, for example after a crash or a timeout:

```python
client.create_campaign(name="Q4", leads=leads, steps=steps, idempotency_key=f"campaign-{job_id}")
```

When a call fails, `error.idempotency_key` holds the key that was sent. Retry with it to get the original outcome. Endpoints without an idempotency key, such as `get_me`, never get one.

## Retries

The SDK retries `429`, `502`, `503` and `504` responses and network errors, up to `max_retries` times (default 2). It waits with exponential backoff and jitter, from 0.5 seconds up to 8. It honours `Retry-After` and the API's `details.retry_after_seconds`. If the server asks for a wait longer than 60 seconds, the SDK raises instead so you can decide.

It never retries any other `4xx`. A `POST` is retried after a `5xx` or a dropped connection only when it carries an idempotency key or only reads, so a retry cannot act twice. Every write endpoint in the API takes a key, and the SDK always sends one.

## Errors

Every error is a `LayrcakeError` with `status`, `code`, `message`, `details`, `request_id` and `idempotency_key`. Subclasses let you catch by kind:

| Class | When |
| --- | --- |
| `AuthenticationError` | 401. The key is missing, wrong, revoked or expired. |
| `PermissionDeniedError` | 403. A missing scope, a paused agent, or a disabled feature. |
| `ApprovalError` | `confirmation_required`, or an approval that was declined, expired, superseded, cancelled or changed. |
| `InsufficientCreditsError` | 402, or a run budget reached. Top up or raise the budget. |
| `BadRequestError` | 400 or 422. `details` usually names the field. |
| `NotFoundError` | 404. |
| `ConflictError` | 409, such as an idempotency key reused with different parameters. |
| `RateLimitError` | 429. `retry_after_seconds` says when to try again. |
| `InternalServerError` | 5xx, such as `provider_unavailable`. |
| `APIConnectionError` | No response arrived. |
| `APITimeoutError` | The request passed its timeout. |
| `APIResponseValidationError` | A success status with a body the SDK cannot read. |
| `OperationTimeoutError` | `wait_for_operation` ran out of time. |

All HTTP errors also derive from `APIStatusError`.

```python
from layrcake import InsufficientCreditsError, LayrcakeError

try:
    client.find_leads(audience="CTOs at US startups", titles=["CTO"], locations=["United States"])
except InsufficientCreditsError:
    print("Top up at https://layrcake.dev")
except LayrcakeError as error:
    print(error.code, error.message, error.request_id)
```

`code` is a string, such as `insufficient_credits` or `provider_unavailable`. Branch on it for detail. `API_ERROR_CODES` lists every code in the spec, and `layrcake.types.ErrorCode` is the same list as a type.

## Background operations

Some calls start work that finishes later and return an `operation_id`. Examples are `research_company`, `prospect_picture` and a confirmed `setup_sending`. Wait for it with `wait_for_operation`:

```python
started = client.research_company(company_domain="acme.com")
operation = client.wait_for_operation(started.result["operation_id"], interval=2.0, timeout=600.0)
if operation["state"] == "succeeded":
    print(operation["result"])
```

It polls `GET /v1/operations/{id}` until the state is `succeeded`, `partially_succeeded`, `failed`, `cancelled` or `unknown`, then returns the operation. It does not raise on `failed`, so check `state`. `unknown` means a provider's outcome could not be confirmed and Layrcake is reconciling it. Do not repeat the action while it is `unknown`. If the timeout passes first, it raises `OperationTimeoutError`, and the operation keeps running.

## Approvals

Nothing reaches a real person until a person on your side says yes. `launch_campaign`, `setup_sending`, `reply_to_thread`, and `add_leads` on an active campaign first return a preview with a `confirmation_token` and, on most calls, an `approval_url`. A signed-in person approves in the Layrcake console. Then you repeat the call with the token, or the server runs it.

```python
from layrcake import approval_required

preview = client.launch_campaign(campaign_id=campaign_id)
approval = approval_required(preview)
if approval is not None:
    print("Approve here:", approval.approval_url)
    # Later, after the person approves:
    client.launch_campaign(
        campaign_id=campaign_id,
        confirmation_token=approval.confirmation_token,
        idempotency_key=f"launch-{campaign_id}",
    )
```

`approval_required` returns `None` when nothing waits on a person. The SDK never approves anything and has no way to.

## Pricing

Calls are billed in credits. See [docs.layrcake.dev/pricing.html](https://docs.layrcake.dev/pricing.html). `get_me()` is free and shows your balance.

## Links

- Docs: [docs.layrcake.dev](https://docs.layrcake.dev)
- For agents: [layrcake.dev/agents.md](https://layrcake.dev/agents.md)
- MCP server: `https://mcp.layrcake.dev/mcp`
- OpenAPI spec: [api.layrcake.dev/openapi.json](https://api.layrcake.dev/openapi.json)
- TypeScript SDK: [github.com/Layrcake-dev/layrcake-js](https://github.com/Layrcake-dev/layrcake-js)

## Development

```sh
uv sync                                # install the dev tools
uv run python scripts/generate.py      # regenerate types, methods and the table above from openapi.json
uv run python scripts/generate.py --check   # fail if generated code is stale
uv run ruff check . && uv run ruff format --check .
uv run mypy
uv run pytest
uv build
```

`make check` runs all of them. To update the API surface, replace `openapi.json` with the live spec and run the generator.

## License

MIT
