# Identity Resolution API - PII to Pseudonymous Person ID (`nabeelbaghoor/identity-graph-resolution-api`) Actor

Resolve names, postal addresses, email addresses and phone numbers, plaintext or hashed, to a stable pseudonymous person identifier, so the same person can be recognised across systems without those systems exchanging contact details. Returns the match level and components. Pay per result.

- **URL**: https://apify.com/nabeelbaghoor/identity-graph-resolution-api.md
- **Developed by:** [Nabeel Hassan](https://apify.com/nabeelbaghoor) (community)
- **Categories:** Business, Developer tools, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 results

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Identity Resolution API - PII to Pseudonymous Person ID

Turn names, addresses, emails and phone numbers into a stable identifier for the person behind them, so two systems can agree they are talking about the same customer without either of them handing over the contact details. One flat row per result.

### What this actor does

- **Resolves contact details to one durable identifier.** The same person, arriving as an email address in one system and as a name and postal address in another, comes back as the same identifier. That is what makes deduplication, suppression and cross-system measurement possible without moving personal data between them.
- **Works down a match cascade and tells you where it landed.** The provider tries name with address and postal code first, then name with email, then name with phone, then partial name with email or phone, then email alone, then phone alone, then name with postal code. Every row says which step produced it and what that step means, so a strong match and a weak one are never mixed together in the same column.
- **Reports which fields did the work.** The components that contributed to a match come back as their own column, so you can require the ones you trust. If your email data is good and your addresses are stale, require the email component and drop the rest.
- **Distinguishes a single clear match from several plausible ones.** The distinct match flag says whether one best answer stood out. It is the strictest quality filter available here, and it is exposed as a filter rather than left in the raw payload.
- **Accepts hashed values as well as plaintext.** If contact details must not leave your side in the clear, hash them and use the lookup mode. MD5, SHA-1 and SHA-256 are all accepted, and a digest of the wrong length for the chosen algorithm is refused here rather than sent, because one bad element can cost you a whole batch.
- **Respects the provider's privacy floor rather than fighting it.** No batch under 100 records is accepted and no batch resolving fewer than 25 distinct identifiers is answered at all, because a small identity lookup is a way to reidentify one person. The run gathers records into batches that clear the floor, evens the sizes so no remainder batch is refused, and stops with a plain explanation when there are too few records rather than sending a request that will be rejected.
- **Never pretends results line up with inputs.** The provider deliberately shuffles its responses so a caller cannot pair result three with input three. Nothing here invents that pairing. Rows carry their position in the response and say what it is.
- **Reads the errors that hide inside a success.** A per-record failure rides inside a 200 response with its own code and message. An opted-out person is reported as opted out, not written up as a match that happened to be empty.
- **Validates before it sends.** Two-letter states, postal codes of five, nine or eleven digits, ten-digit phone numbers, emails with a dot in the domain, and the rule that a name or an address may be given whole or in parts but never both. Every one is checked locally, so a malformed row is a flagged row rather than a rejected batch.
- **Refreshes tokens on a clock, not on a failure.** Access tokens here last about fifteen minutes and an expired one is reported the same way as a missing permission, so retrying it as a permission problem just fails again. This actor renews before expiry.
- **Never charges for a miss.** Only rows carrying a resolved identifier are billed.

### Input

| Field | What it does |
| --- | --- |
| `mode` | Match plaintext details, or look up hashed values. |
| `emails` | Addresses to resolve, one per line. |
| `phones` | Phone numbers to resolve, one per line. |
| `records` | Full records as JSON objects, for names and addresses. |
| `hashes` | Hashed values to resolve, one per line. |
| `hashType` | MD5, SHA-1 or SHA-256. |
| `matchLimit` | Identifiers per record, 1 to 10. |
| `distinctMatchesOnly` | Keep only single clear matches. |
| `maxMatchConfidence` | Weakest cascade level to accept. |
| `requiredComponents` | Require these fields to have contributed. |
| `completeNamesOnly` | Require the whole name to have matched. |
| `requestsPerMinute` | Pace the run under your contract's rate limit. |
| `skipNotFound` | Leave unresolved records out of the dataset. |
| `maxResults` | Cap on rows written. Cannot shrink a batch. |
| `clientId` | Your own service account client id. Stored as a secret. |
| `apiKey` | Your own service account client secret. Stored as a secret. |

### Example output

```json
{
  "found": true,
  "mode": "match",
  "responseIndex": 0,
  "code": 200,
  "identifier": "XiT001sgRRky74xZ6NrpSsF6z2ucg6TeV8rISolIhOMe-R94lh47QP2xuVITxFm6otlyrB",
  "identifiers": ["XiT001sgRRky74xZ6NrpSsF6z2ucg6TeV8rISolIhOMe-R94lh47QP2xuVITxFm6otlyrB"],
  "matchConfidence": "2",
  "matchConfidenceMeaning": "Name and email",
  "matchComponents": ["name", "emailAddress"],
  "isDistinctMatch": true,
  "nameMatchIntegrity": "complete",
  "invalidParameters": [],
  "isOptedOut": false,
  "errorCode": null,
  "error": null,
  "raw": { }
}
```

### Frequently asked questions

#### What is a pseudonymous person identifier for?

Recognising the same person in two places without either place handing over the person's details. Your CRM knows an email address; your agency knows a postal address; a measurement partner knows neither. Resolve all three to the same identifier and they can agree they are looking at one customer, deduplicate, suppress an existing customer from an acquisition campaign, or measure a campaign against actual conversions, while the underlying contact details stay where they were.

#### Why is there a minimum of 100 records?

Because the provider enforces it, and the reason is worth understanding rather than working around. An identity API that answered one record at a time would be a reidentification tool: feed it a single email address and it hands you a durable key for that individual. Requiring at least 100 records per batch, and refusing to answer unless at least 25 distinct identifiers come back, makes that use impractical. This actor gathers your records into batches that clear the floor rather than sending them as they arrive, and evens the sizes so a remainder batch is never refused for being too small.

#### Can I match the results back to the rows I sent?

Not by position, and that is deliberate. The provider returns responses in a randomised order specifically so that result three cannot be paired with input three, which is the whole point of a pseudonymous identifier. This actor does not invent that pairing: each row carries its position in the response, labelled as such. What you get is the set of identifiers behind your list, along with the quality of each match. If you need a per-row join, that is a different product from this one and this API is designed not to be it.

#### What do the match levels mean?

They are steps in the provider's cascade, tried strongest first. Level 1 is name with street address and postal code. Level 2 is name with email. Level 3 is name with phone. Levels 4 and 5 are partial name with email or phone. Levels 6 and 7 are email or phone alone. Level 8 is name with postal code. A lower number means more corroborating evidence, so the weakest-match-to-accept filter is a floor on quality rather than a ceiling.

#### What is the difference between a distinct match and a non-distinct one?

Distinct means one candidate scored highest on its own. Non-distinct means several were equally good and the provider returned one of them. For suppression, where a false positive means failing to contact a real prospect, non-distinct matches are usually fine. For anything where a wrong join has consequences, filter to distinct only. The flag is only reported when you ask for one identifier per record, which is another reason to leave that at one.

#### Should I send plaintext or hashed values?

Send hashed values when your own rules say contact details must not leave your systems in the clear, which is common and often contractual. The cost is matching power: hashed lookup is an exact match on a normalised value, so a misspelling, a nickname or a changed street type will not match, whereas the plaintext match route normalises and matches fuzzily before it gives up. Use plaintext when you can and hashed when you must, and expect a lower hit rate from hashed.

#### Why were some of my records refused before they were sent?

Because this provider validates strictly and rejects the whole batch when one element is malformed. A two letter state, a postal code of exactly five, nine or eleven digits, a phone of exactly ten digits, an email with a dot in the domain. There is also a rule that catches people out: a name may be given whole or split into first, middle and last, and a street address may be given whole or split into its components, but never both ways in the same record. This actor checks all of that locally, so a bad row becomes a flagged row rather than a failed run.

#### What does opted out mean in the results?

That the person has exercised their right to be excluded from this kind of processing. Those records come back with an error code and no identifier, by design, and no amount of retrying will change it. They are reported as their own flag rather than being lumped in with records the provider simply does not know, because they are a different fact about a different situation.

#### Are the routes in this actor verified against the live service?

The token route is: deliberately invalid credentials answer with the provider's own "Invalid Client Credentials". The two batch routes are not, and that is worth saying plainly. This gateway validates the bearer token before it decides whether a path exists, so a real path and an invented one both answer identically and probing proves nothing about either. Those two come from the provider's own published documentation instead.

#### Do I need my own API key?

Yes, and this one is not self-service. This actor does not include data access. You need a service account with LiveRamp, the provider whose identity resolution API this actor calls, which issues a client id and a client secret through your account representative rather than through a signup form. Both go into the input and are stored as Apify secrets. Your own contract, permissions and terms apply, and some capabilities, such as returning more than one identifier per record, are permissions granted separately on your account.

#### How much does a run cost?

Pricing is pay per result: you are charged for each identifier resolved into the dataset, and never for records that resolved to nothing, for people who have opted out, for inputs refused before they were sent, for rows dropped by the filters, or for duplicates. Apify platform usage is included in the per-result price. Your own provider contract is separate and billed by them.

### Keyword map

identity resolution API, identity graph, pseudonymous identifier, person identifier, PII to ID, hashed email matching, hashed phone matching, MD5 email hash, SHA-256 email hash, deterministic matching, fuzzy name matching, match cascade, customer deduplication, cross system customer matching, suppression list matching, audience onboarding, first party data activation, clean room identity, privacy preserving matching, opt out handling, match confidence, record linkage, customer data integration, identity spine

# Actor input Schema

## `mode` (type: `string`):

Match takes plaintext contact details and does its own normalisation and fuzzy matching, working down a cascade from the strongest combination to the weakest. Lookup takes values you have already hashed, which never leave your side in the clear.

## `emails` (type: `array`):

In match mode, addresses to resolve, one per line. Email on its own is a valid combination for this provider and sits at step six of its match cascade. Remember the batch floor: at least 100 records must reach the provider in one run.

## `phones` (type: `array`):

In match mode, numbers to resolve, one per line. The provider wants ten digits and nothing else; a leading country code of 1 is stripped for you and anything that is not ten digits after that is refused before it can spoil the batch.

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

In match mode, full records as JSON objects, when you have more than an address or a number. Accepted keys: name or firstName, middleName, lastName and generationalSuffix; streetAddress or primaryNumber, preDirectional, street, streetSuffix, postDirectional, unitDesignator and secondaryNumber; plus city, state, zipCode, email and phone. Give a name whole or in parts, never both, and the same for a street address: the provider refuses a record that does both, so this actor refuses it first.

## `hashes` (type: `array`):

In lookup mode, the digests to resolve, one per line, all of the same algorithm. Digests of the wrong length for the chosen algorithm are refused here rather than sent, because a single bad element can cost you the whole batch.

## `hashType` (type: `string`):

Which algorithm produced the digests above. SHA-256 and MD5 apply to hashed email addresses; SHA-1 also covers hashed phone numbers. Getting this wrong reads as every value being unknown rather than as an error, which is why the length is checked against it before anything is sent.

## `matchLimit` (type: `integer`):

How many identifiers to return for one record, from 1 to 10. Anything above 1 needs a permission the provider grants separately, so leave it at 1 unless you know your account has it. Note that asking for more than one also stops the provider reporting whether a match was distinct.

## `distinctMatchesOnly` (type: `boolean`):

Keep only rows where the provider found a single clear best match rather than several equally good ones. This is the strictest quality filter available here. It only means anything when you are asking for one identifier per record, because the flag is not reported otherwise.

## `maxMatchConfidence` (type: `integer`):

The provider reports which step of its cascade produced the match, from 1 for name, address and postal code down to 8 for name and postal code alone. A lower number is a stronger match, so this is a floor on quality: set 3 to accept only matches made on a name together with an address, an email or a phone. Leave at zero to accept every level.

## `requiredComponents` (type: `array`):

Keep only rows where every one of these contributed to the match. Use it when a particular field is the one you trust: requiring the email component drops matches that were made on a name and a postal code alone.

## `completeNamesOnly` (type: `boolean`):

Drop rows where only part of the name was used to reach the match. Partial name matching is what lets the provider connect a record for Bob to a record for Robert, which is useful and is also where the false positives live.

## `requestsPerMinute` (type: `integer`):

Pace the run so it stays under whatever rate your contract allows. Each request here carries up to a thousand records, so this number is far smaller than it looks.

## `skipNotFound` (type: `boolean`):

Leave results with no identifier, and inputs refused before they were sent, out of the dataset. Off by default: the count of unresolved records is how you measure the coverage of your own data, and the opted-out flag is worth keeping. Misses are never charged for either way.

## `maxResults` (type: `integer`):

Stop after this many rows. Note that this cannot make a batch smaller: the provider refuses anything under 100 records, so a lower cap here still sends the full batch and simply writes fewer rows.

## `clientId` (type: `string`):

The client id of your own service account with the identity provider, issued by your account representative. Stored as a secret and never logged.

## `apiKey` (type: `string`):

The client secret that goes with the id above. It is exchanged for a short-lived access token, which the actor refreshes before it expires rather than after, because this provider reports an expired token the same way it reports a missing permission. Stored as a secret.

## Actor input object example

```json
{
  "mode": "match",
  "emails": [],
  "phones": [],
  "records": [],
  "hashes": [],
  "hashType": "sha256",
  "matchLimit": 1,
  "distinctMatchesOnly": false,
  "maxMatchConfidence": 0,
  "requiredComponents": [],
  "completeNamesOnly": false,
  "requestsPerMinute": 60,
  "skipNotFound": false,
  "maxResults": 1000
}
```

# Actor output Schema

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

One row per result, carrying the pseudonymous person identifier, the level of the provider's match cascade that produced it and what that level means, which input components contributed, whether the match was distinct, whether the whole name was used, and whether the person has opted out. Rows are in the provider's own shuffled order and are not paired with input rows.

# 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 = {
    "emails": [],
    "phones": [],
    "records": [],
    "hashes": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("nabeelbaghoor/identity-graph-resolution-api").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 = {
    "emails": [],
    "phones": [],
    "records": [],
    "hashes": [],
}

# Run the Actor and wait for it to finish
run = client.actor("nabeelbaghoor/identity-graph-resolution-api").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 '{
  "emails": [],
  "phones": [],
  "records": [],
  "hashes": []
}' |
apify call nabeelbaghoor/identity-graph-resolution-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nabeelbaghoor/identity-graph-resolution-api"
        }
    }
}

```

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/pgYHS9FLwp9jNd20H/builds/q4iF7xcncfjb7nXUR/openapi.json
