Creator email
Find the business email a creator publishes, with include=contacts on a profile call. What it searches, what comes back, how to retry, and what it costs
Add include=contacts to a profile call and the response carries the email address the creator publishes for business, with the place it was found. Nothing is guessed or generated: every address was read from a page the creator or their management put online.
curl -G "https://www.socialcrawl.dev/v1/youtube/channel/about" \
--data-urlencode "handle=creator" \
--data-urlencode "include=contacts" \
-H "x-api-key: YOUR_API_KEY"{
"success": true,
"data": {
"author": {
"ext": {
"contacts": [
{
"kind": "email",
"value": "business@example.com",
"role": "management",
"confidence": 0.92,
"provenance": "published",
"sources": [
{
"tier": "about",
"url": "https://www.youtube.com/@creator/about",
"snippet": "For business enquiries: business@example.com"
}
]
}
]
}
},
"contact_lookup": {
"status": "found",
"checked_at": "2026-11-01T10:00:00.000Z",
"recheck_after": "2027-04-30T10:00:00.000Z",
"tiers_run": ["store", "about_page"],
"pending": null
}
},
"credits_used": 1
}Which endpoints take it
| Endpoint | Where the search starts |
|---|---|
youtube/channel/about | The channel's About page, then video descriptions |
instagram/profile and instagram/profile/about | The profile's own text and links |
tiktok/profile | The profile's own text and links |
twitter/profile | The profile's own text and links |
facebook/profile | The page's own text and links |
linkbio/page and pillar/page | The page's own email field |
Send include=contacts on any of them. max_duration, min_confidence and roles work the same way on all of them except linkbio/page and pillar/page, which read one page and take only include.
What it searches
The search goes from the cheapest place to the most expensive and stops as soon as it has an address for the creator:
- Our store. An address we already found for this creator, from an earlier call by anyone. Nothing is fetched.
- The About page or bio. The creator's own text on the platform.
- Video descriptions (YouTube only).
- Linked profiles and link-in-bio pages. Only profiles and pages the creator links. We never guess a handle on another platform.
- The creator's own website. Read in the background by SocialCrawlContactBot, which honors robots.txt.
- The View email address button (YouTube only). Some channels hide the address behind this button. Reading it is the one paid step, and it runs only when every place above found nothing.
data.contact_lookup.tiers_run lists the places that ran, in order.
Read the response
Each entry of author.ext.contacts[] is one address:
| Field | Meaning |
|---|---|
value | The email address. |
role | own for the creator, management for a manager, agency for an agency. |
confidence | 0 to 1. Higher means more confidence that the address belongs to this creator in that role. |
provenance | published when the creator put it on a page, gated_reveal when it came from the YouTube button. |
sources | Where it was read. Each source has a tier (such as bio, about, video_description, link_in_bio, website or button), the url and a snippet of the text around the address. |
data.contact_lookup says how the search ended:
status | Final? | Meaning |
|---|---|---|
found | Yes | At least one address is in author.ext.contacts. |
none_published | Yes | Every place ran and the creator publishes no address. recheck_after says when it is worth asking again. |
partial | No | The search ran out of time with places still being read. What was found so far is in the response. |
gated_pending | No | The address is behind the YouTube button and the read has not delivered yet. |
unavailable | No | A source failed on this call and nothing was found elsewhere. This does not mean the creator has no address. |
On partial and gated_pending, data.contact_lookup.pending holds a reason, a retry_after_seconds and a doc_url that points at the entry on the errors page. Treat partial, gated_pending and unavailable as "not yet", never as "no email".
Retry an unsettled answer
The search carries on after a partial or gated_pending response and writes what it finds to our store. Wait retry_after_seconds, then send the same call again. The later call is served from the store.
# status is "partial", pending.retry_after_seconds is 30
sleep 30
curl -G "https://www.socialcrawl.dev/v1/youtube/channel/about" \
--data-urlencode "handle=creator" \
--data-urlencode "include=contacts" \
-H "x-api-key: YOUR_API_KEY"const KEY = process.env.SOCIALCRAWL_API_KEY!;
const sleep = (seconds: number) => new Promise((r) => setTimeout(r, seconds * 1000));
async function findEmail(handle: string, maxAttempts = 4) {
const url = new URL("https://www.socialcrawl.dev/v1/youtube/channel/about");
url.searchParams.set("handle", handle);
url.searchParams.set("include", "contacts");
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = await fetch(url, { headers: { "x-api-key": KEY } });
const body = await res.json();
const lookup = body.data.contact_lookup;
// found and none_published are final.
if (lookup.status === "found" || lookup.status === "none_published") {
return { status: lookup.status, contacts: body.data.author?.ext?.contacts ?? [] };
}
// partial, gated_pending and unavailable are not. Wait as told, then ask again.
await sleep(lookup.pending?.retry_after_seconds ?? 30);
}
return { status: "unsettled", contacts: [] };
}import os, time, requests
def find_email(handle, max_attempts=4):
for _ in range(max_attempts):
res = requests.get(
"https://www.socialcrawl.dev/v1/youtube/channel/about",
params={"handle": handle, "include": "contacts"},
headers={"x-api-key": os.environ["SOCIALCRAWL_API_KEY"]},
)
data = res.json()["data"]
lookup = data["contact_lookup"]
if lookup["status"] in ("found", "none_published"):
return lookup["status"], (data.get("author", {}).get("ext", {}).get("contacts") or [])
time.sleep((lookup.get("pending") or {}).get("retry_after_seconds", 30))
return "unsettled", []Cap the attempts. With include=contacts a call answers within about 20 seconds (max_duration sets the budget, 3 to 20 seconds). A search that needs longer, such as a YouTube button reveal, carries on after the answer and saves what it finds, so the next call after retry_after_seconds is answered from that. Waiting longer in one call does not help: async=true runs the same call as a job and returns the same answer.
What it costs
youtube/channel/about charges its usual price only when the call returns an address. A call that ends in none_published, partial, gated_pending or unavailable on that endpoint is refunded.
On every other endpoint that takes include=contacts, the option costs nothing extra and the call costs its usual price whatever the status. An unsettled answer is not cached, so a retry is charged like any other call. On those endpoints, cap the attempts in your loop and wait the full retry_after_seconds between them.
Endpoint pricing lists the price of each endpoint.
Filter what comes back
| Parameter | Default | Effect |
|---|---|---|
roles | own,management | Comma list of own, management and agency. Add agency to include agency addresses. |
min_confidence | 0.6 | 0 to 1. Addresses below it are left out. |
max_duration | 12 | Seconds the first call may spend searching before it answers, from 3 to 20. A longer value makes partial less likely. |
curl -G "https://www.socialcrawl.dev/v1/instagram/profile" \
--data-urlencode "handle=creator" \
--data-urlencode "include=contacts" \
--data-urlencode "roles=own,management,agency" \
--data-urlencode "min_confidence=0.8" \
-H "x-api-key: YOUR_API_KEY"Report a wrong or bounced address
When an address bounces or belongs to someone else, tell us with POST /v1/feedback/contact, which is free. A bounce marks the stored address suspect, and a bounce from a second account removes it. The request body, the outcomes and the error cases are on the errors page.
curl -X POST "https://www.socialcrawl.dev/v1/feedback/contact" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "request_id": "req_V1StGXR8Z5jdHi6B", "email": "business@example.com", "outcome": "bounced" }'Remove an address or block the crawler
A creator or site owner who wants an address removed, or the crawler kept off their site, can use the steps on the contact crawler page: add a Disallow rule for SocialCrawlContactBot to robots.txt, or write to socialcrawl@ridio.io with the domain or the address concerned.
Next steps
Which Endpoint Should I Use?
Decision tables for the endpoint pairs developers most often confuse, covering profile vs profile/full, Instagram's 1cr vs 5cr endpoints, list vs composite, search, transcripts, comments, batch, jobs, ad libraries, app stores, commerce reviews, and the Prism composites.
Skills & MCP Server
Connect any MCP client or agent skill to the SocialCrawl API, hosted or local
