# Duplicate pre-check

> POST /api/v1/engine/dupe-check: ask whether ICON would call a lead a duplicate, before you post it.

```http
POST /api/v1/engine/dupe-check
```

Optional. Answers one thing: `duplicate_likely`, `true` or `false`. No score, no matched fields. **Submit always runs ICON's full duplicate check**, whatever the pre-check answered.

- **Server to server only.** The endpoint has no CORS: call it from your server, never from a web page.
- **Rate limit:** 30 per minute per key (bursts of up to 10), separate from your other limits.
- **Body:** at most 16 KB.

## Request

Send any of the lead's fields, raw (the same names as a lead), or hashed under `hashed`, or both. A hashed key wins over the same raw key. The more fields you send, the more accurate the answer.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `offer_id` | string (uuid) | — | Optional: one of your offers (for an offer-scoped policy). |
| `email` | string | — | — |
| `phone` | string | — | — |
| `first_name` | string | — | — |
| `last_name` | string | — | — |
| `zip` | string | — | — |
| `address` | string | — | — |
| `dob` | string | — | YYYY-MM-DD or MM/DD/YYYY. |
| `hashed` | object | — | — |

Raw:

```bash
curl -X POST 'https://iconroute.io/api/v1/engine/dupe-check' \
  -H "Authorization: Bearer $ICON_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "email": "ada.lovelace@mail.test",
  "phone": "(602) 555-2368",
  "zip": "85004",
  "last_name": "Lovelace"
}'
```

Hashed:

```json
{
  "hashed": {
    "email": "d6117306485ed0e50afab3ac871e98f81699151f30281527d63ff5f233656c69",
    "phone": "2f83685e66d4cb4d1bcff5f422ff9b0d7ce748a6f3103269d1adad16ab2e279b",
    "zip": "918abeeaae3a90ad25f3a5e45408af4cdd71ac92accc3638461dc2f76e39cc01"
  }
}
```

## Hashing

`hashed.<key>` is the **lower-case hex SHA-256** of the key's normalized form, encoded as UTF-8. Normalize first, then hash. Each example below was computed by ICON's own normalizer:

| Key | Normalized form | Example |
| --- | --- | --- |
| `email` | Trimmed and lower-case. At gmail.com and googlemail.com the dots and any +tag are removed from the local part and the domain is gmail.com. | `Jane.Doe+promo@GoogleMail.com` → `janedoe@gmail.com` |
| `phone` | The 10 US digits: no country code, no punctuation. | `+1 (602) 555-2368` → `6025552368` |
| `last_name` | Lower-case a–z only: accents removed (ł ø đ ð þ æ œ ß ı ħ ŧ written l o d d th ae oe ss i h t); spaces, hyphens and apostrophes dropped. | `Núñez-O'Brien` → `nunezobrien` |
| `zip` | The 5 digits (a +4 is dropped). | `85001-1234` → `85001` |
| `dob` | YYYY-MM-DD. Send birth_year with it when you hash a date of birth. | `04/07/1990` → `1990-04-07` |
| `birth_year` | Four digits (from a year-born answer or the date of birth). | `1990-04-07` → `1990` |
| `grad_year` | Four digits. | `2008` → `2008` |
| `ip` | The client address: the first of a forwarded list; IPv4 as a dotted quad without leading zeros; IPv6 compressed and lower-case, an IPv4-mapped address written as IPv4. | `2001:0DB8:0000:0000:0000:0000:0000:0001` → `2001:db8::1` |

The SHA-256 of each normalized example above:

```text
email      d6117306485ed0e50afab3ac871e98f81699151f30281527d63ff5f233656c69
phone      2f83685e66d4cb4d1bcff5f422ff9b0d7ce748a6f3103269d1adad16ab2e279b
last_name  36a69f8b745a87f4314b2dbcb8906ab78c03d4e2595f6b694f7af49b31f51242
zip        918abeeaae3a90ad25f3a5e45408af4cdd71ac92accc3638461dc2f76e39cc01
dob        8b44df2e1ab8ebe9eb4704090cf1524944141f66c7ab6dbed65391681a0d4878
birth_year a7be8e1fe282a37cd666e0632b17d933fa13f21addf4798fc0455bc166e2488c
grad_year  e5e53c784d5d49de1cabb6e904bf3380026aadcb9769775a268dd304dd9aa2df
ip         5afd19e856d1c18d17d600dfd2b5f534992333985e126c2a951047102c1ed536
```

Send the first name, the street address and the consent certificate **raw**, under their usual lead names (`first_name`, `address`, `trusted_form_cert_url` or `universal_leadid`): their matching forms are ICON's.

```javascript
import { createHash } from 'node:crypto';

const sha256 = (value) => createHash('sha256').update(value, 'utf8').digest('hex');

// Normalize first (see the table), then hash.
const body = {
  hashed: {
    email: sha256('janedoe@gmail.com'),
    phone: sha256('6025552368'),
    zip: sha256('85001'),
  },
};
```

```python
import hashlib

def sha256(value: str) -> str:
    return hashlib.sha256(value.encode("utf-8")).hexdigest()

body = {"hashed": {"email": sha256("janedoe@gmail.com")}}
```

## Response

```json
{
  "code": "OK",
  "status": "accepted",
  "reason": "OK",
  "success": true,
  "duplicate_likely": false,
  "retryable": false
}
```

`duplicate_likely: true` means a submit of this lead would very likely be refused as `DUPLICATE`. `false` is not a promise: the submit's own check decides.

## Errors

| HTTP | Meaning |
| --- | --- |
| 400 | MALFORMED_REQUEST: no usable lead field was sent (raw or hashed); INVALID_FIELD: offer_id is not a UUID. |
| 401 | AUTH_REQUIRED (no API key) or AUTH_INVALID (a key ICON does not know); status invalid. |
| 403 | FORBIDDEN: the offer_id is not yours, or this key is not permitted to use the pre-check. |
| 404 | Not found: the pre-check is not available on your account; ask your ICON contact. |
| 405 | MALFORMED_REQUEST: use POST. |
| 413 | The body is over 16 KB. |
| 429 | RATE_LIMITED (retryable): this key is over its pre-check limit. Wait the Retry-After seconds and retry. |
| 503 | UNAVAILABLE (retryable): the check could not run. Retry, or post the lead: submit runs the full check. |

A `404` answers a plain `{"error": "Not found"}` body.
