Everything the API does, in one page.
One core promise: paste a LinkedIn URL and get fact-checked personal context for cold email, mid-funnel, and CSM/CX: hobbies, causes, and public milestones contact data and scrapes miss.
Every fact carries a source link and the quote it came from. Broader
interest signals and the personality read are clearly marked as a
read, not a fact. A fresh enrichment usually
takes 45–90s (occasionally up to ~2–3 min), so the primary flow is
asynchronous: submit to
/api/enrich, then poll /api/jobs/:id.
Cache hits return inline, instantly. The deprecated
/api/brief compatibility alias follows the same async
paid-admission contract.
Quickstart
Verify a named client, claim its key once, then buy credit before submitting a real enrichment.
curl -X POST https://uniquenessengine.com/api/signup \
-H 'content-type: application/json' \
-d '{"email":"you@work.com","client_name":"My integration","device_name":"Laptop"}'
# -> 202 { "status":"email_verification_required",
# "transaction_id":"...", "polling_token":"...", "expires_at":"..." }
# Approve the named request from email. Poll /api/signup/verification/:transaction_id
# with x-verification-token, then POST .../:transaction_id/claim with the same
# header. The approved key is shown exactly once and is named for client_name.
# Signup grants no live credit. If email delivery returns 503, honor Retry-After
# and start a new verification transaction; no key was created.
# Buy a credit pack from /account. First offer: 5 credits for
# $5 USD subtotal plus applicable tax. Then submit:
# 1. submit -> 202 { "job_id": "...", "status": "queued" }
# (or 200 { "status": "done", "brief": {...} } on a cache hit)
curl -X POST https://uniquenessengine.com/api/enrich \
-H "authorization: Bearer uq_live_..." \
-H "content-type: application/json" \
-d '{"linkedin_url":"https://www.linkedin.com/in/<their-handle>"}'
# 2. poll every ~5s until status is done | refused | failed
curl https://uniquenessengine.com/api/jobs/<job_id> \
-H "authorization: Bearer uq_live_..."
# -> { "status": "done", "result": { ...the personal context... } }
Prefer not to write curl by hand? Point an AI coding agent at /agents: it does this whole flow for you.
Want to see a full personal context before spending a credit?
GET /api/examples/kitchen-sink returns a no-key,
fictional static fixture ("Jordan Ellis",
mode:"synthetic_fixture"). It never performs a live
lookup and is not a trial enrichment.
Response shape & the detail param
Personal contexts default to full in production. Pass
?detail=compact explicitly for identity, the top-ranked
facts signals (each with its
evidence_quote + source_url), one
personality.how_to_communicate recommendation, the
compliance labels. It NEVER drops a field you would filter on ,
sensitivity_score,
special_category and verdict are in both
shapes. Pass ?detail=full explicitly (or send
"detail":"full" in the JSON body) to add the internal
ranking sub-scores.
Set detail explicitly to pin an
integration rather than depending on the production default. An
invalid detail value
returns 400 invalid_detail. Async
/api/enrich responses include a poll_url
that carries your resolved detail.
Every personal context carries ONE facts array: every fact we
surfaced, de-duplicated, each with its full score set,
category, source_url and
evidence_quote. The old split across
safe_to_reference / interest_signals /
professional_topics / communication_style /
public_context / weak_hints, the
band verdict, the evidence[] projection and
the capabilities block were all removed on 2026-08-03
(ADR-0025): the split hid about a third of the facts from anyone who
read the authoritative-sounding array, and band was
identical on 99.98% of facts.
Authentication
POST /api/signup starts an opaque, named email
verification transaction. Approval mints a revocable bearer key,
shown exactly once by the claim endpoint; signup never grants a live
enrichment credit. The
account is also accessible through the passwordless /app
dashboard and legacy /account flow, and keys can be
rotated without replacing the account. Send a key as
Authorization: Bearer <key> on every
authenticated call.
Keys are server-side secrets. Never put one in client-side/browser code. If a key ever leaks, rotate it immediately, see rotate-key below.
Programmatic clients should call /api/* directly:
website pages are bot-protected and may 403 a bot-like User-Agent,
but API routes aren't. Sending a descriptive
User-Agent naming your tool or agent is good API
citizenship and helps us debug, though it isn't required, default
library User-Agents work fine on /api/*.
Endpoints
Start named email verification for a new revocable key. New and returning emails receive the same opaque response shape. No key, account details, target, or live credit appears in this response.
curl -X POST https://uniquenessengine.com/api/signup \
-H 'content-type: application/json' \
-d '{"email":"you@work.com","client_name":"Clay workspace","device_name":"Production"}'
# 202
{
"status": "email_verification_required",
"transaction_id": "...",
"polling_token": "...",
"expires_at": "..."
}
# After deliberate approval, poll GET /api/signup/verification/:transaction_id
# and claim once with POST /api/signup/verification/:transaction_id/claim.
# Send x-verification-token: <polling_token> to both.
# A verification_delivery_failed 503 returns no polling secret: honor Retry-After
# and repeat this request to create a fresh transaction.
The core endpoint. Submit one person; a fresh enrichment usually runs
45–90s server-side (up to ~2–3 min), so this returns immediately
with a
job_id to poll. Accepts linkedin_url, or
name (+ optional company,
domain, title), or a work
email. One exact credit allocation is
reserved on submit and released if the personal context refuses
(needs_review) or the job fails: you are only charged
for a resolved personal context. Re-submitting the same person is deduped and
served from cache.
Profiles and public social posts across major public social and profile platforms are read from multiple independent public-data providers with automatic fallback, so a single provider outage doesn't drop coverage. When fewer than three verified personal facts remain, additional verified public-source lanes engage automatically before any paid source. Every candidate must pass the same identity and fact checks.
Discovery hard-denies FEC, court, obituary, wedding, and
people-search URLs, strips addresses, and drops any page that does
not name the target. Health, religion, politics, family/children,
and other sensitive categories are surfaced with a
category label, not blocked: see
sensitivity labels below. Only
home/physical addresses and private contact info are never surfaced.
curl -X POST https://uniquenessengine.com/api/enrich \
-H "authorization: Bearer uq_live_..." \
-H "content-type: application/json" \
-d '{"linkedin_url":"https://www.linkedin.com/in/<their-handle>"}'
# 202, queued, poll /api/jobs/:id
{ "job_id": "5be41fd7-...", "status": "queued" }
# 200, cache hit, personal context returned inline, no polling needed
{ "status": "done", "personal_context": { ... }, "brief": { ... } /* brief is deprecated alias */ }
Poll the job you submitted. Poll every ~5s. Terminal statuses are
done (personal context in result),
refused (returned a needs_review personal context, no
charge), and failed (error set, no
charge). queued / running mean keep
polling.
curl https://uniquenessengine.com/api/jobs/5be41fd7-... \ -H "authorization: Bearer uq_live_..." # 200, done, the resolved personal context { "status": "done", "result": { "status": "ok", "identity": { "full_name": "...", "title": "...", "company": "...", "identity_confidence": 0.92, "metadata_uncertain": false }, "facts": [ { "claim": "...", "evidence_quote": "quote from the source", "source_url": "...", "connection_type": "personal_signal", "category": "family_or_children", "special_category": false, "mentionability": 0.9, "confidence_label": "high", "sensitivity_score": 0.05, "verdict": "entailed" } ], "personality": { "archetype": "...", "disc_label": "...", "how_to_communicate": ["..."], "predicted": true }, "detail": "compact", "usage": { "cache_status": "hit|miss", "credits_charged": 1, "credits_remaining": 9 } } } # explicit compact example; production defaults to full # 200, refused, personal context returned but withheld; exact reservation released { "status": "refused", "result": { "status": "needs_review", "usage": { "credits_charged": 0 } } }
Health, recovery, religion, politics, marital status, and other
sensitive facts are surfaced: labeled with
category, not withheld; see
sensitivity labels. Only home/physical
addresses and private contact information are
never surfaced.
Compatibility alias for /api/enrich. It uses the same
paid admission, returns 200 {status:"done",brief} on a
cache hit, or 202 {job_id,status:"queued",poll_url} for
uncached work. New integrations should use
/api/enrich. GET with query params works the same as
POST with a JSON body.
curl -X POST https://uniquenessengine.com/api/brief \
-H "authorization: Bearer uq_live_..." \
-H "content-type: application/json" \
-d '{"linkedin_url":"https://www.linkedin.com/in/<their-handle>"}'
# -> 200 inline cache hit, or 202 job_id + poll_url
connection_type splits personal vs. professional
signal. Facts in facts carry a source link
and the quote they came from. discovered_handles
is a map of platform → public profile URL (present on compact and
full). aggregated_interests
contains broader interest signals, clearly marked as a read, not a
fact.
negative_preferences remains in the full schema but is
currently disabled and normally returns [].
Credit balance, billing status, and recent usage for your key.
curl https://uniquenessengine.com/api/account -H "authorization: Bearer uq_live_..."
# 200
{
"account_id": "...",
"credits_remaining": 8,
"subscription_credits": 0,
"grandfathered_credits": 2,
"service_recovery_credits": 1,
"purchased_credits": 5,
"total_credits": 8,
"debit_order": ["plan credits with the nearest expiry", "grandfathered credits",
"service-recovery credits", "oldest purchased credits"],
"recent_usage": [ { "endpoint": "/api/enrich", "cache_status": "hit", "credits_charged": 1, "status": "success" } ]
}
Revokes the key you authenticated with, immediately, and returns a
fresh one: same account, same balance. A human can run
uniqueness account rotate-key in a terminal; it asks
first, and --yes is the only non-interactive consent.
Do not have an agent run it or read the printed key. If the key is
lost, sign in by email through /account and rotate all
account keys.
curl -X POST https://uniquenessengine.com/api/account/rotate-key \
-H "authorization: Bearer <old key>"
# 200, old key is dead the instant this returns
{ "api_key": "uq_live_new...", "key_prefix": "uq_live_a1b2",
"message": "Old key is revoked immediately. This new key is shown once, save it now." }
Canonical, read-only pack and subscription terms consumed by the CLI and other clients. It contains no account, target, credential, Checkout action, or purchase.
curl https://uniquenessengine.com/api/billing/catalog
# -> { "commercial_contract":"hard-paid-v1", "packs":[...],
# "subscriptions":[...], "account_url":"https://.../account" }
Pack purchases and subscriptions begin from the passwordless Account page. These bearer endpoints save a payment method without enabling auto-recharge, or open Stripe's management portal.
# CLI discovery remains read-only until a human confirms a browser action
uniqueness batch preflight --file people.json --json
uniqueness batch submit --file people.json --json
uniqueness billing packs
uniqueness billing subscriptions
uniqueness billing status
uniqueness billing auto-recharge enable|disable|retry
uniqueness billing portal
curl -X POST https://uniquenessengine.com/api/billing/setup -H "authorization: Bearer uq_live_..." # -> { "setup_url": "https://checkout.stripe.com/..." } # Saving a method makes no purchase and never enables auto-recharge. # Auto-recharge is a separate, explicit opt-in in Account. curl -X POST https://uniquenessengine.com/api/billing/portal -H "authorization: Bearer uq_live_..." # -> { "portal_url": "https://billing.stripe.com/..." } (cancel, update card, invoices)
Service status. Deliberately minimal: no vendor/stack detail.
curl https://uniquenessengine.com/api/health
# { "ok": true, "service": "uniqueness-engine", "time": "..." }
Rate a personal context 👍 / 👎 so the engine keeps improving.
Pass rating ("up" or "down")
and the personal context's identity.canonical_id (or the
job_id), with an optional note. This is a
quality signal only: it never charges or refunds a credit.
curl -X POST https://uniquenessengine.com/api/feedback \
-H "Authorization: Bearer $UNIQUENESS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"rating":"up","canonical_id":"<from personal_context.identity.canonical_id (or brief.identity.canonical_id)>","note":"nailed the personal signal"}'
# { "ok": true }
Also available as the submit_feedback MCP tool and the
uniqueness feedback up|down --canonical-id <id>
CLI command.
Got a delivered personal context that missed? The account
owner can use Return credit from the passwordless
Account page or
POST /api/account/return-credit. This returns one
credit, never cash: once per delivered paid personal context within 90 days,
capped at 10 in a rolling 90 days, with a required reason. Refusals
and failed runs cost 0. Cash refunds remain support-controlled.
Use in Clay, Freckle & no-code tools
No-code enrichment tools send one HTTP request per row and can't
poll. Pass a callback_url and we POST the finished
personal context straight to that URL when it's ready: one column, no polling.
Clay (recommended: HTTP API → wait for webhook)
- Add an HTTP API enrichment column.
-
Method
POST, URLhttps://uniquenessengine.com/api/enrich, headerAuthorization: Bearer <your key>. -
Body:
{"linkedin_url": "{{LinkedIn URL}}", "callback_url": "<your Clay/webhook URL>"} -
We reply
202 {"status":"accepted"}, then POST the personal context to yourcallback_urlwhen it finishes (usually ~45–90s). Map the fields from the table below.
curl -X POST https://uniquenessengine.com/api/enrich \
-H "authorization: Bearer $UNIQUENESS_API_KEY" \
-H "content-type: application/json" \
-d '{"linkedin_url":"https://www.linkedin.com/in/<their-handle>","callback_url":"https://webhook.site/<id>"}'
# -> 202 { "status": "accepted", "delivery": "callback" }
# then, when ready, we POST to your callback_url:
# { "event":"brief.completed", "job_id":"...", "status":"ok",
# "input":{"linkedin_url":"..."}, "credits_charged":1,
# "credits_remaining":41, "brief":{ ...redacted personal context... } }
The callback_url must be a public https URL. We
never charge for a refusal (needs_review) or failure:
those still fire a callback (brief.refused /
brief.failed, brief:null) so your row
never hangs. If you set WEBHOOK_SIGNING_SECRET, each
callback carries X-UE-Timestamp +
X-UE-Signature: sha256=hmac(ts.body) to verify.
No webhook? Poll the job endpoint
Tools that can't receive a webhook should submit to
POST /api/enrich, then poll
GET /api/jobs/:id until terminal. Cache hits still
return inline. The deprecated /api/brief alias follows
that same pattern.
Field map: what to pull from the personal context
| Field | What it is |
|---|---|
status |
ok or needs_review |
identity.full_name / title / company /
identity_confidence
|
Who we resolved + confidence (0–1) |
facts[].claim / evidence_quote / source_url
|
The headline facts: each with a source link and the quote it
came from. Also connection_type,
mentionability, band.
|
aggregated_interests[].label / source |
Broader interest signals: a read, not a fact |
discovered_handles |
Social / web profiles we resolved (platform → URL). Present on compact and full. Logos are a client concern — the wire is just the map. |
personality.archetype / primary_disc / disc_label
|
The personality read: a read, not a fact, plus how to open |
negative_preferences[].entity / sentiment / use_case
|
Present in the full schema but currently disabled; normally
returns [].
|
credits_charged / credits_remaining (callback
envelope)
|
Spend tracking: on the callback wrapper, not the personal context |
facts[].category / special_category
|
What KIND of fact this is (e.g. family_or_children,
health, authored_work): see
sensitivity labels below.
|
facts |
Every fact we surfaced, in one array, with every score. |
Sensitivity labels
Every fact in facts (and in
public_context, where present) carries a
category: what KIND of fact it is. Most categories are
non-sensitive (hobby_public, alma_mater,
authored_work, and similar) and need no special
handling. A smaller set is
sensitive but still surfaced:
family_or_children, health,
religion, politics, legal,
financial_distress,
fundraiser_beneficiary, trauma_or_crisis,
protected_attribute, sexual_orientation,
and death_or_grief.
Sensitivity is a label, not a block. An
explicitly-stated sensitive fact (never inferred from a photo, name,
or proximity alone) is extracted, verified, and surfaced with its
category so you, the operator, can decide how to use
it, or whether to skip it. GDPR Art.9 categories
(health, religion, politics,
protected_attribute, sexual_orientation)
additionally carry special_category: true. Only two
categories are a hard block and never leave the server:
home_location (physical addresses) and
private_contact (phone/personal email/SSN): the
doxxing/physical-safety floor. The wrong-person identity gate is the
one other absolute lock; it is unrelated to sensitivity.
We label; we don't advise on use. The old
safe_to_use_in_outreach stamp has been removed from the
contract, along with the band verdict:
deciding what is "safe to send" put us in the position of advising
use, which is your call, not ours. Facts carry factual labels only
(category / special_category, attribution
tier, confidence band); how you use a fact is yours to decide,
informed by those labels. Publish-gating (what we surface) is not
use-gating (what you send).
{ "claim": "He coaches his daughter's soccer team on weekends.",
"evidence_quote": "I coach my daughter's soccer team on weekends",
"source_url": "...", "connection_type": "personal_signal",
"category": "family_or_children", "special_category": false,
"attribution_tier": "self_stated", "confidence_label": "high", "mentionability": 0.9 }
Billing and credits: the exact rules
| Question | Answer |
|---|---|
| Is there a free live trial? | No. Jordan Ellis is a fictional static fixture. Every real enrichment starts only when paid credit is available. |
| How do I start? | The one-time Intro 10 is ten credits for a $5 USD subtotal plus applicable tax, with no subscription. Larger credit packs are 20/$7.40, 50/$18.50, and 100/$37. |
| What costs a credit? |
A successful first unlock costs 1 credit, including a global
cache hit. A same-account re-pull costs 0 and reports
usage.cache_status:"hit" unless
refresh:true on a single
POST /api/enrich (CLI --refresh, MCP
refresh:true). Batch rejects refresh.
|
| What consumes no credit? |
needs_review, invalid input, auth errors, and
re-pulling a person your account has already unlocked before ,
even if that first pull happened weeks ago.
|
| Someone else looked up the same person first: free for me too? |
No. The engine's cache makes it fast and cheap for us, but your
account still pays once on your own first lookup of that person.
After that, it costs 0 for your account unless
refresh:true.
|
| Out of credits: what happens? |
402 on new lookups. Re-pulling someone you've
already unlocked still works unless refresh:true.
The response recommends one sufficient credit pack through
a short-lived Checkout action and links to Account. The unpaid
recovery path stores only HMAC
retry metadata, not the raw target.
|
| Is recharge automatic? |
No by default. Optional auto-recharge requires separate,
versioned consent for 20 credits/$7.40 plus applicable tax. A
saved card alone never enables it. It triggers only after an
exact delivered debit crosses from at least 3 credits to below
3; a failed attempt becomes action_required with no
automatic retry.
|
| Rate | Intro 10 is $0.50/credit (one-time); larger packs are $0.37/credit. Monthly plans remain Starter $15/mo (45 credits), Growth $40/mo (130 credits), and Pro $90/mo (300 credits). All prices are USD subtotals plus applicable tax. |
| Cancel |
Stripe Customer Portal: POST /api/billing/portal.
|
Rate limits
Per API key: 15 requests/10s burst, 100 requests/60s sustained on
the enrichment endpoints (/api/enrich,
/api/brief). Polling /api/jobs/:id is
cheap and not rate-limited on the same budget. Per IP: 5
signups/hour. A 429 means slow down: it doesn't help
to mint a new key.
Successful and throttled responses include RFC-style
RateLimit-Limit (for example 100;w=60).
Every 429 includes Retry-After in seconds.
Public probe endpoints such as GET /api/health expose
a separate 60;w=60 policy.
Errors
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_linkedin_url |
Not a linkedin.com/in/<handle> URL, or malformed input. |
| 400 |
invalid_email / email_not_allowed
|
Bad shape, or a reserved/disposable address. |
| 401 | unauthorized |
Missing/invalid/revoked key. |
| 402 | payment_required |
Canonical insufficient-credit response with exact required/available counts, an expiring sufficient pack offer, a billing URL, and idempotent retry metadata. |
| 429 | rate_limited |
See rate limits. |