API documentation
The API lets you submit searches programmatically and receive a structured answer. Base URL — https://lookupyou.com/api/v1, JSON in and out, all dates in responses are ISO 8601 (UTC). Below is a live reference: it is built from the same OpenAPI schema the service serves, so it matches the real behaviour.
Keys, balance, packages and request history live in the dashboard. Open dashboard
Authentication
Every request carries an API key in the Authorization header. The key is created in the dashboard and shown once — save it right away, it cannot be recovered. Afterwards the UI shows only a fragment like lp_live_a7f3…9c21.
Authorization: Bearer lp_live_a7f3f0c98b1d4e7aa2b5c8d19c21
A key can be limited by lifetime and by a list of IP addresses. If a key is compromised, revoke it in the dashboard — access stops immediately: requests get 401 and are not charged. Manage keys
First request
A minimal search in fast mode — the program waits for the result on the same connection. The answer usually takes 6–12 seconds; longer under queue.
curl -X POST https://lookupyou.com/api/v1/search \
-H "Authorization: Bearer lp_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"product":"ssn_bg","first_name":"John","last_name":"Miller","hint":"1026 Maple Lane","mode":"sync"}'Response · 200 OK
{
"request_id": "req_8f21c4",
"client_ref": "order-1187",
"product": "ssn_bg",
"mode": "sync",
"status": "completed",
"query": { "first_name": "John", "last_name": "Miller", "hint": "1026 Maple Lane", "dob": null },
"charged": true,
"charge": { "source": "package", "amount_cents": 0, "requests": 1 },
"cached": false,
"error": null,
"queue_position": 0,
"duration_ms": 7840,
"created_at": "2026-08-17T11:07:12Z",
"finished_at": "2026-08-17T11:07:20Z",
"result_expires_at": "2026-11-15T11:07:20Z",
"result": {
"found": true,
"matches_count": 1,
"records": [{ "full_name": "MILLER, JOHN A", "ssn": "445-72-4417", "dob": "3/14/1979", "age": 47, "addresses": [ … ], "phones": [ … ] }]
}
}product, first_name and last_name are required, plus at least one clarifier: hint (street without ZIP, city or state) or dob (date of birth MM/DD/YYYY). Without a clarifier the source does not search — 422, nothing charged.
Sync and async mode
The data source allows one active session, so searches run one after another. The mode field decides how you get the result:
- sync (default) — the connection is held until the result is ready. Suits single searches triggered by a user action. Our timeout is 5 minutes.
- async — the API answers 202 Accepted with the queue position right away, and the result is delivered to your webhook as search.completed or search.failed. It can also be fetched any time via GET /requests/{id}. The only convenient option for a stream of requests.
Queue and waiting
The 202 response has queue_position and eta_seconds — how many requests are ahead and roughly how long to wait; sync responses carry X-Queue-Position. The current queue length is served by GET /queue. The queue is fair: no plan has priority.
No more than 8 of your requests can be in progress at once; beyond that — 429 too_many_running. For a stream this is not a problem: send the next one when you get an answer or a notification.
Add-ons for a found person
If an SSN + DOB search found a person, the driver licence (DL) and the MVR can be taken from the same record without a second call to the source — cheaper and instant: POST /requests/{id}/addons with the addon field. If the record lacks that part, the answer has status not_found and nothing is charged. Buying the same add-on again is free from the saved answer.
| Add-on | After which product | What you get | Price |
|---|
| dl_after_ssn | ssn_bg | Licence number and issuing state | $1.00 |
| mvr_after_ssn | ssn_bg | Licence issue and expiry dates, sometimes sex, race, height | $7.50 |
| mvr_after_dl | dl | The same — for the licence number already bought | $5.00 |
The credit report (CR and CS) is not available via the API yet — 409 addon_unavailable. It is handled by support.
Endpoints
All paths are relative to the base URL. Every endpoint requires the Authorization: Bearer <key> header.
POST/searchRun a search
Sync mode holds the connection until the source answers (usually 6–12 seconds, longer under queue). Async mode returns 202 immediately; the result is delivered to your webhook and stays available via GET /requests/{id}. Only a completed search is charged: a validation error, an unavailable source or a rate-limit rejection cost nothing.
Request body
| Field | Type | Description | Example |
|---|
| productrequired | ssn_bg | dl | mvr | ssn_bg — SSN + DOB (package or balance), dl / mvr — balance only | |
| first_namerequired | string | | John |
| last_namerequired | string | | Miller |
| hint | string | Street without ZIP, city or state. Required unless dob is given | 1026 Maple Lane |
| dob | string | MM/DD/YYYY. Required unless hint is given | 03/14/1979 |
| mode | sync | async | (default: sync) | |
| client_ref | string | Your identifier, echoed back in responses and webhooks | |
Responses
200Search finished (found or not found)Request 202Accepted into the queue (async mode)Accepted 401Missing, invalid, revoked or expired API keyError 402Not enough funds: no package requests left and balance is below the product priceError 403IP address is not allowed for this key, or the account is suspendedError 422Validation error — the search was not sent to the sourceError 429Rate limit: 60 requests per minute per account, or too many requests running at onceError 503Source did not answer; nothing was chargedError GET/requestsList recent requests
Parameters
| Name | In | Type | Description |
|---|
| limit | query | integer | |
| before | query | string | Return requests created before this ISO timestamp (pagination) |
| status | query | queued | running | completed | not_found | failed | rejected | |
Responses
200Requests without result bodies
GET/requests/{id}Get a request with its result
Parameters
| Name | In | Type | Description |
|---|
| idrequired | path | string | |
Responses
404Unknown request idError GET/requests/{id}/reportFull report as plain text
Parameters
| Name | In | Type | Description |
|---|
| idrequired | path | string | |
Responses
404No result for this request (not finished, failed, or expired)Error POST/requests/{id}/addonsBuy an add-on from a saved result
Driver licence or MVR taken from the record already found — no second call to the source, so it is cheaper and instant. If the record has no such data, the answer is `not_found` and nothing is charged. Already bought add-ons are returned again for free.
Parameters
| Name | In | Type | Description |
|---|
| idrequired | path | string | |
Request body
| Field | Type | Description | Example |
|---|
| addonrequired | dl_after_ssn | mvr_after_ssn | mvr_after_dl | | |
Responses
402Balance is below the add-on priceError 404Unknown request idError 409Add-on is not applicable to this request, or the credit report is not available via API yetError GET/accountBalance, packages and prices
Responses
GET/queueQueue length at the source
Responses
Objects
Fields of response objects. Types follow JSON Schema; nullable fields may carry no data.
Error
| Field | Type | Description |
|---|
| error | string | · Example: payment_required |
| message | string | |
| charged | "false" | |
| retry_after | integer | seconds, only with 429 |
Accepted
| Field | Type | Description |
|---|
| request_id | string | · Example: req_8f21c4 |
| status | "queued" | |
| queue_position | integer | |
| eta_seconds | integer | |
Request
| Field | Type | Description |
|---|
| request_id | string | |
| client_ref | string | null | |
| product | string | |
| mode | sync | async | |
| status | queued | running | completed | not_found | failed | rejected | |
| query | object | |
| query.first_name | string | |
| query.last_name | string | |
| query.hint | string | null | |
| query.dob | string | null | |
| charged | boolean | |
| charge | object | |
| charge.source | package | balance | none | |
| charge.amount_cents | integer | |
| charge.requests | integer | |
| charge.reason | string | null | |
| cached | boolean | Same query within 5 minutes — served from the saved answer for free |
| error | object | null | |
| error.code | string | |
| error.message | string | null | |
| queue_position | integer | null | |
| duration_ms | integer | null | |
| created_at | string | |
| finished_at | string | null | |
| result_expires_at | string | null | Results are kept for 90 days |
| result | Result | null | |
Result
| Field | Type | Description |
|---|
| found | boolean | |
| matches_count | integer | |
| records | Person[] | |
PersonFor ssn_bg: identity, addresses, phones, emails, relatives, civil records. For dl: name and licence numbers. For mvr: licences with dates.
| Field | Type | Description |
|---|
| full_name | string | |
| ssn | string | null | |
| dob | string | null | · Example: 3/14/1979 |
| age | integer | null | |
| gender | string | null | |
| aka | string[] | |
| addresses | object[] | |
| addresses[].raw | string | |
| addresses[].city | string | null | |
| addresses[].state | string | null | |
| addresses[].zip_code | string | null | |
| addresses[].date_from | string | null | |
| addresses[].date_to | string | null | |
| addresses[].is_current | boolean | |
| phones | object[] | |
| phones[].number | string | |
| phones[].formatted | string | |
| phones[].phone_type | string | null | |
| phones[].carrier | string | null | |
| emails | string[] | |
| relatives | object[] | |
| relatives[].full_name | string | |
| relatives[].relationship_type | string | null | |
| relatives[].dob_year | string | null | |
| relatives[].current_age | integer | null | |
| civil_records | object | null | |
| civil_records.bankruptcies | integer | |
| civil_records.liens | integer | |
| civil_records.judgments | integer | |
| driver_licenses | object[] | |
| driver_licenses[].number | string | |
| driver_licenses[].state | string | null | |
| driver_licenses[].issue_date | string | null | |
| driver_licenses[].expiration_date | string | null | |
| driver_licenses[].gender | string | null | |
| driver_licenses[].race | string | null | |
| driver_licenses[].height | string | null | |
Account
| Field | Type | Description |
|---|
| nickname | string | |
| balance_cents | integer | |
| available_cents | integer | balance minus reservations of running searches |
| package_requests_left | integer | |
| packages | object[] | |
| packages[].id | string | |
| packages[].name | string | |
| packages[].requests_left | integer | |
| packages[].requests_total | integer | |
| packages[].expires_at | string | |
| prices | object | your price per product/add-on, cents |
| limits | object | |
| limits.requests_per_minute | integer | |
| limits.concurrent_requests | integer | |
Response codes
Every error body looks the same: error (machine-readable code), message (text), charged: false. With 429 there is also retry_after in seconds and a Retry-After header.
| Code | error | Meaning | Request charged |
|---|
| 200 | — | Search finished: status completed (found) or not_found (no matches) | Yes — a completed search |
| 202 | — | Accepted into the queue (mode: async) | No — charged on execution |
| 401 | unauthorized | No key, wrong key, revoked or expired | No |
| 402 | payment_required | No package requests and the balance is below the product price | No |
| 403 | forbidden | IP not allowed for the key, or the account is suspended | No |
| 404 | not_found | Unknown request_id | No |
| 409 | addon_unavailable | Add-on not applicable to this request, or not available via API yet | No |
| 422 | validation_error | Parameters rejected: no clarifier, ZIP in the address, non-Latin letters, bad date | No |
| 429 | rate_limited / too_many_running | More than 60 requests per minute per account, or too many requests in progress | No |
| 503 | source_unavailable | The data source did not answer | No |
A special case — a person was found but the record lacks the required part (SSN without a full date of birth, DL without a number, MVR without dates). Such an answer comes as not_found and is never charged.
Webhook notification format
The URL and secret are set in the dashboard. A notification is a POST with a JSON body: the same Request object GET /requests/{id} returns, plus an event field. A 2xx response counts as delivered, otherwise retries follow. Answer quickly — the timeout is 5 seconds, do the processing in the background.
Events
- search.completed — search finished, result ready (including not_found)
- search.failed — search did not take place, nothing charged
- package.low — requests below the threshold from notification settings
- package.expiring — package expires soon
Headers: X-Signature — signature, X-Event — event, X-Delivery-Id — delivery id (for deduplication), X-Request-Id, X-Timestamp.
POST https://your-service.com/hooks/search
Content-Type: application/json
X-Signature: sha256=6b1c…f0a2
X-Event: search.completed
X-Delivery-Id: 4c1d0e9a-…
X-Request-Id: req_8f21c4
X-Timestamp: 1786901240
{
"event": "search.completed",
"request_id": "req_8f21c4",
"client_ref": "order-1187",
"product": "prod_ssn_bg",
"status": "completed",
"charged": true,
"error": null,
"finished_at": "2026-08-17T11:07:20Z",
"result": { "found": true, "matches_count": 1, "records": [ … ] }
}Signature check
X-Signature is sha256= followed by HMAC-SHA256 of the raw request body, keyed with the webhook secret from the dashboard. Compute the HMAC over the body bytes before parsing JSON and compare in constant time.
Delivery retries
If your endpoint answered non-2xx or did not answer within 5 seconds, delivery is retried after 1, 5, 15 and 60 minutes — four attempts in total. Every attempt is visible in the dashboard, and undelivered ones can be resent manually there. After three failures in a row an email is sent. Results are not lost — they are available via GET /requests/{id}.
When a request is charged
A charge happens only for a completed search: the source answered and the answer was passed to you. No matches is also a completed search — the work was done. Parameter errors, an unavailable source, a rate-limit rejection and a revoked key charge nothing. Every response has the charged field and a charge object with the source of the charge.
A package request is an ssn_bg search: the package with the earliest expiry is used first, then the balance at retail price. Products dl and mvr and add-ons are always charged from the balance.
The same request (product and every field match) within 5 minutes is served from the saved answer: cached: true, nothing charged.
The result is kept for 90 days (result_expires_at), then deleted; request metadata stays in the history.
Rate limiting
One limit for all plans — 60 requests per minute per account and 8 requests in progress at once. It is not for sale, not raised for money, and exists only so that one client’s looping program cannot occupy the whole queue. The current state comes back in the headers of every response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 34
X-Request-Id: req_8f21c4
X-Queue-Position: 3
Code examples
Fast mode in three languages. Put in your own key and URL.
curl
curl -X POST https://lookupyou.com/api/v1/search \
-H "Authorization: Bearer lp_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"product":"ssn_bg","first_name":"John","last_name":"Miller","hint":"1026 Maple Lane","mode":"sync"}'Python
import requests
r = requests.post(
"https://lookupyou.com/api/v1/search",
headers={"Authorization": "Bearer lp_live_YOUR_KEY"},
json={"product": "ssn_bg", "first_name": "John", "last_name": "Miller", "hint": "1026 Maple Lane"},
timeout=300,
)
data = r.json()
if r.status_code == 200 and data["status"] == "completed":
print(data["result"]["records"][0]["ssn"])
else:
print(r.status_code, data.get("error"), data.get("message"))Node.js
const res = await fetch("https://lookupyou.com/api/v1/search", {
method: "POST",
headers: { Authorization: "Bearer lp_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ product: "ssn_bg", first_name: "John", last_name: "Miller", hint: "1026 Maple Lane" }),
});
const data = await res.json();
if (res.ok && data.status === "completed") console.log(data.result.records[0].ssn);
else console.log(res.status, data.error, data.message);Async mode and webhook
Submitting to the queue and verifying the notification signature on your server.
// 1. отправка в очередь
const accepted = await fetch("https://lookupyou.com/api/v1/search", {
method: "POST",
headers: { Authorization: "Bearer lp_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ product: "ssn_bg", first_name: "John", last_name: "Miller", hint: "TX", mode: "async", client_ref: "order-1187" }),
}).then((r) => r.json());
// { request_id: "req_8f21c4", status: "queued", queue_position: 3, eta_seconds: 32 }
// 2. приём вебхука (Express)
import crypto from "node:crypto";
app.post("/hooks/search", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto.createHmac("sha256", WEBHOOK_SECRET).update(req.body).digest("hex");
const given = req.get("X-Signature") ?? "";
if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) return res.sendStatus(401);
const event = JSON.parse(req.body); // { event: "search.completed", request_id, client_ref, status, charged, result }
res.sendStatus(200); // ответить сразу, обработку — фоном
setImmediate(() => handle(event));
});Changelog
- v1.0 — ssn_bg / dl / mvr search in sync and async modes, DL and MVR add-ons, signed webhooks with retries, request history, balance and packages, OpenAPI schema.