Documentation

Everything the API does, in one page.

One core promise: paste a LinkedIn URL and get a fact-checked brief. 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 permanent 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 brief... } }

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 brief 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

Briefs default to full in production. Pass ?detail=compact explicitly for identity, the top-ranked safe_to_reference signals (each with its evidence_quote + source_url), one personality.how_to_communicate recommendation, the compliance labels, and a capabilities block. Pass ?detail=full explicitly (or send "detail":"full" in the JSON body) for the complete brief — every fact array, the ranking sub-scores, and the top-level evidence[].

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.

The capabilities block honestly declares what the API does NOT return: outreach_angles:"removed" and gift_hook:"disabled" (decommissioned), plus giftable_entities / negative_preferences (enabled|disabled per flag), the resolved detail, and full_available. The removed per-fact safe_to_use_in_outreach stamp is gone too — we verify and label facts; we don't tell you what's safe to send. Brief-body schemas and examples are derived from the serializer; endpoint envelopes, errors, and descriptive text are maintained separately. The complete contract is served at /openapi.json.

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

POST/api/signupno auth

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.
POST/api/enrichbearerasync

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 brief refuses (needs_review) or the job fails — you are only charged for a resolved brief. Re-submitting the same person is deduped and served from cache.

Profiles and public social posts (X, Instagram, TikTok, Threads) 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 -- brief returned inline, no polling needed
{ "status": "done", "brief": { ...the brief... } }
GET/api/jobs/:idbearer

Poll the job you submitted. Poll every ~5s. Terminal statuses are done (brief in result), refused (returned a needs_review brief, 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 brief
{
  "status": "done",
  "result": {
    "status": "ok",
    "identity": { "full_name": "...", "title": "...", "company": "...", "identity_confidence": 0.92, "metadata_uncertain": false },
    "safe_to_reference": [
      { "claim": "...", "evidence_quote": "quote from the source",
        "source_url": "...", "connection_type": "personal_signal",
        "category": "family_or_children", "special_category": false,
        "mentionability": 0.9, "band": "safe" }
    ],
    "personality": { "archetype": "...", "disc_label": "...", "how_to_communicate": ["..."], "predicted": true },
    "capabilities": { "outreach_angles": "removed", "gift_hook": "disabled", "detail": "compact", "full_available": true },
    "usage": { "cache_status": "hit|miss", "credits_charged": 1, "credits_remaining": 9 }
  }
}
# explicit compact example; production defaults to full

# 200, refused -- brief 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.

POST/api/briefbearerdeprecated alias

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 safe_to_reference carry a source link and the quote they came from. 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 [].

GET/api/accountbearer

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" } ]
}
POST/api/account/rotate-keybearer

Revokes the key you authenticated with, immediately, and returns a fresh one — same account, same balance. 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." }
GET/api/billing/catalogno auth

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" }
POST/api/billing/setup · /portalbearer

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 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)
GET/api/healthno auth

Service status. Deliberately minimal — no vendor/stack detail.

curl https://uniquenessengine.com/api/health
# { "ok": true, "service": "uniqueness-engine", "time": "..." }
POST/api/feedbackbearer

Rate a brief 👍 / 👎 so the engine keeps improving. Pass rating ("up" or "down") and the brief'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 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 brief 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 brief 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 brief straight to that URL when it's ready — one column, no polling.

Clay (recommended: HTTP API → wait for webhook)

  1. Add an HTTP API enrichment column.
  2. Method POST, URL https://uniquenessengine.com/api/enrich, header Authorization: Bearer <your key>.
  3. Body: {"linkedin_url": "{{LinkedIn URL}}", "callback_url": "<your Clay/webhook URL>"}
  4. We reply 202 {"status":"accepted"}, then POST the brief to your callback_url when 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 brief... } }

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 brief

Field What it is
status ok or needs_review
identity.full_name / title / company / identity_confidence Who we resolved + confidence (0–1)
safe_to_reference[].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
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 brief
safe_to_reference[].category / special_category What KIND of fact this is (e.g. family_or_children, health, authored_work) — see sensitivity labels below.
capabilities What the API deliberately does NOT return — outreach_angles:"removed", gift_hook:"disabled" — plus the resolved detail level. The removed safe_to_use_in_outreach stamp is declared here and never returned; we label facts, we don't advise on use.

Sensitivity labels

Every fact in safe_to_reference (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 (declared in capabilities, never returned) — 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", "band": "safe", "mentionability": 0.9 }

Billing & 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 repeatable 5-Pack is five permanent credits for a $5 USD subtotal plus applicable tax, with no subscription. Larger permanent 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 unless refresh:true.
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 permanent 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 The 5-Pack is $1/credit; 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.

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.