# B2B Company Data Enrichment & Completion (`akatra/b2b-data-completion-agent`) Actor

Complete missing company data from public sources. Add business emails, phones and company social profiles to existing records without re-enriching fields you already have. Built for CSV, API, AI agent and MCP workflows.

- **URL**: https://apify.com/akatra/b2b-data-completion-agent.md
- **Developed by:** [Akatra](https://apify.com/akatra) (community)
- **Categories:** AI, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 completed records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Akatra B2B Data Completion Agent

**Complete missing company data without enriching everything again.**

Already have part of the data? Akatra identifies what is missing and completes only what you requested.

Send existing business records and the fields you need. Akatra checks which of those fields are already present and valid,
reads the company's own public website for the rest, validates what it finds, records where every value came from, and
stops as soon as your data contract is satisfied. Fields you already have are never worked on again.

### What it does

1. **Normalizes** each record (domain, website, email, phone, social URLs, company name).
2. **Checks your data contract**: a required field counts as done only when it is present and valid, not merely non-empty.
3. **Detects gaps** and works only on those.
4. **Avoids already-complete work**: a record that already satisfies the contract makes no request at all.
5. **Uses internal public-company extraction**: a small, bounded read of the company's own website (home, contact, about, imprint).
6. **Validates** every candidate: business-level email addresses on the company's own domain, company social pages, public phone numbers.
7. **Tracks provenance**: for every value Akatra adds you get the method, the source page, the attempt and the time.
8. **Stops when the contract is satisfied** (or when no further eligible work exists) and tells you why.

Each record ends as `complete`, `partial`, `budget_exhausted` or `failed`, with a status for every requested field.

### Why completion is different from full re-enrichment

A typical enrichment run asks for every field of every record, even when you already hold the value. Akatra starts from
your data instead. If 40 of your 100 records already have the business email, those 40 emails are not searched, not
fetched and not billed. Work is planned per missing field, cached per website (ten records of the same company read the
site once), and capped per field.

### Input

| Field | Meaning |
|---|---|
| `records` | Your company records (objects). Useful keys: `companyName`, `website` or `domain`, `businessEmail`, `businessPhone`, `address`, `linkedinCompany`, `instagram`, `facebook`, `x`, `youtube`. A `recordId` or `id` of your own is kept. |
| `sourceMode`, `sourceDataset` | Read the records from an existing Apify dataset instead. The dataset is only ever read, never changed. |
| `requiredFields` | The data contract. See below. |
| `emailVerification` | `not_required` (default), `verified_if_possible`, `verified_required`. See below. |
| `maxPagesPerWebsite` | Public pages read per company website, 1 to 10 (default 5). |
| `maxConcurrency` | Websites read at the same time, 1 to 50 (default 10). |
| `maxRecords` | Process only the first N records. |
| `maxProviderAttempts` | Attempts per missing field, 1 to 10 (default 3). |
| `maxBudgetUsd` | Optional ceiling on external provider spend. Internal work is not counted. |
| `providerStrategy` | `internal_first` (default) or `internal_only`. |
| `includeProvenance` | Attach provenance to every value Akatra found (default on). |

#### Required fields

`companyName`, `domain`, `website`, `businessEmail`, `businessPhone`, `address`, `linkedinCompany`, `instagram`, `facebook`, `x`, `youtube`.

Akatra can currently complete `businessEmail`, `businessPhone`, `linkedinCompany`, `instagram`, `facebook`, `x` and `youtube`
from public company websites. `address` can be required and checked, but it is not extracted in V1 (see Limitations); a missing
address stays unresolved.

### Email verification semantics

`emailVerification` controls what "done" means for `businessEmail`.

- `not_required`: a valid business address is enough. Its verification status is `not_checked`.
- `verified_if_possible`: Akatra verifies when a mailbox-level verifier is available. If none is, the record can still be
  `complete` and carries a soft warning (`soft_gap_unresolved:businessEmail:verification_required`).
- `verified_required`: the email stays **unresolved** until mailbox-level verified evidence exists.

Verification statuses are never blurred: **`not_checked` is not `verified`. `mx_only` is not `verified`. `catch_all` is not
`verified`. `unknown` is not `verified`.** `verified` means mailbox-level evidence. The current V1 has no mailbox-level verifier,
so addresses are reported as `not_checked`, and `verified_required` records stay `partial`.

### Output

Every input record produces one dataset item. The run also stores a compact `SUMMARY` record in the key-value store.

Each item answers the questions an AI agent or a script needs: which fields were requested (`requestedFields`), which already
existed (`fieldStates` = `existing`), which Akatra resolved (`resolvedFields`), which remain unresolved (`unresolvedFields`)
and why (`unresolvedReasons`), what was tried (`attempts`), where each accepted value came from (`provenance`), what it cost
(`cost`), and why processing stopped (`stopReason`).

`schemaVersion` is the version of the Akatra **result schema** (currently `1`), not the Actor code version. It changes only
when the layout of an item changes in a breaking way.

#### Status meanings

| `status` | Meaning |
|---|---|
| `complete` | Every required field is satisfied. Soft warnings can remain (for example email verification under `verified_if_possible`). |
| `partial` | Some required fields are unresolved after the allowed work. Not finding data is `partial`, not `failed`. |
| `budget_exhausted` | A spending limit blocked the remaining work. |
| `failed` | The record itself could not be processed. |

| `fieldStates` value | Meaning |
|---|---|
| `existing` | Already in your record and valid. Not worked on. |
| `resolved` | Akatra found and validated it in this run. |
| `unresolved` | Not found after the allowed work. |
| `unverified` | Present and valid, but the verification you required has not happened. |

| `stopReason` | Meaning |
|---|---|
| `contract_satisfied` | Everything required is done. |
| `attempts_exhausted` | Every eligible provider was tried and none returned a usable value. |
| `provider_unavailable` | No provider can serve an unresolved field (for example `address`). |
| `identity_conflict` | Your website and domain point to different businesses. Akatra does not guess. |
| `budget_blocked` | A spending limit blocked the remaining work. |
| `engine_error` | The record could not be processed. |

#### Provenance

For each value Akatra adds: `providerId`, `method` (`mailto`, `visible_text`, `tel_link`, `json_ld`, `same_as`, `page_link`,
`cloudflare_protected`), `sourceUrl`, `pageType` (`homepage`, `contact`, `about`, `imprint`, `guessed`), `attempt` and
`discoveredAt`. Akatra does not invent confidence scores, and rejected values are never stored.

### Example input

```json
{
  "records": [
    {
      "recordId": "acme-001",
      "companyName": "Plausible Analytics",
      "website": "https://plausible.io",
      "x": "https://x.com/plausiblehq"
    }
  ],
  "requiredFields": [
    "businessEmail",
    "linkedinCompany",
    "x"
  ]
}
```

### Example output

One dataset item (the website was read live; `x` was already in the record, so it was not searched):

```json
{
  "recordType": "result",
  "schemaVersion": 1,
  "recordId": "acme-001",
  "aliases": [],
  "status": "complete",
  "company": {
    "name": "Plausible Analytics",
    "domain": "plausible.io",
    "website": "https://plausible.io/",
    "businessEmails": [
      {
        "address": "hello@plausible.io",
        "verification": "not_checked"
      }
    ],
    "businessPhones": [],
    "address": null,
    "social": {
      "linkedin": "https://www.linkedin.com/company/plausible-analytics",
      "instagram": null,
      "facebook": null,
      "x": "https://x.com/plausiblehq",
      "youtube": null
    }
  },
  "requestedFields": [
    "businessEmail",
    "linkedinCompany",
    "x"
  ],
  "resolvedFields": [
    "businessEmail",
    "linkedinCompany"
  ],
  "unresolvedFields": [],
  "stopReason": "contract_satisfied",
  "unresolvedReasons": {},
  "fieldStates": {
    "businessEmail": "resolved",
    "linkedinCompany": "resolved",
    "x": "existing"
  },
  "attempts": [
    {
      "providerId": "internal_web",
      "capability": "businessEmailDiscovery",
      "fields": [
        "businessEmail"
      ],
      "attempt": 1,
      "status": "success",
      "latencyMs": 328,
      "estimatedCostUsd": 0.0,
      "errorCode": null
    },
    {
      "providerId": "internal_web",
      "capability": "linkedinCompanyDiscovery",
      "fields": [
        "linkedinCompany"
      ],
      "attempt": 1,
      "status": "success",
      "latencyMs": 0,
      "estimatedCostUsd": 0.0,
      "errorCode": null
    }
  ],
  "provenance": {
    "businessEmail": [
      {
        "providerId": "internal_web",
        "actorId": null,
        "sourceUrl": "https://plausible.io/contact",
        "method": "mailto",
        "attempt": 1,
        "discoveredAt": "2026-10-03T16:22:39+00:00",
        "verification": null,
        "estimatedCostUsd": null,
        "pageType": "contact",
        "context": null
      }
    ],
    "linkedinCompany": [
      {
        "providerId": "internal_web",
        "actorId": null,
        "sourceUrl": "https://plausible.io/",
        "method": "page_link",
        "attempt": 1,
        "discoveredAt": "2026-10-03T16:22:39+00:00",
        "verification": null,
        "estimatedCostUsd": null,
        "pageType": "homepage",
        "context": "found_on_official_company_website"
      }
    ]
  },
  "dataQuality": {
    "completeness": 100.0
  },
  "cost": {
    "providerCalls": 2,
    "internalEstimatedUsd": null,
    "externalProviderUsd": 0.0,
    "akatraChargeUsd": null
  },
  "warnings": [],
  "original": {
    "recordId": "acme-001",
    "companyName": "Plausible Analytics",
    "website": "https://plausible.io",
    "x": "https://x.com/plausiblehq"
  },
  "scrapedAt": "2026-10-03T16:22:39+00:00"
}
```

The `SUMMARY` record (abridged):

```json
{
  "recordType": "summary",
  "schemaVersion": 1,
  "processed": 1,
  "complete": 1,
  "partial": 0,
  "budgetExhausted": 0,
  "failed": 0,
  "requestedFields": [
    "businessEmail",
    "linkedinCompany",
    "x"
  ],
  "resolvedFieldCounts": {
    "businessEmail": 1,
    "linkedinCompany": 1,
    "x": 0
  },
  "unresolvedFieldCounts": {
    "businessEmail": 0,
    "linkedinCompany": 0,
    "x": 0
  },
  "providerCalls": 2,
  "providerFailures": 0,
  "fallbackCount": 0,
  "internalResolutionCount": 2,
  "externalResolutionCount": 0,
  "avoidedProviderCalls": 1,
  "cost": {
    "internalEstimatedUsd": null,
    "externalProviderUsd": 0.0,
    "akatraChargeUsd": null
  },
  "runtimeSeconds": 0.36,
  "providerStrategy": "internal_first",
  "externalProvidersUsed": false,
  "internalWork": {},
  "stopReasonCounts": {
    "contract_satisfied": 1
  },
  "charging": {
    "mode": "off",
    "chargedEvents": 0,
    "limitReached": false
  },
  "scrapedAt": "2026-10-03T16:22:38+00:00"
}
```

### Cost and budget

- Fields you already have are not worked on and are not billed.
- Internal work (reading public pages) has no external provider cost. `cost.internalEstimatedUsd` is `null` because Akatra does not
  invent an internal price; `externalProviderUsd` is `0` unless an external provider was used.
- `maxBudgetUsd` limits external provider spend only. Your Apify run limit still applies to everything.
- Results are saved to the dataset first and charged afterwards, so a spending limit never discards finished records.
- Memory defaults to 1024 MB.

#### Pricing (pay per event)

| Event | Price | When it is charged |
|---|---|---|
| `completed-record` | $0.005 | The record ends `complete` **and** Akatra resolved at least one of your required fields in this run. One charge per record, however many fields were filled. |
| `resolved-field` | $0.001 | The record does **not** end `complete`; charged once for each required field Akatra resolved and validated. |

A record is charged with one event type at most, never both. There is no charge for fields that were already valid in your input,
for records where Akatra resolved nothing, for `failed` records, for rejected person-like data, or for attempts that found nothing.
Results are always saved before the charge, so reaching your run's maximum charge stops the run without losing saved records.
Apify platform usage is included in the event prices.

### Security

- Akatra only fetches public `http` and `https` pages on ports 80 and 443. It resolves the host, checks every returned IP address, and
  connects to the checked address. Private, loopback, link-local, metadata and other internal addresses are blocked, including through redirects.
- Redirects are followed manually (at most 3) and each hop is checked again. Response size, decompressed size, time and content type are limited.
- It reads each site's `robots.txt` and skips pages that are explicitly disallowed. If `robots.txt` cannot be read, Akatra does not assume permission
  and does not crawl that host for that call.
- Your source dataset is read-only. Results go only to this run's own dataset.
- No cookies, no sign-in, no CAPTCHA solving, no browser impersonation. Requests identify themselves as `AkatraB2BDataCompletion`.
- This is a defence against server-side request forgery, not a promise of absolute immunity.

### Public company data only

Akatra returns company-level public information. It does not return or store names of people, job titles, personal email addresses,
personal LinkedIn profiles or private mobile numbers. Role-based business addresses on the company's own domain (such as `info@` or
`sales@`) are accepted; `abuse@`, `postmaster@` and similar technical addresses are not. Person-like keys and values in your own records
are removed from the echoed `original` object. You are responsible for having a lawful basis to process the data you submit and the data you
collect.

