---
name: website-contacts
description: "Get email addresses, phone numbers and Facebook, Instagram, LinkedIn, X, YouTube, TikTok and Pinterest profiles from a list of company websites: homepage plus contact and imprint pages. No login, no browser. Use when the user has company websites or domains and wants their public contact details: email addresses, phone numbers and Facebook, Instagram, LinkedIn, X, YouTube, TikTok or Pinterest profiles, for example to enrich a lead list or CRM export that only has domains. Calls the Pullwire API with the key in PULLWIRE_API_KEY; costs 1 credit per website with contacts."
metadata:
  openclaw:
    requires:
      env:
        - PULLWIRE_API_KEY
      bins:
        - curl
    primaryEnv: PULLWIRE_API_KEY
    homepage: https://pullwire.dev/tools/website-contacts
---

# Website Contact Scraper

Get email addresses, phone numbers and Facebook, Instagram, LinkedIn, X, YouTube, TikTok and Pinterest profiles from a list of company websites: homepage plus contact and imprint pages. No login, no browser. The data comes from the Pullwire API: you send the input as JSON and get structured JSON back. Only public data.

## Setup (once)

The API key must be in the environment variable `PULLWIRE_API_KEY`. Keys start with `af_live_`. Check it without printing it:

```bash
test -n "$PULLWIRE_API_KEY" && echo "key is set" || echo "PULLWIRE_API_KEY is missing"
```

If it is missing, stop and tell the user: sign up at https://pullwire.dev/login (new accounts get 2,000 free credits), create a key at https://pullwire.dev/app/keys, then run `export PULLWIRE_API_KEY="af_live_..."` in the shell the agent uses. Never print the key, write it into a file or repeat it in the chat.

## Run it

```bash
curl -sS -X POST "https://pullwire.dev/api/v1/tools/website-contacts/runs" \
  -H "x-api-key: $PULLWIRE_API_KEY" \
  -H "content-type: application/json" \
  -d '{"websites":["kaercher.com","myrescueplumbing.com"],"maxResults":3}'
```

The request waits up to 25 seconds and answers with `{"run": {...}, "results": [...]}`. Check `run.status` (`succeeded` or `failed`) and `run.error`: when it is set, the run stopped early and `results` is partial (everything in it was charged). `run.creditsUsed` is what the run cost. For big answers add `-o result.json` and read only the fields you need.

Good to know:

- Put every website into one run (`websites`, up to 5,000; plain domains like `stripe.com` work) instead of one run per website. Duplicates written differently (`www.`, `https://`, trailing slash) are scraped once.
- Each website reads the homepage plus contact or imprint pages while an email or phone is still missing, up to `maxPagesPerWebsite` (default 3).
- `status: "none-found"` means the homepage and its contact pages were read but their HTML held no email, phone or social link: the company may only offer a contact form, or the site builds its text with JavaScript. It costs nothing. Say that no contact data was found on the site instead of guessing an address.
- A website that blocks automated visitors (also a bot-check page served instead of the site), is down, or is a social media profile instead of a company site returns no result and costs nothing.

## Input

The JSON body is the tool input. Fields:

| Field | Type | Required | Default | What it does |
| --- | --- | --- | --- | --- |
| `websites` | string[] | yes |  | Websites: Company domains or website URLs, one per line, e.g. "kaercher.com" or "www.example-bakery.de/kontakt". One result per website; duplicates are skipped. |
| `maxPagesPerWebsite` | integer | no | `3` | Pages per website: Homepage plus up to this many minus one contact, imprint or about pages. Extra pages are only visited while an email or a phone number is still missing. |
| `maxResults` | integer | no |  | Max results: Stop after this many websites. Empty: all of them. |

Without `maxResults` a run returns at most 20 results; the maximum is 200 per run. Credits for `maxResults` results are reserved before the run starts and the unused part is given back, so ask for what you need.

## Output

Each item in `results` has these fields:

| Field | Type | Always there |
| --- | --- | --- |
| `input` | string | yes |
| `website` | string | yes |
| `domain` | string | yes |
| `status` | "found" \| "none-found" | yes |
| `email` | string | no |
| `emails` | string[] | yes |
| `emailSources` | object[] | no |
| `phone` | string | no |
| `phones` | string[] | yes |
| `facebookUrl` | string | no |
| `instagramUrl` | string | no |
| `linkedinUrl` | string | no |
| `xUrl` | string | no |
| `youtubeUrl` | string | no |
| `tiktokUrl` | string | no |
| `pinterestUrl` | string | no |
| `pagesVisited` | string[] | yes |
| `scrapedAt` | string | yes |

## Cost

1 credit per website with contacts. One website where at least one email, phone number or social profile was found. Websites without any contact data, unreachable and blocked websites are not charged. Check the balance with:

```bash
curl -sS "https://pullwire.dev/api/v1/me" -H "x-api-key: $PULLWIRE_API_KEY"
```

The answer has `balance` in credits. Tell the user what a run cost when it was more than a few credits.

## Bigger jobs

Add `?async=1` to the run URL to start a background run (up to 50 seconds). It answers `202` with `run.id` right away. Poll every few seconds until `status` is `succeeded` or `failed`, then fetch the results:

```bash
curl -sS "https://pullwire.dev/api/v1/runs/$RUN_ID" -H "x-api-key: $PULLWIRE_API_KEY"
curl -sS "https://pullwire.dev/api/v1/runs/$RUN_ID/results" -H "x-api-key: $PULLWIRE_API_KEY"
```

Add `?format=csv` to the results URL when the user wants a spreadsheet.

## Errors

Errors answer `{"error": {"code": "...", "message": "..."}}`.

| Status | Code | What to do |
| --- | --- | --- |
| 400 | `INVALID_INPUT` | The message names the field. Fix the input and retry; nothing was charged. |
| 401 | `UNAUTHORIZED` | The key is missing, wrong or revoked. Ask the user to set a valid key in `PULLWIRE_API_KEY`. |
| 402 | `INSUFFICIENT_CREDITS` | The balance cannot cover the reservation. Lower `maxResults` or tell the user the balance is too low. |
| 404 | `NOT_FOUND` | Unknown tool or run ID. |
| 429 | `RATE_LIMITED` | Used up its burst of 120 requests and its 60 per minute. Wait, then retry. |
| 500 | `INTERNAL_ERROR` | Retry later. If the body has `runId`, fetch the partial results with `GET /runs/{runId}/results`. |
| 503 | `TOOL_UNAVAILABLE` | Our latest automatic checks against this site failed, so its runs are paused. Nothing was reserved or charged. Tell the user and try again later. |
| 503 | `SERVICE_UNAVAILABLE` | Retry later. |

Docs: https://pullwire.dev/docs · Tool page: https://pullwire.dev/tools/website-contacts
