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. It has one dependency, httpx. It works on Python 3.9 or later, with a sync client and an asyncio client. Every request and response is typed.
Install
pip install layrcake
Quickstart
Get an API key at layrcake.dev and set LAYRCAKE_API_KEY.
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.
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
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.
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:
resultis the outcome, typed from the OpenAPI spec. Calls that answer with a bare object, such asget_me, return that object.layrcakeholds billing facts on metered calls:request_id,credits_chargedandcredits_available. It isNoneon endpoints that return none.request_idis 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.
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.
| 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. |
The types live in layrcake.types, for example FindLeadsResultData and LaunchCampaignParams. They are TypedDicts, 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.
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:
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.
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:
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.
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. get_me() is free and shows your balance.
Links
- Docs: docs.layrcake.dev
- For agents: layrcake.dev/agents.md
- MCP server:
https://mcp.layrcake.dev/mcp - OpenAPI spec: api.layrcake.dev/openapi.json
- TypeScript SDK: github.com/Layrcake-dev/layrcake-js
Development
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
Metadata
Release files for layrcake 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| layrcake-0.1.0.tar.gz | 115.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| layrcake-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 183.4 kB
Release files / layrcake-0.1.0.tar.gz
| Download URL | layrcake-0.1.0.tar.gz |
|---|---|
| Size | 115.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d2c58be19e2ad3aebd4dba88175ed6a471a647dec07af75712d634d11bbc34f9
|
|
BLAKE2b-256 checksum How to use checksums |
5a3d765788461a7726232e21fbe2254922211cff0b376a3a1f45d7bd34b2001b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / layrcake-0.1.0-py3-none-any.whl
| Download URL | layrcake-0.1.0-py3-none-any.whl |
|---|---|
| Size | 67.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c9ed8e8ba7b223d24dd6a8a3ad33a3726fb3af0c7b70dc35c63554c39bb4b631
|
|
BLAKE2b-256 checksum How to use checksums |
584b3ec9566675d207df64249f7685c01b75ff0b13ecc5982dd4116156b7b3ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|