WebmeticDocs
Get startedDetectEnrichPersonalizeIntegrateManagePartnersDevelopers
Guides

Find contacts

Search the API for people at a company that visited your website, and unlock their email.

A script takes a company from your recent visits and finds the right person there. At the end you'll have:

  • a script that lists the people at a visiting company for free and picks one of them
  • the unlocked email address of that person and their LinkedIn profile, if there is one, stored with company_id and contact_id
  • handling for too few credits, and the certainty that a second run doesn't charge the same data again

Before you start

  • Time: Setting up takes about 15 minutes, and one unlock takes a few seconds.
  • Account: Contacts are set up once in your account in the dashboard, as described in Set up contacts. Otherwise search and unlock answer with 403 and CONTACTS_SETUP_REQUIRED.
  • Access: You need the API key for Visitor Intelligence, the same one you use for visits, and Python 3 with requests. The Quickstart shows how to copy the key.
  • Rights: Only admins see the key under Menu > Integrations > API. Any user can set up contacts.
  • Plan: The API is part of every plan, including the Free plan and the trial. On the Free plan, this applies only to companies you can see.
  • Credits: You search for free, but unlocking costs credits and you can't undo it. You buy credits in the dashboard, not through the API. For details, see Credits.

If a partner manages your account, the partner has to turn on Data profiles. Otherwise the API answers with 403 and FEATURE_DISABLED.

Choose a company

You can only search for contacts at companies that visited one of your websites. You get the company_id from GET /intensive-visits and GET /company-sessions, for example. /intensive-visits only contains visits of 30 seconds or more.

If you use webhooks, you get the company_id in data.company.company_id.

If you send any other company_id, the API answers with 403 and COMPANY_NOT_IDENTIFIED. You can't search by domain, name, or person.

On the Free plan, skip rows with "blurred": true. For these hidden companies the search answers with 403 and PLAN_REQUIRED. For details, see Trial and Free plan.

Visit endpoints such as /intensive-visits answer one request per second per endpoint. Rate limits has the details.

Check your credits

GET /contacts/credits shows for free how many credits your account has. balance is the number of your credits, and costs.email is the cost of unlocking one email.

Read the costs from costs instead of hard-coding them. The example and the fields are on Credits in the API.

Don't stop hard when your credits are low, because data your account has already unlocked comes back at no cost.

Search for contacts

POST /contacts/search lists the people at a company for free. This example searches marketing from division lead up, that is levels 6 to 10:

curl -X POST "https://hub.webmetic.de/contacts/search" \
  -H "Authorization: wmtc_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"company_id": "41ea486a9c95", "filters": {"departments": ["Marketing"], "seniority": [10, 9, 8, 7, 6]}}'

filters takes these fields:

  • departments: departments as a list of strings. Webmetic combines several departments with "or".
  • seniority: levels as numbers, 10 founder, 9 C-level, 8 VP, 7 partner, 6 director, 5 manager, 4 senior, 3 entry level. Department and level must both match.
  • require_email and require_phone: only people with an email address or phone number on file.

In departments, use only the values the dashboard sends:

Department in the dashboardValue in departments
ManagementGeneral Management
MarketingMarketing
SalesSales
Business developmentBusiness Development
HRHuman Resources
Tech & productEngineering & Technical, Information Technology, Product
Finance & legalFinance, Legal

The Minimum level in the dashboard matches these levels: manager and above 4 to 10, division lead and above 6 to 10, executives only 9 and 10.

If you leave a filter out or empty, Webmetic uses the values from your Contact profile.

size sets how many people a page holds at most: 10 to 25, 25 by default. You count pages from 0 with page, and total gives you all matching people.

Choose a contact

Pick the person in your code, for example by job_title, and use has_email and has_phone to see which data is on file.

You don't get department, level, or location in the response. In the search, email, linkedin, direct_phone, and mobile_phone are always null, even for people your account has already unlocked.

Keep the contact_id from the search and treat it as text without a fixed format.

Unlock the email

POST /contacts/reveal unlocks a person's data and costs credits. With reveal_email you get the email address and the LinkedIn profile, and with reveal_phone the phone numbers in direct_phone and mobile_phone.

curl -X POST "https://hub.webmetic.de/contacts/reveal" \
  -H "Authorization: wmtc_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact_id": "v1.q7Rk2mXv9TnL4w", "reveal_email": true}'

The response contains everything your account has unlocked for this person. Anything you haven't unlocked stays null. The example leaves out credits_used and credits_remaining, which the real response also contains.

If Webmetic finds no address, email stays null and Webmetic charges nothing for it. Because a new attempt needs enough credits again, don't repeat it in a loop.

For some people, Webmetic isn't allowed to hand out contact details. Then the API answers with 404 and CONTACT_NOT_FOUND and charges nothing.

You get the same answer when the contact_id doesn't come from a search of your account. Search again and use the new contact_id.

Handle insufficient credits

If your credits aren't enough, the API answers with 402 and INSUFFICIENT_CREDITS. Webmetic checks this before it fetches anything, so you aren't charged.

You can therefore get a 402 even when Webmetic wouldn't have found any data.