### Limitations

- **Static HTML only.** There is no browser rendering. Some JavaScript-heavy sites show no contact data in their HTML and stay unresolved
  (reported as `insufficient_static_content` or `no_data_found`).
- **No proxy by default.** Some sites block requests from data centre addresses (HTTP 403 or 429); those records stay `partial`.
- **Email is the hardest field.** Many companies publish only a contact form. Akatra accepts only role-based addresses on the company's own domain.
- **Address is not extracted in V1.** A wrong address is worse than a missing one.
- **No personal contact enrichment** (decision makers, employees, personal emails, private mobiles).
- **External providers may be unavailable** depending on the Apify plan. V1 works internal-first and does not depend on them.
- **Website identity conflicts are not silently resolved** (`identity_conflict`).
- **Email verification** is not performed (see above).
- Sites that disallow crawling in `robots.txt`, or deny access to it, are skipped.

**What to expect.** In a 100-company mixed test on 3 October 2026 (public company websites, every email and social field required, 1024 MB),
Akatra found a business email for about 1 in 5 records that lacked one and a given social profile for roughly 55 to 65 percent of
records that lacked it. Results depend heavily on the sites. Treat Akatra as a way to fill what public pages offer, not as a guarantee.

### Use it from code, AI agents and MCP

Input and output are plain JSON and deterministic apart from timestamps and latencies.

