Skip to main content

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:

  • 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.

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.

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)

Source distribution for layrcake 0.1.0
File Size Uploaded
layrcake-0.1.0.tar.gz 115.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for layrcake 0.1.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page