Enrichment Tools

Identifier in, attributes out: professional email, cross-platform handles, coordinates

Enrichment tools all share a shape: you pass an identifier you already have, and you get back an attribute you don’t. A name plus a domain becomes a verified email address. A name becomes a set of social handles. A free-text location becomes coordinates.

They are grouped together because they behave alike, not because they share a data source. Each answers in one call, none of them paginate, and each returns a status field that tells you whether the answer is trustworthy rather than handing back a guess. When enrichment can’t confirm something, it says so — that contract is the reason these tools are safe to run across a list unattended.

Tools

ToolCreditsDescription
get_email5Get a person’s professional email (SMTP-verified)
get_social_profiles5Get a person’s handles across LinkedIn, X, Instagram and TikTok
get_coordinates1Get coordinates (lat/lng) for a free-text location

get_email requires the GTM Tools browser extension to be connected. It confirms Google Workspace mailboxes (the largest provider) through a logged-in Google session shared via the extension. If no session is connected, the tool returns setup instructions instead of running. Install it and press Connect once per workspace. get_social_profiles and get_coordinates need no session.

get_email

ParameterTypeRequiredDescription
namestringYesFull name of the person (e.g. "Justin Mares")
domainstringYesCompany domain (e.g. "kettleandfire.com")
input_parametersobjectNoFree-form metadata echoed back in the response (useful for batch processing)
$curl -X POST https://api.gtm-tools.sh/api/v0/get_email \
> -H "Authorization: Bearer $GTM_TOOLS_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "name": "Justin Mares",
> "domain": "kettleandfire.com",
> "input_parameters": {"source": "linkedin"}
> }'
1{
2 "email": "justin@kettleandfire.com",
3 "status": "ok",
4 "domain": "kettleandfire.com",
5 "input_parameters": {"source": "linkedin"}
6}

Response fields

FieldTypeDescription
emailstring | nullThe SMTP-verified deliverable address, or null on any non-ok status
statusstringok, not_found, or try_again_later, the whole contract in one field
reasonstringPresent on non-ok results: catch_all, no_mx, unverifiable_provider, no_pattern_verified, smtp_blocked, smtp_unavailable, google_cooldown
retry_after_secondsnumberPresent on try_again_later only
domainstringEchoes back the input domain
input_parametersobjectEchoes back the input metadata (for batch correlation)

When the domain is catch-all

Catch-all domains accept SMTP traffic for every local-part, so verification can’t disambiguate the true address. Rather than hand back a guess that might silently bounce, get_email stays strict:

1{ "email": null, "status": "not_found", "reason": "catch_all", "domain": "acme.com" }

If you want to act anyway, generate the dominant pattern yourself and treat it as a hypothesis: cross-check against LinkedIn, and prefer a channel where being wrong is cheap. Every reason value and what to do about it is on Why is get_email returning not_found?, and the batching and retry patterns are in Outbound.

How verification works

get_email does three things:

  1. Generate candidates. Every common pattern is produced from the name (justin@, justinmares@, justin.mares@, j.mares@, jmares@, etc.).
  2. Verify via SMTP. Each candidate is probed against the domain’s MX records: the verifier speaks SMTP, issues RCPT TO, and observes the server’s response, all without DATA, so no mail is ever sent.
  3. Return the first deliverable hit. Catch-all domains are detected by probing a known-bogus address first; if the server accepts it, the result comes back as not_found with reason: "catch_all" rather than a best guess.

This adds 2–5 seconds of latency per call.

get_social_profiles

get_social_profiles answers a different question: given a person’s name, which accounts across LinkedIn, X, Instagram and TikTok belong to them?

ParameterTypeRequiredDescription
namestringYesThe person’s full name
companystringNoCompany name, to disambiguate a common name
domainstringNoCompany domain, to disambiguate a common name
locationstringNoCity or country, to disambiguate a common name
keywordsstringNoOther distinguishing terms, such as a role or niche
$curl -X POST https://api.gtm-tools.sh/api/v0/get_social_profiles \
> -H "Authorization: Bearer $GTM_TOOLS_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{"name": "Andrei Negrau", "domain": "siena.cx"}'

It reads search-engine snippets only. There is no platform login and no profile fetch, which is why it works on Instagram and TikTok at all: their profile pages are hostile to anonymous requests, but their snippets are indexed.

Always pass context when you have it. A bare common name returns whoever ranks highest in search, which may be a different person entirely. Any company, domain, location or keywords you supply must be corroborated in a candidate’s own snippet before that candidate is accepted, so context does not just re-rank the results, it filters them.

The strongest signal is a brand appearing on two different platforms. That is what resolves a handle bearing no resemblance to the person’s name, such as a personal brand or a nickname account.

Response fields

FieldTypeDescription
statusstringok or error
data.namestringEchoes back the name searched
data.confidencenumber0 to 1, reflecting how many platforms were resolved — not a per-handle probability
data.linkedin_profile_urlstringPresent only when LinkedIn resolved confidently
data.x_profile_urlstringPresent only when X resolved confidently
data.instagram_profile_urlstringPresent only when Instagram resolved confidently
data.tiktok_profile_urlstringPresent only when TikTok resolved confidently
data.matchesarrayPer-platform detail: the platform, handle, url and the score it won on

Platforms with no confident match are omitted rather than guessed, so treat a missing key as “not found”, not as an error.

Reach for get_linkedin_profile_url instead when you only need LinkedIn and you already know the company domain: it is cached, so a repeat lookup is instant.

get_coordinates

get_coordinates forward-geocodes any free-text location — a street address, city, region, landmark or country — to a latitude/longitude using global OpenStreetMap data. At 1 credit it is the cheapest tool in the catalog.

ParameterTypeRequiredDescription
locationstringYesFree-text location (e.g. "1600 Amphitheatre Parkway, Mountain View, CA")
input_parametersobjectNoFree-form metadata echoed back in the response (useful for batch processing)
$curl -X POST https://api.gtm-tools.sh/api/v0/get_coordinates \
> -H "Authorization: Bearer $GTM_TOOLS_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{"location": "1600 Amphitheatre Parkway, Mountain View, CA"}'
1{
2 "status": "ok",
3 "location": "1600 Amphitheatre Parkway, Mountain View, CA",
4 "lat": 37.4224858,
5 "lng": -122.0855846,
6 "formatted": "Google Building 41, 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States of America",
7 "country": "United States",
8 "city": "Mountain View",
9 "result_type": "amenity",
10 "confidence": 1
11}

Response fields

FieldTypeDescription
statusstringok, not_found, or try_again_later
locationstringEchoes back the input
lat / lngnumber | nullCoordinates of the best match, null on any non-ok status
formattedstring | nullThe full address that was actually matched — check this before trusting lat/lng
countrystring | nullCountry of the match
citystring | nullCity of the match
result_typestring | nullWhat kind of thing matched: amenity, city, country, and so on
confidencenumber | nullMatch confidence from 0 to 1
input_parametersobjectEchoes back the input metadata (for batch correlation)

formatted and result_type together are how you catch a wrong answer. A location of "Springfield" resolves, but result_type: "city" with a formatted naming the wrong state tells you the input was ambiguous rather than the geocoder wrong. On try_again_later the upstream geocoder was rate-limited; retry the same input.

Next steps