```bash
curl -X POST "https://api.apify.com/v2/acts/akatra~b2b-data-completion-agent/run-sync-get-dataset-items?token=<YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"records":[{"companyName":"Plausible Analytics","website":"https://plausible.io"}],"requiredFields":["businessEmail","linkedinCompany"]}'
```

For AI agents, add this Actor to the Apify MCP server (`https://mcp.apify.com`) and let the agent call it with `records` and
`requiredFields`. Read `status`, `unresolvedFields`, `unresolvedReasons` and `stopReason` to decide what to do next; read `provenance` to
cite where a value came from.

### Support and legal note

Questions or a site that should be handled differently: use the Issues tab on this Actor. Akatra reads publicly available company
pages. Check the terms of the sites you process and the data protection rules that apply to you.

# Changelog

This Actor's version history is a separate document: https://apify.com/akatra/b2b-data-completion-agent/changelog.md

# Actor input Schema

## `sourceMode` (type: `string`):

'records' reads the list below. 'dataset' reads an existing Apify dataset you select. Your dataset is only ever read, never changed.

## `records` (type: `array`):

Used when the source is 'records'. A list of objects. Useful keys: recordId (or id), companyName, website or domain, businessEmail, businessPhone, address, linkedinCompany, instagram, facebook, x, youtube. Other keys are echoed in the output under 'original'; person-like keys and values (names, job titles, personal emails, personal LinkedIn profiles, mobile numbers) are removed. Akatra only completes company-level public data.