detail.required gives the credits the action needs, and detail.available gives the credits your account has. Buy more credits in the dashboard, then repeat the call.

On 409 and CONCURRENT_UPDATE, repeat the call after a moment, and on 502 and PROVIDER_UNAVAILABLE after a few minutes. All codes are on Errors.

Avoid paying twice

Webmetic charges your account for the same data only once:

  • Unlock again: A second call for the same person and data type costs nothing and returns from_cache: true. This holds for every user and key of your account.
  • New data type: If you ask for the phone after the email, you only pay for the phone.
  • Simultaneous calls: If you send the same request twice at once, Webmetic charges it once.
  • Email and phone together: If you ask for both in one call, Webmetic charges the delivered data in one step or not at all.

If you don't want to send a second call, store the contact_id and skip known people on the next run.

Store the result

Store company_id, contact_id, name, email address, and LinkedIn profile. You never see unlocked data in the search results.

You can read the data again for free with GET /contacts/lookup and the same filters, or with a repeat unlock.

Unlocked data belongs to your whole account, so every user, every key, and your AI assistant can use it.

The person shows up with their data in the company's dashboard profile only if your search used the filters of the Contact profile. For that, it's best to leave out filters.

Complete script

The script connects all the steps, and you only replace the key and the website with your own values.

The script picks the company with the highest relevance_score among the visits of 30 seconds or more in the last seven days. Then it searches marketing from division lead up for people with an email address and appends the result to contacts.jsonl.

import json
import sys

import requests

API = "https://hub.webmetic.de"
HEADERS = {"Authorization": "wmtc_YOUR_API_KEY"}
WEBSITE = "your-website.com"


def fail(res):
    try:
        detail = res.json().get("detail")
    except ValueError:
        detail = res.text[:200]
    # Paths outside the API answer {"detail": "Not Found"}
    if not isinstance(detail, dict):
        detail = {"error": f"HTTP_{res.status_code}", "message": str(detail)}
    sys.exit(f"{res.status_code} {detail['error']}: {detail['message']}")


# 1. Companies with a visit of 30 seconds or more in the last 7 days
res = requests.get(
    f"{API}/intensive-visits",
    headers=HEADERS,
    params={"domain": WEBSITE, "from_date": "-7 days"},
)
if not res.ok:
    fail(res)
companies = [c for c in res.json()["result"] if not c.get("blurred")]
if not companies:
    sys.exit("No company with an intensive visit")
company = max(companies, key=lambda c: c.get("relevance_score") or 0)

# 2. Check credits (free), warn only: a repeat unlock costs nothing
credits = requests.get(f"{API}/contacts/credits", headers=HEADERS).json()
if credits["balance"] < credits["costs"]["email"]:
    print("Not enough credits, the API may answer 402")

# 3. Search contacts (free): marketing, division lead and above, with email
res = requests.post(
    f"{API}/contacts/search",
    headers=HEADERS,
    json={
        "company_id": company["company_id"],
        "filters": {
            "departments": ["Marketing"],
            "seniority": [10, 9, 8, 7, 6],
            "require_email": True,
        },
    },
)
if not res.ok:
    fail(res)
people = [p for p in res.json()["contacts"] if p["has_email"]]
if not people:
    sys.exit(f"No contact found at {company['company_name']}")
person = people[0]

# 4. Unlock the email (costs credits, a repeat costs nothing)
res = requests.post(
    f"{API}/contacts/reveal",
    headers=HEADERS,
    json={"contact_id": person["contact_id"], "reveal_email": True},
)
if res.status_code == 402:
    detail = res.json()["detail"]
    sys.exit(f"Not enough credits: {detail['required']} needed, {detail['available']} available")
if not res.ok:
    fail(res)
unlocked = res.json()

# 5. Store the result
record = {
    "company_id": company["company_id"],
    "company_name": company["company_name"],
    "contact_id": person["contact_id"],
    "name": person["name"],
    "job_title": person["job_title"],
    "email": unlocked["contact"]["email"],
    "linkedin": unlocked["contact"]["linkedin"],
}
with open("contacts.jsonl", "a", encoding="utf-8") as f:
    f.write(json.dumps(record, ensure_ascii=False) + "\n")
print(record)

Because the script sets require_email, the person doesn't show up in the list in the dashboard. If the script runs again with the same visits, it picks the same person, and the unlock costs nothing.

Search and unlock in one call

POST /contacts/find searches and unlocks in one call and costs credits. reveal picks email, phone, or both. Without it, email applies.

curl -X POST "https://hub.webmetic.de/contacts/find" \
  -H "Authorization: wmtc_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"company_id": "41ea486a9c95", "filters": {"departments": ["Marketing"]}, "reveal": "email"}'

From the first 25 results, Webmetic takes the first person with all requested data on file. If there's no such person, Webmetic takes the first person with part of it.

You get a flat response with found and the person's fields.

If nobody matches, you get found: false and Webmetic charges nothing. If you send the same call again, it picks the same person and costs nothing.

You can't pick a second or third person with find, so for that you use search and unlock as above.

Next steps

On this page