LeadHound REST API · v1
Every lead, call and conversion — over HTTP
Read the leads and calls LeadHound captured, push leads in from your own systems, and pull the attribution and ROI numbers behind them. JSON in, JSON out, authenticated with an API key you can create in a few seconds.
Base URL
https://getleadhound.io/api/public/v1curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/me"Introduction
The API is organised around the objects the product is: leads, the calls some of them came from, the websites and tracking numbers that produced them, and the reports that join them to ad spend. Every path lives under one versioned base URL, and every response is JSON.
Requests are scoped to your account automatically. There is no account id in any path — the key you authenticate with decides what you can see, and a record belonging to someone else is a 404, never a partial result.
Single objects are returned unwrapped. Collections come back under a key named for what they hold — leads, calls, profiles — alongside their pagination counters. There is no generic data envelope to unwrap.
Authentication
The API uses HTTP Basic authentication. The key’s token is the username and its secret is the password — every HTTP client has a first-class way to send that, so there is nothing to hand-roll.
Create a key under Settings → Integrations → API Key. The secret is shown once, at creation. There are two kinds:
ltk_…An account key, bound to one account. 10,000 requests a day.ltm_…A master key, held by an agency, valid for every client account in its organization. 50,000 requests a day.
Keep keys server-side
HTTP Basic credentials cannot be used from a browser without exposing them. Call the API from your backend. If you need to capture leads from a page, use the tracking snippet or the public capture endpoint instead — neither needs a secret.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/me"curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
-H "X-Account-Id: 43" \
"https://getleadhound.io/api/public/v1/leads"Accounts & websites
Data sits at three levels: an organization (an agency) owns accounts, and each account owns one or more websites. A website is called a profile, and its id is what every profile_id filter in this API expects.
An account key is pinned to its own account and needs nothing extra. A master key must say which account each request is for, with the X-Account-Id header — without it the request is a 400. Call GET /accounts for the ids you may use.
Omitting profile_id means “every website on this account”. That is the intended account-wide view, not a missing filter.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
-H "X-Account-Id: 43" \
"https://getleadhound.io/api/public/v1/leads"Pagination
Collections are paged with page_number and a per-page size named for the collection — leads_per_page on leads, calls_per_page on calls. Both are 1-indexed.
Every paged response reports total_pages and the total record count, so you can size a job before you run it. Keep pages modest and let the counters drive the loop rather than asking for the maximum every time; the ceiling exists for backfills, not for polling.
Paging a moving collection
New leads arrive while you page, which shifts everything down by one. For a full export, pin the window with `start_date` and `end_date` and page inside it — the set is then fixed no matter how long the export takes.
{
"page_number": 2,
"leads_per_page": 50,
"total_pages": 3,
"total_leads": 118,
"leads": [ /* … */ ]
}Errors
Errors use conventional HTTP status codes and always return the same body: a human-readable message, an error object naming the machine-readable type, and — for a validation failure — an errors map keyed by field.
Branch on error.type rather than on the message text. Messages are written for whoever is reading the log and may be reworded; the types below are stable.
| Status | Type | When you see it |
|---|---|---|
| 400 | invalid_request_error | The request could not be understood. A master key sent without an X-Account-Id header lands here. |
| 401 | authentication_error | No credentials, or a token/secret pair that does not match a key. |
| 403 | permission_error | Authenticated, but this key may not perform that action. |
| 404 | invalid_request_error | No such record on this account. Another account’s record is a 404, never a 403 — the API does not confirm that it exists. |
| 422 | validation_error | The payload or a filter failed validation. `errors` names each field. |
| 429 | rate_limit_error | The key’s daily quota is spent. `Retry-After` says how many seconds until it resets. |
| 5xx | api_error | Something failed on our side. Retry with backoff, and quote the X-Request-Id if it persists. |
{
"message": "The request payload failed validation.",
"error": {
"type": "validation_error",
"status": 422
},
"errors": {
"email": ["Provide at least an email or a phone number."]
}
}Rate limits
Each key has a daily request quota — 10,000 for an account key, 50,000 for a master key — which resets at midnight UTC. Every response, successful or not, reports where you stand.
X-RateLimit-Limit— the key’s daily ceiling.X-RateLimit-Remaining— requests left today.X-RateLimit-Reset— Unix timestamp of the reset.X-Request-Id— this request’s id. Send your own and we will echo it back, so your logs and ours line up.
Once the quota is spent every request is a 429 carrying Retry-After in seconds. Watch X-RateLimit-Remaining and slow down before you get there rather than after.
HTTP/1.1 200 OK
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9871
X-RateLimit-Reset: 1788393600
X-Request-Id: 6f1c9b2e-8f4a-4f6e-9d17-2b0a5c8e77aaIdempotency
Creating a lead is idempotent on external_id — your own id for the record. Send it and a retried request returns the lead that already exists with a 200 instead of creating a second one.
This is what makes a webhook or a queued job safe to retry. Without an external_id there is nothing to match on, and a repeated POST /leads will create a duplicate.
Backfilling history
When importing old leads, send `occurred_at` with the original timestamp and `sync_conversions: false`. Otherwise a year of history is uploaded to Google Ads and Meta as conversions that all happened today, and their bidding models will believe it.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
-H "Content-Type: application/json" \
-d '{ "email": "jane@example.com", "external_id": "crm-8841" }' \
"https://getleadhound.io/api/public/v1/leads"
# 201 the first time, 200 and the same lead every time after.Account
Who these credentials are, which account they act on, and how much of today’s quota is left. Start here when wiring up a new integration.
Verify credentials
/api/public/v1/meReturns the account the credentials resolve to, the key doing the resolving, the remaining daily quota, and every website on the account.
This is the cheapest way to confirm a key works, and the only way to see which account a master key is currently pointed at. The secret half of the pair is never returned.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/me"{
"api_version": "v1",
"account": {
"id": 42,
"name": "Acme Plumbing",
"slug": "acme-plumbing",
"plan": "pro",
"organization_id": 7
},
"api_key": {
"name": "Zapier",
"token": "ltk_9f2c…",
"scope": "account"
},
"rate_limit": {
"limit": 10000,
"remaining": 9871,
"resets_at": "2026-09-03T00:00:00+00:00"
},
"profiles": [
{
"id": 11,
"name": "acme-plumbing.com",
"domain": "acme-plumbing.com",
"tracking_id": "a7Kd92Lm",
"is_active": true
}
]
}List accounts
/api/public/v1/accountsEvery account these credentials can act on, and which one the current request is bound to.
An account key returns exactly one account — its own. A master key returns every account in its organization, and those ids are what you pass in X-Account-Id to switch between them.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/accounts"{
"scope": "master",
"active_account_id": 42,
"accounts": [
{
"id": 42,
"name": "Acme Plumbing",
"slug": "acme-plumbing",
"plan": "pro",
"profiles_count": 2,
"active": true
},
{
"id": 43,
"name": "Zenith Roofing",
"slug": "zenith-roofing",
"plan": "starter",
"profiles_count": 1,
"active": false
}
]
}Websites
A website (a “profile”) is the unit almost everything else is filtered by. Its tracking id is the public identifier baked into the snippet.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/profiles"{
"total_profiles": 2,
"profiles": [
{
"id": 11,
"tenant_id": 42,
"name": "acme-plumbing.com",
"domain": "acme-plumbing.com",
"timezone": "America/Chicago",
"tracking_id": "a7Kd92Lm",
"settings": null,
"is_active": true,
"created_at": "2026-02-11T09:04:22+00:00",
"updated_at": "2026-08-30T18:15:03+00:00"
}
]
}curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/profiles/11"{
"id": 11,
"tenant_id": 42,
"name": "acme-plumbing.com",
"domain": "acme-plumbing.com",
"timezone": "America/Chicago",
"tracking_id": "a7Kd92Lm",
"settings": null,
"is_active": true,
"created_at": "2026-02-11T09:04:22+00:00",
"updated_at": "2026-08-30T18:15:03+00:00"
}Leads
Everything a visitor turned into: a form submission, a tracked call, or a lead your own systems pushed in. Reading, creating, correcting and deleting them.
List leads
/api/public/v1/leadsA paginated, filterable list of leads, newest first.
Query parameters
leads_per_pageintegerdefault 25Results per page, 1–2500.
page_numberintegerdefault 1Which page to return.
order_bystringdefault created_atWhich column to sort on.
created_atupdated_atvalueorderstringdefault descSort direction.
ascdescstart_datestringOnly include records created on or after this moment. ISO-8601 date or datetime, interpreted in UTC.
end_datestringOnly include records created on or before this moment. Must not precede start_date.
lead_typestringHow the lead arrived.
formphoneapilead_statusstringWhere the lead sits in your pipeline.
newqualifiedconvertedspamprofile_idintegerRestrict to a single website. Omit for every website on the account. An id belonging to another account is rejected, not silently ignored.
sourcestringExact match on the captured source, e.g.
google.campaignstringExact match on the captured campaign name.
external_idstringFind the lead you created under your own id. The fast way to reconcile a sync.
searchstringPartial match across name, email and phone.
includestringComma-separated relations to embed in each lead. Omitted by default because they cost a query each.
attributioncall
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/leads?leads_per_page=50&lead_status=converted"{
"page_number": 1,
"leads_per_page": 50,
"total_pages": 3,
"total_leads": 118,
"leads": [
{
"id": 1042,
"tenant_id": 42,
"profile_id": 11,
"type": "phone",
"status": "converted",
"is_repeat": false,
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "+15125550147",
"value": "1250.00",
"currency": "USD",
"source": "google",
"medium": "cpc",
"campaign": "emergency-plumbing",
"channel_label": "Google Ads",
"keyword": "emergency plumber austin",
"gclid": "Cj0KCQjw…",
"ad_group": "987654321",
"ad_id": "555000111",
"external_id": "crm-8841",
"lead_data": { "message": "Burst pipe under the sink" },
"metadata": { "session_id": "s_8f21c9" },
"converted_at": "2026-08-31T16:20:11+00:00",
"created_at": "2026-08-30T14:02:55+00:00",
"updated_at": "2026-08-31T16:20:11+00:00",
"profile": { "id": 11, "name": "acme-plumbing.com", "domain": "acme-plumbing.com" }
}
]
}Get a lead
/api/public/v1/leads/{id}One lead, always with its resolved attribution and — for a phone lead — the call it came from, including the transcript and any AI analysis.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/leads/1042"{
"id": 1042,
"type": "phone",
"status": "converted",
"name": "Jane Doe",
"value": "1250.00",
"channel_label": "Google Ads",
"attribution": {
"source": "google",
"medium": "cpc",
"campaign": "emergency-plumbing",
"keyword": "emergency plumber austin",
"adgroup": "Emergency — Exact",
"ad_id": "555000111"
},
"call_log": {
"id": 806,
"status": "completed",
"duration_seconds": 214,
"tracking_number": "+15125550100",
"caller_number": "+15125550147",
"recording_url": "https://recordings.leadhound.io/806.mp3",
"transcription": "Hi, I have water coming through the ceiling…",
"transcription_redacted": true,
"speaker_source": "diarized",
"analysis": { "summary": "Emergency call-out booked", "sentiment": "positive" }
}
}Create a lead
/api/public/v1/leadsPush a lead in from your own site, CRM or call centre. At least an email or a phone number is required.
Send external_id and the call becomes idempotent: a repeat of the same id returns the existing lead with 200 instead of creating a second one, so a retried webhook cannot duplicate a lead.
Send gclid, fbclid or a session_id from the snippet and the lead attributes exactly like one we captured ourselves — including the campaign, ad group and keyword we look up from the Google Ads API.
Importing history? Set occurred_at to the lead’s original timestamp and sync_conversions to false, or a year of old leads is restated to the ad platforms as conversions that happened today.
Body parameters
namestringThe person’s name.
emailstringrequiredEmail address. Required unless
phoneis given.phonestringrequiredPhone number. Required unless
emailis given.valuenumberWhat the lead is worth. Drives ROAS reporting.
currencystringdefault USDThree-letter currency code.
lead_typestringdefault apiHow it arrived.
formphoneapiprofile_idintegerWhich website the lead belongs to.
external_idstringYour own id for this lead. Makes the call idempotent.
sourcestringTraffic source, e.g.
google.mediumstringTraffic medium, e.g.
cpc.campaignstringCampaign name.
keywordstringSearch term.
ad_groupstringGoogle Ads ad group id.
ad_idstringGoogle Ads creative id.
gclidstringGoogle click id captured on the landing page.
fbclidstringMeta click id captured on the landing page.
session_idstringThe snippet’s session id, linking this lead to the visit that produced it.
fieldsobjectAny extra key/value data to keep with the lead.
occurred_atstringThe lead’s original timestamp, for backfills. Never in the future.
sync_conversionsbooleandefault trueSet false to skip the ad-platform upload and the new-lead alert.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
-X POST \
-H "Content-Type: application/json" \
-d '{ "name": "Jane Doe", "email": "jane@example.com", "phone": "+15125550147", "value": 1250, "external_id": "crm-8841", "gclid": "Cj0KCQjw...", "profile_id": 11 }' \
"https://getleadhound.io/api/public/v1/leads"{
"id": 1042,
"type": "api",
"status": "new",
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "+15125550147",
"value": "1250.00",
"currency": "USD",
"external_id": "crm-8841",
"gclid": "Cj0KCQjw…",
"created_at": "2026-09-02T11:41:07+00:00"
}Update a lead
/api/public/v1/leads/{id}Correct a lead or move it through your pipeline. Only the fields you send are changed.
Setting status to converted stamps the conversion time and restates the conversion at the ad platforms; changing value restates the amount. This is how a deal closed three weeks after the click gets credited to the campaign that produced it.
Body parameters
statusstringPipeline status.
newqualifiedconvertedspamvaluenumberRevised lead value.
currencystringThree-letter currency code.
namestringCorrected name.
emailstringCorrected email.
phonestringCorrected phone number.
sourcestringCorrected source.
mediumstringCorrected medium.
campaignstringCorrected campaign.
keywordstringCorrected keyword.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
-X POST \
-H "Content-Type: application/json" \
-d '{ "status": "converted", "value": 1250 }' \
"https://getleadhound.io/api/public/v1/leads/1042"{
"id": 1042,
"status": "converted",
"value": "1250.00",
"converted_at": "2026-09-02T11:44:19+00:00",
"updated_at": "2026-09-02T11:44:19+00:00"
}Delete a lead
/api/public/v1/leads/{id}Remove a lead permanently.
Deleting does not retract a conversion already uploaded to Google Ads or Meta. If you are removing junk, set the status to spam instead — that is what keeps the platforms’ bidding honest.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
-X DELETE \
"https://getleadhound.io/api/public/v1/leads/1042"{
"deleted": true,
"id": 1042
}Get a lead’s journey
/api/public/v1/leads/{id}/journeyThe visit behind the lead: first touch, the pages viewed, the device and location, and every conversion we uploaded for it.
The conversions array is the audit trail for the ad platforms — what we sent, when, and the error if a send failed. It is the first thing to check when a conversion has not appeared in Google Ads.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/leads/1042/journey"{
"session": {
"first_touch_at": "2026-08-29T19:44:02+00:00",
"landing_page": "https://acme-plumbing.com/emergency",
"referrer": "https://www.google.com/",
"last_landing_page": "https://acme-plumbing.com/contact",
"gclid": "Cj0KCQjw…"
},
"visitor": {
"ip": "203.0.113.24",
"geo": { "city": "Austin", "region": "TX", "country": "US" },
"browser": "Chrome",
"os": "iOS",
"device": "mobile",
"device_make": "Apple"
},
"page_views": [
{ "url": "https://acme-plumbing.com/emergency", "at": "2026-08-29T19:44:02Z" },
{ "url": "https://acme-plumbing.com/contact", "at": "2026-08-30T14:01:30Z" }
],
"conversions": [
{
"platform": "google_ads",
"event_type": "lead",
"kind": "click",
"status": "sent",
"value": "1250.00",
"currency": "USD",
"error": null,
"sent_at": "2026-08-31T16:20:44Z"
}
]
}Calls
Tracked calls in their own right. A missed call or a voicemail never becomes a lead — and those are usually the ones worth chasing.
List calls
/api/public/v1/callsA paginated, filterable list of calls, newest first.
Query parameters
calls_per_pageintegerdefault 25Results per page, 1–1000.
page_numberintegerdefault 1Which page to return.
orderstringdefault descSort direction on call time.
ascdescstart_datestringOnly include records created on or after this moment. ISO-8601 date or datetime, interpreted in UTC.
end_datestringOnly include records created on or before this moment. Must not precede start_date.
statusstringCall outcome.
ringingansweredmissedvoicemailcompletedprofile_idintegerRestrict to a single website. Omit for every website on the account. An id belonging to another account is rejected, not silently ignored.
tracking_numberstringThe number the call came in on, in E.164.
caller_numberstringThe number that called, in E.164.
min_durationintegerSeconds. Filters out hang-ups and wrong numbers.
has_recordingbooleanOnly calls that have a recording.
answered_onlybooleanOnly calls a person picked up. A voicemail is a missed call with a message, so it is excluded.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/calls?answered_only=1&min_duration=60"{
"page_number": 1,
"calls_per_page": 25,
"total_pages": 2,
"total_calls": 34,
"calls": [
{
"id": 806,
"profile_id": 11,
"lead_id": 1042,
"status": "completed",
"tracking_number": "+15125550100",
"caller_number": "+15125550147",
"destination_number": "•••••••1400",
"duration_seconds": 214,
"started_at": "2026-08-30T14:02:11+00:00",
"recording_url": "https://recordings.leadhound.io/806.mp3",
"transcription": "Hi, I have water coming through the ceiling…",
"transcription_redacted": true,
"speaker_source": "diarized",
"speakers_observed": true,
"analysis": { "summary": "Emergency call-out booked", "sentiment": "positive" },
"created_at": "2026-08-30T14:02:11+00:00",
"lead": { "id": 1042, "name": "Jane Doe", "status": "converted", "value": "1250.00" }
}
]
}Get a call
/api/public/v1/calls/{id}One call, with its recording, transcript, speaker turns and AI analysis where the number pool has them switched on.
speaker_source says how much to trust the speaker labels: diarized and corrected are evidence, labelled and inferred are a best guess. speakers_observed is the same distinction as a boolean.
When PCI redaction is on for the pool, transcription is the redacted text and transcription_redacted is true — a card number read aloud never leaves the platform.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/calls/806"{
"id": 806,
"profile_id": 11,
"lead_id": 1042,
"status": "completed",
"tracking_number": "+15125550100",
"caller_number": "+15125550147",
"destination_number": "•••••••1400",
"duration_seconds": 214,
"started_at": "2026-08-30T14:02:11+00:00",
"recording_url": "https://recordings.leadhound.io/806.mp3",
"transcription": "Hi, I have water coming through the ceiling…",
"transcription_redacted": true,
"transcript_segments": [
{ "speaker": "caller", "text": "Hi, I have water coming through the ceiling.", "start": 1.2 },
{ "speaker": "agent", "text": "I can get someone to you within the hour.", "start": 6.8 }
],
"speaker_source": "diarized",
"speakers_observed": true,
"analysis": {
"summary": "Emergency call-out booked",
"sentiment": "positive",
"outcome": "booked"
},
"created_at": "2026-08-30T14:02:11+00:00"
}Tracking numbers
The numbers the snippet swaps onto your pages, and the pool each one serves. Use these to resolve a call’s `tracking_number` to a website and a traffic source.
List tracking numbers
/api/public/v1/phone-numbersEvery tracking number on the account.
Forwarding destinations are masked to their last four digits. That is the customer’s real line, not tracking data, so the API never returns it in full.
Query parameters
profile_idintegerRestrict to a single website. Omit for every website on the account. An id belonging to another account is rejected, not silently ignored.
active_onlybooleanExclude numbers that have been retired.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/phone-numbers"{
"total_phone_numbers": 1,
"phone_numbers": [
{
"id": 60,
"name": "Emergency landing page",
"tracking_number": "+15125550100",
"formatted": "+1 (512) 555-0100",
"destination_number": "•••••••1400",
"is_active": true,
"provider": "plivo",
"number_pool_id": 4,
"created_at": "2026-03-02T10:00:00+00:00",
"profile": { "id": 11, "name": "acme-plumbing.com" },
"pool": { "id": 4, "name": "Paid search", "traffic_source": "search" }
}
]
}Reports
Pre-aggregated numbers, so you are not paging the whole leads collection to do the sums yourself.
Lead summary
/api/public/v1/reports/leadsHeadline counts for a date window, plus one breakdown — by day, source, campaign, keyword, type, status or website.
Query parameters
start_datestringdefault 29 days agoFirst day of the window,
YYYY-MM-DD.end_datestringdefault todayLast day of the window,
YYYY-MM-DD.group_bystringdefault dayThe dimension the breakdown is cut by.
daysourcemediumcampaignkeywordtypestatusprofileprofile_idintegerRestrict to a single website. Omit for every website on the account. An id belonging to another account is rejected, not silently ignored.
lead_typestringRestrict the whole report to one kind of lead.
formphoneapi
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/reports/leads?group_by=source&start_date=2026-08-01&end_date=2026-08-31"{
"start_date": "2026-08-01",
"end_date": "2026-08-31",
"group_by": "source",
"totals": {
"total": 118,
"by_type": { "form": 61, "phone": 49, "api": 8 },
"by_status": { "new": 44, "qualified": 39, "converted": 28, "spam": 7 },
"conversion_rate": 23.7,
"total_value": 41250
},
"breakdown": [
{ "key": "google", "leads": 74, "converted": 21, "value": 32100, "conversion_rate": 28.4 },
{ "key": "facebook", "leads": 29, "converted": 5, "value": 6400, "conversion_rate": 17.2 },
{ "key": "direct", "leads": 15, "converted": 2, "value": 2750, "conversion_rate": 13.3 }
]
}Cost per lead and ROAS
/api/public/v1/reports/roiImported ad spend joined to captured leads, per source and per campaign.
Only platforms whose spend has been imported appear here — connect Google Ads or Meta first, or the cost columns read zero.
Free traffic is excluded from a platform’s figures: a site’s organic Google visits share the google source with its Ads clicks, and counting them would flatter cost-per-lead and inflate ROAS.
Query parameters
start_datestringdefault 29 days agoFirst day of the window,
YYYY-MM-DD.end_datestringdefault todayLast day of the window,
YYYY-MM-DD.profile_idintegerRestrict to a single website. Omit for every website on the account. An id belonging to another account is rejected, not silently ignored.
curl -u "$LEADHOUND_TOKEN:$LEADHOUND_SECRET" \
"https://getleadhound.io/api/public/v1/reports/roi?start_date=2026-08-01&end_date=2026-08-31"{
"from": "2026-08-01",
"to": "2026-08-31",
"totals": {
"cost": 4820,
"leads": 103,
"revenue": 38500,
"cost_per_lead": 46.8,
"roas": 7.99
},
"by_source": [
{
"source": "google",
"clicks": 1904,
"cost": 3600,
"leads": 74,
"revenue": 32100,
"cost_per_lead": 48.65,
"roas": 8.92
}
],
"campaigns": [
{
"source": "google",
"campaign": "emergency-plumbing",
"clicks": 902,
"cost": 2100,
"leads": 48,
"revenue": 24800,
"cost_per_lead": 43.75,
"roas": 11.81
}
]
}