## `sourceDataset` (type: `string`):

Used when the source is 'dataset'. Read-only access.

## `requiredFields` (type: `array`):

The data contract. A field counts as done only when it is present and valid, not merely non-empty. Akatra works only on fields that are not done and stops when all are. It can complete businessEmail, businessPhone, linkedinCompany, instagram, facebook, x and youtube from the company's public website; address can be required and checked but is not extracted in V1.

## `emailVerification` (type: `string`):

'not_required' skips verification. 'verified_if_possible' verifies when a verifier is available. 'verified_required' keeps a business email unresolved until mailbox-level verification succeeds. Without a mailbox-level verifier an email is reported as mx_only at best, never verified.

## `maxBudgetUsd` (type: `number`):

Optional ceiling on estimated spend with external providers. Internal work is not counted. Leave empty for no extra ceiling beyond your Apify run limit.

## `maxRecords` (type: `integer`):

Optional. Process only the first N records.

## `maxProviderAttempts` (type: `integer`):

How many providers may be tried for one missing field before it is reported as unresolved.

## `providerStrategy` (type: `string`):

'internal_first' completes records with Akatra's own website reader and uses an approved external provider only as a fallback when one is available and affordable. 'internal_only' never calls an external provider.

## `includeProvenance` (type: `boolean`):

