# Creator email (/docs/find-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. ```bash title="cURL" 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" ``` ```json title="Response (trimmed)" { "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: 1. **Our store.** An address we already found for this creator, from an earlier call by anyone. Nothing is fetched. 2. **The About page or bio.** The creator's own text on the platform. 3. **Video descriptions** (YouTube only). 4. **Linked profiles and link-in-bio pages.** Only profiles and pages the creator links. We never guess a handle on another platform. 5. **The creator's own website.** Read in the background by [SocialCrawlContactBot](/docs/contact-crawler.md), which honors robots.txt. 6. **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](/docs/errors.md#contact-lookup-statuses). 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. ```bash title="cURL" # 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" ``` ```typescript title="TypeScript" 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: [] }; } ``` ```python title="Python" 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](/docs/endpoint-pricing.md) 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. | ```bash title="cURL" 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](/docs/errors.md#report-a-bounced-contact). ```bash title="cURL" 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](/docs/contact-crawler.md): add a `Disallow` rule for `SocialCrawlContactBot` to robots.txt, or write to socialcrawl@ridio.io with the domain or the address concerned. ## Next steps - [Contact lookup statuses](/docs/errors.md#contact-lookup-statuses): Every status and pending reason, and what each costs. - [Contact crawler](/docs/contact-crawler.md): What SocialCrawlContactBot reads and how to opt out. - [Endpoint pricing](/docs/endpoint-pricing.md): The price of each endpoint.