Attach, for every value Akatra found, the method, source page, attempt and time.

## `maxPagesPerWebsite` (type: `integer`):

How many public pages (home, contact, about, imprint) may be read per company website.

## `maxConcurrency` (type: `integer`):

How many company websites are read at the same time.

## Actor input object example

```json
{
  "sourceMode": "records",
  "records": [
    {
      "companyName": "ABC Hotel",
      "website": "https://abc.com"
    }
  ],
  "requiredFields": [
    "website",
    "businessEmail"
  ],
  "emailVerification": "not_required",
  "maxProviderAttempts": 3,
  "providerStrategy": "internal_first",
  "includeProvenance": true,
  "maxPagesPerWebsite": 5,
  "maxConcurrency": 10
}
```

# Actor output Schema

## `results` (type: `string`):

One item per input record: status, completed company data, resolved and unresolved fields, attempts, provenance and cost.

## `resultsFull` (type: `string`):

The same items with every field.

## `summary` (type: `string`):

Counts of complete, partial, budget-exhausted and failed records, resolved and unresolved fields per field, provider calls and failures, estimated spend.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "records": [
        {
            "companyName": "ABC Hotel",
            "website": "https://abc.com"
        }
    ],
    "requiredFields": [
        "website",
        "businessEmail"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("akatra/b2b-data-completion-agent").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "records": [{
            "companyName": "ABC Hotel",
            "website": "https://abc.com",
        }],
    "requiredFields": [
        "website",
        "businessEmail",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("akatra/b2b-data-completion-agent").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "records": [
    {
      "companyName": "ABC Hotel",
      "website": "https://abc.com"
    }
  ],
  "requiredFields": [
    "website",
    "businessEmail"
  ]
}' |
apify call akatra/b2b-data-completion-agent --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,akatra/b2b-data-completion-agent"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/xAAlYXh73yLfC0Nxr/builds/epbww8Ec77cVtx7Xp/openapi.json
