{"openapi":"3.1.0","info":{"title":"KingMinos by OutboundSync","version":"0.1.0","description":"Phase 1 + 1b skinny API. `company.resolve` (ZoomInfo + LeadMagic company-search),\n`person.verify_employment` (LeadMagic email validate; optional job-change + AI Ark;\noptional ZoomInfo `zoominfo_contact_id` append),     `company.domain` (integrator walk-through: `docs/company-domain.md` —\nhow to read a response, HubSpot / Cap field map, path ladder;\n`speed` → `balance` → `accuracy` → `coverage`),\n`company.hierarchy` (ZoomInfo-first\ncorporate family: immediate + ultimate parent + capped subsidiaries),\n`company.b2b_social` (B2B social company page — v1 LinkedIn\n`https://www.linkedin.com/company/{slug}` or null; house websearch +\nAI Ark (balance default) / LeadMagic (accuracy+) / Findymail (coverage);\nWiza is `routing.only` / BYOK only; SoT `docs/company-b2b-social.md`),\n`company.x_account` (company X / formerly Twitter account —\n`https://x.com/{handle}` or null; homepage-first + Wikidata +\nSerper X + house AI Ark; ZoomInfo TWITTER on accuracy/coverage BYOK;\nWiza / LeadMagic / Findymail off defaults; never GET x.com;\nSoT `docs/company-x-account-plan.md` until PR 6),\n`company.icon` (one hosted logo — house favicon-fetch →\nBrandfetch Brand API (prefer square icon; Free plan soft-miss) →\nZoomInfo scoped enrich `logo`; quality-gated raster stored on\n`logos.kingminos.com`; LeadMagic off the stamp path;\n`result.icon_url`),\nand `company.description` (first-acceptable English description —\nhits-first; hard rejects wrong company / junk/PII / empty;\nflags `hype` / `first_person` / `tagline` / `same_as_speed` /\n`truncated` / `cta` / `grammar`;\n`answer.confidence` high/medium/low is the write signal; `speed` homepage meta (no Jev) → AI\nArk; `balance` extracts + meta/og + AI Ark (Jev-ranked); `accuracy`\nxAI compose + one Jev under 10s, fallback to cheaper hit;\n`coverage` accuracy compose + off-homepage Serper / ZI BYOK,\nfalling back to the accuracy winner; AI Ark is the only\nfirmographic vendor (no LeadMagic); SoT `docs/company-description.md`).\nInternal SFDC Sales Env first. Not a public Enrich-CRM claim.\n\n`person.language` answers the outbound question — can we engage this\nperson in English, and if not, in what language? — from an email or\nLinkedIn URL (`english`, `recommended_language`, `local_language`,\n`detected`, ranked `languages`). API tenants only; not in\n`openapi.es.yaml`. SoT `docs/person-language.md`.\n\nResponse field names match the live Worker JSON serializers (`RunResponse`, `toRunResponse`, `publicResult`).\nWorker contract. For Salesforce External Services / Flow import, use\nrepo-root `openapi.es.yaml` (closed OpenAPI 3.0 subset for Account\n`company.resolve` and `company.domain`). Importing this full file into ES collapses\ncomplex schemas to Type Any (Active Objects 0).\n\nSalesforce Named Credential / Custom auth: Auth Parameters do not leave\nSalesforce. Custom Headers on the External Credential must list\nAuthorization. Guide: `docs/salesforce-named-cred.md`. Import\n`openapi.es.yaml` into External Services — do not import this file.\n\nConsumer contract (schema_version 2): automations branch on `answer.outcome`\nfor verify (`high_confidence_send` | `low_confidence_send` |\n`low_confidence_block` | `high_confidence_block` | `no_decision`). Catch-all /\nSEG / role / employment live in `answer.summary` and `result.email_validation`\n— not fifth outcomes. Layer-2 `result.employment` remains `still_there` |\n`left` | `unknown`. Optional `result.zoominfo_contact_id` is enrichment after\nthat decision (`continueForContactId`) — not required for `es_decision: hit`.\nSalesforce / stamp-safe branching uses `es_decision` (`hit|miss|ambiguous|error|noop|reject`).\n`reject` is for `company.domain` hygiene refusals. Existing Account Flows\nthat branch `hit / miss / else` treat `reject` as `else` (do nothing).\nHarris opened ES for `company.domain` on 2026-09-26 (`openapi.es.yaml`\n`operationId: companyDomain`).\nCurrent enrich is default / `balance` (legacy `auto`): one callout — fill Account Name from\n`result.company_name` when present (including on `es_decision=miss`); stamp\n`result.zoominfo_company_id` when `es_decision=hit` and the id is non-blank\n(websearch name + ZI id on the same answer). A websearch-only name may\narrive on `miss`. Stamp from `es_decision` + result fields + `answer.confidence`.\nOutboundSync sets `routing.path` on company.resolve\n(`speed` | `balance` | `accuracy` | `coverage`; omit = `balance`) and\nperson.verify_employment (`speed` | `balance` | `accuracy`; omit = `balance`).\nLegacy `value` / `auto` / `fast` / `name_only` are accepted on company\ncapabilities and expand to canonical paths in responses. Employment request\n`value` / `auto` / `coverage` / `fast` / `name_only` are `400 invalid_routing`.\n`routing.preset` is a back-compat alias on company (`name_only` → `speed`).\n`zi_stamp` is optional ZoomInfo-first (company). `confidence` is not a path.\n`result.company_domain` is optional hygiene — never clobber.\nNested `evidence` and `trace.attempts` are audit/debug (`explain=full`).\nBusiness misses stay HTTP 200.\n\n401 ErrorBody `detail`: `missing` | `malformed` | `mismatch` (plus a\nstatic `hint`; never echoes the key). Bearer only; bare keys are\n`malformed`.\n\nPasswordless KingMinos login lives on this Worker (the user row is the\nKingMinos account — not an OutboundSync identity). `POST /v1/auth/login`\nemails a one-time code through SendGrid when `SENDGRID_API_KEY` is set\nand SendGrid accepts the send. A successful send returns\n`code_delivery: email` plus `sent: true`, `code_sent: true`, and\n`status: sent` so the live website can confirm delivery. From address\nis the verified sender `support@outboundsync.com` (from name\nOutboundSync). Production fails closed if the key is missing or\nSendGrid does not accept the send —\nit does not pretend a code was sent. Localhost or\n`ENVIRONMENT=development` may skip send when the key is unset and may\n`POST /v1/auth/verify` without a code. Production verify requires the\nemailed code. Wrong codes are counted. After five failures in fifteen\nminutes the code is deleted and the email is locked out. Auth routes\nshare the ingress limiter. Login attempts store success and failure\n(never the plaintext code). Expired codes and sessions are swept on\ncron. `POST /v1/auth/logout-all` revokes every session for the\nsigned-in person. Verify creates the person and their first workspace.\nThe product app at `https://app.kingminos.com` may call this API with\ncredentials; CORS is that origin only (never `*`).\n`GET /v1/auth/me` and `GET /v1/account`\nreturn the signed-in user plus the current workspace credit balance\nand KingMinos subscription status (`none` | `trialing` | `active`).\nA workspace is the company. A person can belong to more than one\nand switches the session into one. Tokens, history, and credit spend\nbelong to that workspace — a token cannot read or spend another.\nThe creator is owner; an owner invites by email. The Worker stores\na SHA-256 of a one-time invite token and emails the plaintext\nthrough SendGrid (`support@outboundsync.com` / OutboundSync).\nAccepting requires that token. Roles are owner and member. A new\nuser gets one workspace they own. A new workspace starts with a\ncapped trial credit balance of 100 (not a price). The person is\nnot the meter. Checkout / Stripe charge is later — this Worker\ndoes not call Stripe. Personal access tokens are user-owned `km_`\nkeys with an explicit capability scope and a daily credit cap\n(default 100; not wildcard-by-default). `POST /v1/account/tokens`\nreturns the raw bearer once. Owner manages tokens. Member can see\nthat workspace's usage. Stored notifications cover workspace\nerrors and their resolutions plus workspace `credits_exhausted`\nand account-level `invite`. A successful call does not notify.\nLogin does not mint or return a router API Bearer. Production\ndoes not sign in without a code, including on localhost, and does\nnot store an invite without sending mail.\n\nConsumer layers:\n- Control: `answer.outcome`, `es_decision`, `answer.confidence`\n- Explain: `answer.summary`, `sources` (not control flow)\n- Agent guidance: `next_steps[]` on completed runs; `candidates[]` on a\n  miss that had candidates (default explain — do not require `trace`).\n  Workflow catalog: `GET /v1/recipes`. Agent brief: `docs/for-ai-agents.md`.\n- company.domain explain object: `trace.explain` when `X-Router-Explain` /\n  `?explain=` is `full` (not control flow)\n- Audit: `trace.attempts` and `result.evidence` when `explain=full`\n\nCredits: you are only charged when an answer is delivered. A miss, error,\ntimeout, ambiguous, or below-floor withhold bills 0. `routing.max_provider_credits`\nis a **reservation ceiling** (budget gate before hops).\n`usage.credits.spent` / `usage.credits.by_provider` on a completed run is the **reconciled bill**.\ncompany.domain `speed` / `balance` reservation is **0** (intentional). ZoomInfo company\nsearch is ledger **0** — `by_provider.zoominfo: 0` means the hop ran and billed 0, not\n“did not run.” Use `usage.providers_ran` and `usage.hops[].status` (`ok` | `empty` |\n`skipped` | …) to see execution. Operations advertise `billing_mode` as hop\ntelemetry (`always` vs `on_success`); that is not the customer bill.\nFree ZoomInfo search hops still run when the cap is `0`.\n\nEnvelope pin: optional `X-Router-Schema` / query `schema` / body `schema_version`.\nOmit = current tip (`schema_version` 2). Pin `2` locks this envelope. Unknown pin\nis `400 unsupported_schema` (run not started). Header, query, and body must agree.\n\nField/attempt endings (pipe0-style vocabulary → router):\n- completed → successful `decision.outcome` (resolved / high_confidence_send / low_confidence_send / low_confidence_block / high_confidence_block)\n- no_result → `no_decision` with reasons like `zero_hits` / `personal_mailbox` / `no_domain`\n- failed → `provider_error` → `es_decision: error`\n- skipped → attempt `status: skipped` (`max_credits`, `not_configured`, `excluded_by_routing`, `compliance_blocked`, `status_down`, `rate_limited`)\n\n`X-Router-Explain` or query `explain`: `default` (omit) is the clean envelope\n(`answer`, flats, `sources`, `usage`, `input`, `routing`, `provenance`,\n`next_steps`, optional `candidates` — no `trace`). `full` adds `trace`\n(decision, attempts, candidates, routing, writer, explain). `minimal` is an\nalias of `default` so existing Flow `X-Router-Explain: minimal` keeps working.\nHeader and query must agree when both are set. A JSON body field `explain`\nis `400 invalid_explain` — use the\nheader or query, not the body.\n\nZoomInfo credentials: `400 zi_credentials_required` only when the caller\nasked for ZoomInfo (`zi_stamp` / `routing.only` includes `zoominfo` /\nZoomInfo-first effective order) and no credentials decrypt. Default\n`auto` / `balance` / `value` where ZoomInfo is a later hop skip\n`not_configured` (HTTP 200, no warning).\n\n`company.domain` result cache: keyed on `capability_version` + schema +\nnormalized input + path (deploy misses prior trust-v1 rows). Eval bypass:\n`X-Router-Cache: bypass`, `Cache-Control: no-cache|no-store`, or body\n`skip_cache: true`. Bypass skips cache read only. Janus first-100 re-smoke\nmust also omit or rotate `Idempotency-Key` or the response is\n`replayed: true` with the old decision.\n\n`GET /v1/credentials` and `PUT` / `DELETE /v1/credentials/{provider}`\nstore workspace or tenant Your Keys after a vendor auth check.\n(ZoomInfo OAuth + LeadMagic / Findymail / Wiza / AI Ark / BuiltWith) in\nthe existing D1 AES-GCM envelope. Responses never echo the raw key.\nWebsearch is house-only. Not in `openapi.es.yaml`.\n\n`DELETE /v1/subjects/{subject_key}` is tenant-scoped erasure (entities,\nidentifiers, runs subtree, matching R2 `raw/…/{run}/` objects). Requires\nthe `erase` key scope — Flow capability keys cannot delete. Not in\n`openapi.es.yaml`. Scheduled retention: expired `resolve_cache` is always\nswept; runs / entities older than `DATA_RETENTION_DAYS` (30 in production).\nCredit ledger rows are purged only after `max(DATA_RETENTION_DAYS,\nLEDGER_MIN_RETENTION_DAYS)` (7-year floor). R2 `raw/` lifecycle is\ndashboard-owned (30 days); see `HARDENING.md`. Expired `description_cache`\nrows are swept too.\n\nPrivacy requests (docs/privacy-requests.md): person endpoints return `451\nsubject_suppressed` for an `email` / `linkedin_url` on the global\nsuppression list (0 credits, no run). Operator admin routes (session or\n`kmadm_` token; not in `openapi.es.yaml`): `POST /v1/admin/privacy/erase`,\n`GET /v1/admin/privacy/suppressions`, `GET /v1/admin/privacy/suppressions/check`,\n`DELETE /v1/admin/privacy/suppressions/{id}`. Digests only — never raw emails\nor URLs unless the table already stores them.\n\nLive spec (auth-free, OutboundSync-shaped): `GET /openapi.json` and\n`GET /openapi.yaml` (also `/v1/openapi.json` / `/v1/openapi.yaml`). YAML\nin this repo is the checked-in source of truth; JSON is that document\nparsed. `/v1/*` responses carry RFC 8631 `Link` (`rel=service-desc` +\n`rel=service-doc`). Public hostname: `https://api.kingminos.com` on Worker\n`kingminos-api-prod`. Product/CF edge is KingMinos, not OutboundSync\nRouter. `/health` is unversioned (OutboundSync-style). A future\n`/api/v1/` prefix is a follow-up — do not invent it here.\n`*.workers.dev` is not the public contract.\n"},"servers":[{"url":"https://api.kingminos.com","description":"KingMinos production API. `/health` is unversioned."},{"url":"http://127.0.0.1:8790","description":"Local `wrangler dev` only (not production)."}],"paths":{"/health":{"get":{"operationId":"getHealth","summary":"Liveness","responses":{"200":{"description":"Worker is up. Body is `{ ok, service, version, git_sha, billing_mode? }`. `git_sha` is null unless `GIT_SHA` is set on the Worker. `billing_mode` is `test` or `live` when Stripe card billing is enabled (`src/stripe.ts`); omitted when billing is unavailable (no key, or live key without `STRIPE_LIVE_BILLING=on`). Never includes key material."}}}},"/openapi.json":{"get":{"operationId":"getOpenApiJson","summary":"OpenAPI 3.1 document (JSON)","description":"Parsed form of repo-root `openapi.yaml`. Also available at `/v1/openapi.json`.","responses":{"200":{"description":"OpenAPI document","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/openapi.yaml":{"get":{"operationId":"getOpenApiYaml","summary":"OpenAPI 3.1 document (YAML)","description":"Same file as repo-root `openapi.yaml`. Also available at `/v1/openapi.yaml`.","responses":{"200":{"description":"OpenAPI document","content":{"application/yaml":{"schema":{"type":"string"}}}}}}},"/v1/openapi.json":{"get":{"operationId":"getOpenApiJsonV1","summary":"OpenAPI 3.1 document (JSON)","responses":{"200":{"description":"OpenAPI document","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/v1/openapi.yaml":{"get":{"operationId":"getOpenApiYamlV1","summary":"OpenAPI 3.1 document (YAML)","responses":{"200":{"description":"OpenAPI document","content":{"application/yaml":{"schema":{"type":"string"}}}}}}},"/v1/auth/login":{"post":{"operationId":"startLogin","summary":"Start passwordless login","description":"Emails a one-time code through SendGrid from the verified OutboundSync sender. Does not create the person until verify. Does not set a session and does not mint an API Bearer. Production fails closed when the SendGrid key is missing or the send is not accepted. A successful send sets `sent` / `code_sent` / `status: sent` in addition to `code_delivery: email`. Development / localhost may skip send when the key is unset (`code_delivery: not_wired`). Invites are not emailed.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string"}}}}}},"responses":{"200":{"description":"Code sent (`code_delivery: email` plus `sent` / `code_sent` / `status: sent`), or development skip when send is not configured (`code_delivery: not_wired`).\n","content":{"application/json":{"schema":{"type":"object","required":["ok","next","code_delivery"],"properties":{"ok":{"type":"boolean","enum":[true]},"next":{"type":"string","enum":["code"]},"code_delivery":{"type":"string","enum":["email","not_wired"]},"sent":{"type":"boolean","description":"True only after SendGrid accepted the mail."},"code_sent":{"type":"boolean","description":"Alias of sent for the live website client."},"status":{"type":"string","enum":["sent"],"description":"Present only after SendGrid accepted the mail."},"message":{"type":"string","description":"Present on the development skip path."}}}}}},"400":{"description":"Invalid email"},"429":{"description":"Rate limited or email locked out after failed guesses"},"502":{"description":"SendGrid did not accept the send"},"503":{"description":"Production send is not configured"}}}},"/v1/auth/verify":{"post":{"operationId":"verifyLogin","summary":"Complete passwordless login","description":"Accepts the emailed one-time code, then creates the KingMinos person and their first workspace. Wrong or expired codes return 401 and are stored as failures (hash of the code is never stored on the attempt). After five failures in fifteen minutes the code is dead and the email is locked out (`429 login_locked`). Localhost / development may establish an httpOnly session without a code. Production refuses the bypass and does not pretend a code was sent. Session cookie is HttpOnly, SameSite=Lax, Secure except localhost. `app.kingminos.com` and `api.kingminos.com` are same-site (eTLD+1); credentialed fetch still needs CORS on the app origin, never `*`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string"},"code":{"type":"string"}}}}}},"responses":{"200":{"description":"Session cookie set"},"401":{"description":"Invalid or expired code"},"403":{"description":"Production bypass refused"},"429":{"description":"Rate limited or email locked out after failed guesses"}}}},"/v1/auth/me":{"get":{"operationId":"getSessionUser","summary":"Signed-in KingMinos user","description":"Reads the httpOnly session cookie. Does not accept an API Bearer as login.","responses":{"200":{"description":"Current user"},"401":{"description":"Not signed in"}}}},"/v1/auth/intercom-token":{"get":{"operationId":"getIntercomMessengerToken","summary":"Intercom Messenger identity JWT","description":"Session cookie only. Returns a short-lived HS256 JWT for Intercom Messenger identity (user and active workspace). API Bearer keys are rejected. Requires `INTERCOM_MESSENGER_SECRET` on the Worker; otherwise 503 with no token.\n","responses":{"200":{"description":"`{ token }` HS256 JWT: user_id, email, company (company_id, name, optional plan), optional role, optional signed `stripe_customer_id` (user) and `company.workspace_stripe_customer_id` (workspace billing). `salesforce_id` omitted until stored. Aligns with OutboundSync Intercom JWT shape (PR #965).\n"},"401":{"description":"Not signed in"},"403":{"description":"Session required (API key auth not allowed)"},"503":{"description":"Intercom signing unavailable (secret unset or error)"}}}},"/v1/auth/logout":{"post":{"operationId":"logout","summary":"End the session","description":"Deletes the current session cookie only.","responses":{"200":{"description":"Session cookie cleared"}}}},"/v1/auth/logout-all":{"post":{"operationId":"logoutAll","summary":"Revoke every session","description":"Signed-in only. Deletes every session for that person and clears the current cookie. Does not mint or revoke API keys.\n","responses":{"200":{"description":"All sessions revoked"},"401":{"description":"Not signed in"}}}},"/v1/account":{"get":{"operationId":"getAccount","summary":"Signed-in KingMinos account","description":"Session cookie or the current workspace token. Returns the user, the current workspace credit balance, and subscription status. Status `none` is not active. No Stripe call. The person is not the meter. `free_credits` says whether this workspace earns free credits (welcome, onboarding, daily 10): `{ eligible, reason, message }`, reason one of `eligible`, `personal_email`, `disposable_email`, `domain_already_used`, `email_unverified`, `under_review`. Free credits need a verified work email and go to the first workspace on a company domain only; anyone can still buy credits. A 402 `out_of_credits` carries the same object in `details.free_credits` when the workspace is not eligible.\n","responses":{"200":{"description":"Account"},"401":{"description":"Not signed in"}}}},"/v1/account/history":{"get":{"operationId":"listAccountHistory","summary":"Recent calls for the signed-in account","description":"Session cookie or a token for the current workspace. Returns that workspace's runs from the existing run log: endpoint, capability, outcome, reason, error, dry_run, latency_ms, time, succeeded, credits spent. The same array is emitted as both `calls` (documented name) and `history` (live app.kingminos.com parser). Optional `before=created_at|run_id` keyset cursor (newest-first). A token cannot read another workspace.\n","parameters":[{"name":"before","in":"query","required":false,"schema":{"type":"string"},"description":"Keyset cursor `created_at|run_id` of the last row from the previous page."}],"responses":{"200":{"description":"Recent calls"},"400":{"description":"Invalid cursor"},"401":{"description":"Not signed in"}}}},"/v1/account/workspaces":{"get":{"operationId":"listAccountWorkspaces","summary":"List workspaces on the account","description":"Lists companies the signed-in person belongs to. Session has a current workspace. Tokens, history, and credit spend belong to that company. A token only sees its own workspace.\n","responses":{"200":{"description":"Workspaces"},"401":{"description":"Not signed in"}}},"post":{"operationId":"createAccountWorkspace","summary":"Create a workspace","description":"Creates a company workspace. The creator is owner. The new workspace starts with the same trial balance of 100. Does not mint a second key system. Does not send email.\n","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64}}}}}},"responses":{"201":{"description":"Workspace created"},"400":{"description":"Invalid workspace name"},"401":{"description":"Not signed in"}}}},"/v1/account/workspaces/{id}":{"patch":{"operationId":"patchAccountWorkspace","summary":"Rename a workspace","description":"Owner only. Session cookie required. Name is 1–64 printable characters and may not contain a URL. Same name rules as create.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":64}}}}}},"responses":{"200":{"description":"Workspace renamed"},"400":{"description":"Invalid workspace name"},"401":{"description":"Not signed in"},"403":{"description":"Owner or session required"},"404":{"description":"Workspace not found for this person"}}}},"/v1/account/workspaces/{id}/switch":{"post":{"operationId":"switchAccountWorkspace","summary":"Switch the session into a workspace","description":"Sets the current workspace on the session. Session cookie only. A personal access token cannot switch. The person must already be a member of that company.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Session now uses that workspace"},"401":{"description":"Not signed in"},"403":{"description":"Token cannot switch"},"404":{"description":"Workspace not found for this person"}}}},"/v1/account/members":{"get":{"operationId":"listAccountMembers","summary":"List members of the current workspace","description":"Owner and member can both see who belongs to the current company. Roles are owner and member only.\n","responses":{"200":{"description":"Members"},"401":{"description":"Not signed in"}}}},"/v1/account/members/{user_id}":{"delete":{"operationId":"deleteAccountMember","summary":"Remove a member from the current workspace","description":"Owner only. Session cookie required. Revokes that person's `km_` tokens for this workspace. Sessions pointed at this workspace fall back to their next remaining workspace. The last owner cannot remove themselves (`409 last_owner`).\n","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Member removed"},"401":{"description":"Not signed in"},"403":{"description":"Owner or session required"},"404":{"description":"Member not found in this workspace"},"409":{"description":"Last owner cannot leave"}}}},"/v1/account/invites":{"get":{"operationId":"listAccountInvites","summary":"List pending outgoing invites for the current workspace","description":"Owner only. Pending invites this company stored (email, id, created time). Token hash is never returned. A member gets `owner_required`.\n","responses":{"200":{"description":"Outgoing pending invites"},"401":{"description":"Not signed in"},"403":{"description":"Owner required"}}},"post":{"operationId":"createAccountInvite","summary":"Invite a person to the current workspace","description":"Owner only. Mints an invite token, stores SHA-256, and emails an accept link `https://app.kingminos.com/app/invite/#invite={id}&token={token}` plus the token as fallback through the existing SendGrid path. Invites expire after 7 days. Production without a SendGrid key is `503 invite_delivery_unavailable`. A failed send deletes the row (`502 invite_delivery_failed`). The response never includes the token. If the person already exists, an account-level `invite` notice is stored for them.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string"}}}}}},"responses":{"201":{"description":"Invite stored and emailed (or not_wired in development)"},"400":{"description":"Invalid invite"},"401":{"description":"Not signed in"},"403":{"description":"Owner required"},"409":{"description":"Already a member or invite pending"},"502":{"description":"Invite email failed"},"503":{"description":"Invite email is not configured"}}}},"/v1/account/invites/incoming":{"get":{"operationId":"listIncomingAccountInvites","summary":"List incoming workspace invites","description":"Pending invites addressed to the signed-in email. Accept is `POST /v1/account/invites/{id}/accept` with the emailed token and only works for an invite to that email.\n","responses":{"200":{"description":"Incoming invites"},"401":{"description":"Not signed in"}}}},"/v1/account/invites/{id}/accept":{"post":{"operationId":"acceptAccountInvite","summary":"Accept a workspace invite with the emailed token","description":"The signed-in email must match the invite. The body token must match the stored hash. They become a member of that company. Does not switch the session. Expired invites (7-day TTL) return `invalid_invite`.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"}}}}}},"responses":{"200":{"description":"Membership created"},"400":{"description":"Invite token required or invalid"},"401":{"description":"Not signed in"},"404":{"description":"Invite not found for this email or token"}}}},"/v1/account/invites/{id}":{"delete":{"operationId":"deleteAccountInvite","summary":"Revoke a pending workspace invite","description":"Owner only. Session cookie required. Revokes a pending invite for the current workspace. Accepted, expired, or another workspace's invite is `404`.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Invite revoked"},"401":{"description":"Not signed in"},"403":{"description":"Owner or session required"},"404":{"description":"Pending invite not found"}}}},"/v1/account/notifications":{"get":{"operationId":"listAccountNotifications","summary":"List stored account notifications","description":"Durable notifications for workspace errors (`error`) and their resolutions (`error_resolved`), plus workspace `credits_exhausted`. Session also sees account-level events that are not one workspace (`invite`). Not mixed into the call log. A token only sees its workspace. No email and no webhook on successful calls.\n","responses":{"200":{"description":"Notifications"},"401":{"description":"Not signed in"}}}},"/v1/account/tokens":{"get":{"operationId":"listAccountTokens","summary":"List personal access tokens","description":"`km_` tokens for the current workspace. Prefix, id, explicit scopes, daily credit cap, and last_used_at. Never returns a raw bearer. Owner manages tokens. A token cannot read another workspace.\n","responses":{"200":{"description":"Tokens for the account"},"401":{"description":"Not signed in"}}},"post":{"operationId":"createAccountToken","summary":"Create a personal access token","description":"Owner only. Stores SHA-256 hash + prefix on the current workspace. Default scope is the sold capabilities (not `*`). Default daily credit cap is 100. Returns the raw bearer once in this response. Not a tenant key and not a master key.\n","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string"},"scopes":{"type":"array","description":"Capability ids, `capability:*` (every capability), `webhooks` (webhook management; never implied by `capability:*`), and/or `billing:read` (read-only `GET /v1/balance`; never implied by `capability:*`). Omit for the default set (every capability plus `billing:read`).","items":{"type":"string"}},"daily_credits_cap":{"type":"integer","nullable":true}}}}}},"responses":{"201":{"description":"Token created; bearer shown once"},"401":{"description":"Not signed in"}}}},"/v1/account/tokens/{id}/rotate":{"post":{"operationId":"rotateAccountToken","summary":"Rotate a personal access token","description":"Replaces the stored hash immediately. The old bearer stops working. The new raw bearer is returned once.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Token rotated; bearer shown once"},"401":{"description":"Not signed in"},"404":{"description":"Token not found for this workspace"}}}},"/v1/account/tokens/{id}":{"delete":{"operationId":"deleteAccountToken","summary":"Delete a personal access token","description":"Owner only. The bearer stops working immediately. Does not delete the workspace or its credit balance.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Token deleted"},"401":{"description":"Not signed in"},"404":{"description":"Token not found for this user"}}}},"/v1/balance":{"get":{"operationId":"getBalance","security":[{"bearer":[]}],"summary":"Workspace credit balance (read-only; docs/balance-and-usage.md)","description":"Session cookie or a `km_` token that explicitly carries the `billing:read` scope (`capability:*` does not imply it; the default mint set includes it). Customer `osr_` tenant keys get `401 not_signed_in`. Strictly read-only: it never grants the daily free credits, sweeps expired buckets, or touches a key; when today's free credits are still ungranted it reports `free_daily.will_grant_on_next_call`. Returns `available`, `in_flight`, `total`, live `by_kind[]` in draw order, `buckets[]` (workspace owner or session only; never `source_ref`), `expiring`, `expired_unswept`, `free_daily`, `plan` (subscription, PAYG), `next_refill` / `refills[]` (subscription refill uses stored `current_period_end` when it is still in the future), and `key` for `km_` callers (`spent_last_24h` is net of refunds). `used_this_cycle_usd_estimate` is a list-price estimate, not an invoice figure.\n","responses":{"200":{"description":"Balance snapshot"},"401":{"description":"not_signed_in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"billing_scope_required (`details.required_scope` is `billing:read`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"rate_limit_exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/balance/ledger":{"get":{"operationId":"getBalanceLedger","security":[{"bearer":[]}],"summary":"Workspace credit ledger (read-only; docs/balance-and-usage.md)","description":"Same auth as `GET /v1/balance` (session or a `km_` token with `billing:read`). Newest first. `delta` is signed: debits, expiries, subscription lapses and refund clawbacks are negative; grants and refunds positive. `source` is an allow-listed enum (`free_daily`, `trial`, `onboarding`, `promo`, `admin_grant`, `subscription`, `subscription_lapse`, `payg_line`, `partner_payout`, `run`, `expiry`, `refund_clawback`, `other`) and `ref` a safe id (run id, cycle, bucket kind) or null; raw memos are never returned. Filters: `since` / `until` (ISO-8601 UTC, `until` defaults to now), repeatable `kind` (`credit` | `debit` | `adjustment`), `key_id`, `user_id`. Page with `limit` (default 50, max 200) and the `next_cursor` value as `before`. A member key sees only its own run rows (`visibility: \"self\"`); owner and session see all.\n","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"before","in":"query","schema":{"type":"string"},"description":"Cursor `<created_at>|<id>` from `next_cursor`"},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"kind","in":"query","style":"form","explode":true,"schema":{"type":"array","items":{"type":"string","enum":["credit","debit","adjustment"]}}},{"name":"key_id","in":"query","schema":{"type":"string"}},{"name":"user_id","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Ledger page"},"400":{"description":"invalid_cursor, invalid_since, invalid_until, or invalid_param","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"not_signed_in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"billing_scope_required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"rate_limit_exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/balance/history":{"get":{"operationId":"getBalanceHistory","security":[{"bearer":[]}],"summary":"Workspace balance time series (read-only; docs/balance-and-usage.md)","description":"Same auth as `GET /v1/balance`; workspace owner or session only (`403 owner_required` for a member key). UTC-aligned `day` (default, span up to 400 days) or `hour` (span up to 7 days) points with `opening`, `closing`, `credits_in`, `credits_out`, `expired`, `adjustments`, `charged_calls`, plus a window `summary`. `closing` is the `balance_after` of the last ledger row before the point ends, carried forward on empty buckets. Balances exclude in-flight holds. Defaults: `until` = now; `since` = now − 30 days for `interval=day`, now − 7 days for `interval=hour`.\n","parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"interval","in":"query","schema":{"type":"string","enum":["day","hour"],"default":"day"}}],"responses":{"200":{"description":"History points and summary"},"400":{"description":"invalid_since, invalid_until, invalid_param, or invalid_window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"not_signed_in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"billing_scope_required or owner_required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"rate_limit_exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/balance/forecast":{"get":{"operationId":"getBalanceForecast","security":[{"bearer":[]}],"summary":"Workspace runout forecast (read-only; docs/balance-and-usage.md)","description":"Same auth as `GET /v1/balance` (session or a `km_` token with `billing:read`); a member key sees the same workspace-wide numbers. `method` is `trailing_average`: `rates.d1` / `d7` / `d30` are `run:` debit credits per day (expiry, lapse, and clawback are not spend), divided by the window length or the workspace age when younger. `primary_window` is `d7`, `d30` when d7 has fewer than 20 charged calls, or `d1` for a workspace under 2 days old; `confidence` is `low` below 20 charged calls on that window. `runout_at` assumes NO refills (conservative) and accounts for buckets expiring before the draw reaches them; `runout_with_refills_at` adds the projected refills (`next_refill`: free daily, PAYG line, subscription at `current_period_end`). `shortfall_before_refill` is measured to `next_major_refill` (subscription / PAYG line) when there is one. `status` is `exhausted`, `frozen`, `critical` (&lt; 1 day), `low` (&lt; 3 days) or `ok`. A free-daily-only workspace keeps the conservative `runout_at` but takes its status from the with-refills projection. Frozen paid credits are not burnable. The projection looks at most 400 days ahead.\n","responses":{"200":{"description":"Rates, runouts, next refills, and status"},"401":{"description":"not_signed_in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"billing_scope_required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"rate_limit_exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/balance/health":{"get":{"operationId":"getBalanceHealth","security":[{"bearer":[]}],"summary":"Workspace balance health (read-only; docs/balance-and-usage.md)","description":"Same auth as `GET /v1/balance`. A cheap status read: `status` (`ok` | `low` | `critical` | `exhausted` | `frozen`, same rules as the forecast), `available`, the conservative `runout_at`, `next_refill_at`, and `advice`, a fixed English string from a short enum (zero spend, steady, spend spike, raise the monthly limit for a PAYG workspace, add credits, or fix billing when paid credits are frozen). With `?capability=<id>` the response adds `can_afford`, `capability`, and `required_credits` (the cheapest paid charge for that capability's default routing vs the spendable balance); nothing is reserved. When that default routing cannot be priced the fields stay `null` and `pricing` is `unknown` (never reported as free).\n","parameters":[{"name":"capability","in":"query","schema":{"type":"string"},"description":"Capability id, e.g. company.resolve"}],"responses":{"200":{"description":"Status, runout, next refill, advice"},"400":{"description":"invalid_param (unknown capability)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"not_signed_in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"billing_scope_required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"rate_limit_exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/usage":{"get":{"operationId":"getUsage","security":[{"bearer":[]}],"summary":"Workspace usage aggregate (read-only; docs/balance-and-usage.md)","description":"Same auth as `GET /v1/balance` (session or a `km_` token with `billing:read`). A call is one non-dry-run run by `created_at`; dry runs are counted separately as `dry_runs`. A charged call has `charged_credits > 0` (rows without that stamp fall back to the `provider_credits_json` sum); status never decides it, so a failed run released by the stale-hold sweep can still be charged. No-charge calls are bucketed `miss` / `cached` / `failed` / `withheld` (`below_min_confidence` only) / `other`. `totals.ledger_debits` sums `run:` debits in the same window for reconciliation. `group_by` is a comma list of up to two of `capability`, `provider`, `key`, `user`, `day`, `outcome`, `reason`; provider groups report `attributed_credits` (per-provider spend, not always equal to charged credits). Groups are sorted by credits descending, capped at 500 (`truncated`). Default window is the last 7 days; the span is at most 30 days and `since` cannot be older than the data retention window (`400 window_out_of_retention`, `details.max_since`). A member key sees only its own runs (`visibility: \"self\"`).\n","parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"group_by","in":"query","schema":{"type":"string"},"description":"Comma list, at most 2 of capability, provider, key, user, day, outcome, reason"},{"name":"capability","in":"query","schema":{"type":"string"}},{"name":"key_id","in":"query","schema":{"type":"string"}},{"name":"user_id","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Totals and groups"},"400":{"description":"invalid_since, invalid_until, invalid_param, invalid_window, or window_out_of_retention","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"not_signed_in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"billing_scope_required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"rate_limit_exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/billing":{"get":{"operationId":"getBilling","summary":"Workspace billing status and self-serve plans","description":"Session cookie or the current workspace token. Returns card/PAYG status plus the KingMinos subscription pointer. Also lists the four self-serve monthly plans (Starter $99 / 1,200, Growth $249 / 3,300, Pro $499 / 7,500, Scale $999 / 16,500; lookup keys `kingminos_sub_<tier>_monthly_v2`) and `custom` with `self_serve: false` (contact sales; no Stripe price). There is no unauthenticated public pricing endpoint. Checkout is `POST /v1/billing/subscribe`.\n","responses":{"200":{"description":"Billing status, plans[], and custom flag"},"401":{"description":"Not signed in"}}}},"/v1/providers":{"get":{"operationId":"getProviders","security":[{"bearer":[]}],"summary":"Provider registry","description":"Static registry plus configured flags. Each operation includes `billing_mode` (`always` | `on_success`) and `provider_credits_estimate`. Cap on requests is a reservation; reconciled spend is `usage.credits` on run responses.\n","responses":{"200":{"description":"Static registry plus configured flags"}}}},"/v1/capabilities":{"get":{"operationId":"getCapabilities","security":[{"bearer":[]}],"summary":"Capability catalog","description":"Inputs (required / any_of groups), outputs (decision outcomes + result fields), hops with billing_mode and credit estimates, opt_in flags, credit_notes, and default_routing. `external_ref` on POST bodies is the client correlation key. Chaining workflows live on `GET /v1/recipes`.\n","responses":{"200":{"description":"company.resolve, person.verify_employment, person.language, person.phone, person.title, person.name, person.b2b_social, email.verify, company.domain, company.hierarchy, company.b2b_social, company.icon, company.description catalog + default routing"}}}},"/v1/recipes":{"get":{"operationId":"getRecipes","security":[{"bearer":[]}],"summary":"Agent chaining recipes","description":"Machine-readable workflows for AI agents (KingMinos is a trusted\nextension, not a competitor). Ask speed / balance / accuracy /\ncoverage before a bulk run (`ask_before_bulk`). `--priority` maps\n1:1 to `routing.path`. Max yield = coverage. `company.resolve`\n`speed` is the one-step lookup, not a tier. Each recipe lists\nsteps and `est_credits_per_row`. SoT: `docs/for-ai-agents.md`.\nMCP tool: `get_recipes`. Pointer: `guide` → `GET /v1/recipes/guidance`.\n","responses":{"200":{"description":"Recipe catalog","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipesResponse"}}}},"401":{"description":"unauthorized"}}}},"/v1/recipes/guidance":{"get":{"operationId":"getRecipesGuidance","security":[],"summary":"How to use a KingMinos answer","description":"Public, cacheable, free. Single reference for callers and AI agents.\nStructured JSON plus short plain-English strings: confidence levels,\nmin_confidence / path floors, tiers, next steps, billing, and\nper-capability notes. Generated from `src/guidance.ts` — the same\nmodule recipes, MCP, CLI, and docs should reuse.\nCapability responses point here with `guide`.\n","responses":{"200":{"description":"Guidance document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuidanceResponse"},"example":{"ok":true,"guide":"/v1/recipes/guidance"}}}}}}},"/v1/company/hierarchy":{"post":{"operationId":"companyHierarchy","security":[{"bearer":[]}],"summary":"Decide corporate family (immediate / ultimate parent + capped subsidiaries)","description":"One decision: who owns this company, and who sits under it?\nPaths: `speed` = Wikidata immediate claims only (0 credits).\n`balance` (default; legacy `auto`) and `accuracy` call ZoomInfo\ncompany search (0) then `companies/corporate-hierarchy/enrich`\n(1 credit on first `FULL_MATCH`). `coverage` is\n`400 invalid_routing`. Outcomes: `subsidiary` | `ultimate_parent` |\n`no_decision`. Never “standalone”. You are only charged when a\nparent answer is delivered; a miss, timeout, or error bills 0.\nSoT: `docs/company-hierarchy.md`.\nNot in `openapi.es.yaml`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyHierarchyRequest"},"example":{"domain":"lidl.com","name":"Lidl US"}}}},"responses":{"200":{"description":"Decision envelope (ZoomInfo hierarchy billed only on FULL_MATCH)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyHierarchyResponse"}}}},"400":{"description":"validation_failed | domain_website_mismatch | invalid_routing | invalid_compliance | invalid_explain | invalid_schema | unsupported_schema"},"401":{"description":"unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/company/b2b-social":{"post":{"operationId":"companyB2bSocial","security":[{"bearer":[]}],"summary":"Decide the company's B2B social company page (LinkedIn /company/{slug})","description":"One decision: the company's own LinkedIn company page URL, or null.\nSubject is domain / website / email **or name** (`400 missing_subject`\nwhen all four are blank). Name-only is allowed — it is not\n`name_only_unsupported`. Name→domain runs on accuracy / coverage\nonly. All paths start homepage-first: fetch the company\nsite (www/apex, redirects, browser-UA first, 4 s / 3.5 MB), then\n`/contact` and `/about` if no link, extract\n`linkedin.com/company/{slug}`. A homepage 301/302 onto\nlinkedin.com / x.com / other social hosts is recorded from\n`Location` — the Worker never GETs those hosts.\nSearch fallback takes\nthe highest-ranked Serper `site:linkedin.com/company {domain}` row\nthat passes the title/snippet identity check; a name-only SERP row\nis not a hit. `speed` = homepage + one websearch (cap 1). `balance`\n(default) then house AI Ark (cap 2). `accuracy` adds name→domain +\nLeadMagic + AI Ark (cap 3). `coverage` adds Findymail + last-resort\nBrowser Rendering (cap 4). Wiza is `routing.only` / BYOK only.\nConfidence is the write signal. Vendor / search identity-pass\nvalues are at least medium; conflict with a usable candidate\nreturns the best guess at low plus `next_steps.verify_linkedin_page`.\nOptional `min_confidence` overrides the path floor (speed/coverage\nlow, balance medium, accuracy high). Below-floor hits are withheld.\nAny AI agent\ncan call this (Muse, OpenClaw, Hermes, Dots, Grok Bot). A\ndelivered answer bills the path price once (speed 1 / balance\n1.2 / accuracy 1.4 / coverage 1.4), homepage / rendered\nincluded; a miss or a blank held below the confidence floor\nbills 0; BYOK stays 0.1. Hard 10 s server\nbudget including credit hold/settle.\nOutput `result.linkedin_url` is accept-form\n`https://www.linkedin.com/company/{slug}` or null. Additive\n`result.verification` and `result.evidence`\n(`homepage_link` | `homepage_rendered` | `linkedin_redirect` |\n`search_rank_N` | `vendor` | `name_domain`).\nSoT: `docs/company-b2b-social.md`.\nNot in `openapi.es.yaml`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyB2bSocialRequest"},"example":{"domain":"faktor3.de","name":"Faktor 3"}}}},"responses":{"200":{"description":"Decision envelope (LeadMagic billed on_success)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyB2bSocialResponse"}}}},"400":{"description":"validation_failed | domain_website_mismatch | invalid_routing | invalid_compliance | invalid_explain | invalid_schema | unsupported_schema | missing_subject | zi_credentials_required | apollo_credentials_required | prospeo_credentials_required | companyurlfinder_credentials_required | findymail_credentials_required | wiza_credentials_required"},"401":{"description":"unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/company/x-account":{"post":{"operationId":"companyXAccount","security":[{"bearer":[]}],"summary":"Decide the company's X (formerly Twitter) account","description":"One decision: the company's own X account URL, or null.\nSubject is domain / website / email **or name** (`400 missing_subject`\nwhen all four are blank). Name-only is allowed — it is not\n`name_only_unsupported`. Name→domain runs on accuracy / coverage\nonly.         All paths start homepage-first: fetch the company\nsite (www/apex, redirects, browser-UA first), then `/contact` and\n`/about` if no link, extract `x.com/{handle}` / `twitter.com/{handle}`.\nA homepage 301/302 onto linkedin.com / x.com / other social hosts\nis recorded from `Location` — the Worker never GETs those hosts.\nSearch fallback is Serper `site:x.com` / `site:twitter.com` (unquoted\ndomain). `speed` = homepage + one Serper host (cap 1). `balance`\n(default) adds Wikidata + house AI Ark (cap 2). `accuracy` /\n`coverage` add handle-confirm + ZoomInfo BYOK (caps 3 / 4). House\nZoomInfo is skipped unless `credentialSource === \"byok\"`. Wiza /\nLeadMagic / Findymail stay off defaults. Unmapped providers never\ncontribute an X URL. Confidence is the only write signal — the\nresponse has no `safe_to_write`. High includes a unique first-party\nhandle at placement 1–4 (JSON-LD, follow widget, site chrome,\ntwitter:site). Default floors: speed and coverage\naccept `low` and up; balance accepts `medium` and up; accuracy\naccepts `high` only. Optional `min_confidence` (`high` | `medium` |\n`low`) overrides the path floor. Below the floor the field is blank\n(`es_decision=miss`) with the withheld confidence,\n`evidence.candidates`, and `next_steps.verify_x_account`. Outcome\nprices (independent of LinkedIn): speed 1 / balance 1.3 /\naccuracy 1.6 / coverage 1.7. Only a delivered floor-passing hit\nbills the path price. Below-floor withholds and true misses bill\n0. BYOK on a delivered hit is 0.1. Hard 10 s server\nbudget. Output `result.x_url` is `https://x.com/{handle}`\n(lowercase, no `@`) or null. Never emit twitter.com. Not in\n`openapi.es.yaml`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyXAccountRequest"},"example":{"domain":"stripe.com","name":"Stripe"}}}},"responses":{"200":{"description":"Decision envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyXAccountResponse"}}}},"400":{"description":"validation_failed | domain_website_mismatch | invalid_routing | invalid_compliance | invalid_explain | invalid_schema | unsupported_schema | missing_subject | zi_credentials_required"},"401":{"description":"unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/company/icon":{"post":{"operationId":"companyIcon","security":[{"bearer":[]}],"summary":"Decide the company's stampable icon URL","description":"One decision: a hosted HTTPS PNG on `logos.kingminos.com`, or null.\nDomain (or website/email → same registrable) is required; name-only\nis `400`. No path, variant, format, size, or refresh knobs — send\nthe domain and the Worker stores the single best logo. Engine\n`first_acceptable`: house favicon-fetch first, then Brandfetch\nBrand API BYOK (prefer square `icon` over `logo`), then ZoomInfo\nscoped `companies/enrich` `logo` (not GTM search). Cap 2\n(Brandfetch 1 + ZoomInfo 1; favicon 0). LeadMagic is off the\nstamp path. Favicon first so missing `ZI_CLIENT_*` does not 400\n`zi_credentials_required`. Brandfetch is BYOK only (not House\nKeys). Raster only (PNG/JPEG/GIF/WebP/ICO). A site SVG icon is\nrasterized when every on-site raster is under 128px; vendor SVG\nURLs are a miss. The\nwinner is fetched server-side, normalized to 256×256 PNG\n(pad, never crop; never upscale more than 2×), and stored at\n`i/<sha256>.png`. Default answer-first mode returns the stable\nper-company URL `i/d/<sha256(domain)>.png` as soon as a source is\nproved and renders after the response (`flags.pending_render`).\nSmall sources still hit (`flags.low_res`,\nconfidence `low`). Default floor is low so they return unless\n`min_confidence` is set. `result.icon_url` is always\n`https://logos.kingminos.com/i/….png` on hit, never a third-party\nURL. Fetch / validate / store failure is a miss with\n`icon_url: null`. SoT: `docs/company-icon.md`.\nES action `companyIcon` in `openapi.es.yaml` (not stuffed onto\ncompanyDomain).\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyIconRequest"},"example":{"domain":"outboundsync.com"}}}},"responses":{"200":{"description":"Decision envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyIconResponse"}}}},"400":{"description":"validation_failed | domain_website_mismatch | invalid_routing | invalid_compliance | invalid_explain | invalid_schema | unsupported_schema | name_only_unsupported | zi_credentials_required | prospeo_credentials_required | brandfetch_credentials_required"},"401":{"description":"unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/company/description":{"post":{"operationId":"companyDescription","security":[{"bearer":[]}],"summary":"Decide the company's stampable description","description":"One decision: a usable English company description, or null.\nDomain (or website/email → same registrable) is required; name-only\nis `400`. Engine `first_acceptable`. No `company_name` in the output.\nHits-first on every path. Hard rejects are wrong company\n(identity < 0.85), junk/PII, and empty. Hype, first-person,\ntagline, same-as-speed, truncated, cta, and grammar are\n`result.flags`, not misses. `truncated` and `cta` force\nconfidence `low`. `grammar` is informational and does not\naffect confidence.\nNever invent a terminal period; mid-word cuts reject\nor trim to a whole sentence/word. Accents transliterate via\nNFKD. Audience/segment X (including compound adjectives after\nto/for/serving/helping) must appear as the company's own\ncustomers in the evidence.\nPaths: `speed` = homepage `<meta name=description>` (og fallback),\ndeterministic gates, no Jev, then AI Ark (cap 1). `balance`\n(default, cap 2) = Jev-ranked extracts + meta/og + AI Ark\n(optional 0.5 rewrite). `accuracy` (cap 3) = xAI neutral\nthird-person compose from homepage evidence using the resolved\ncompany name, one Jev, under 10s from request start; fallback\nto the cheaper hit. `coverage` (cap 6) = accuracy compose plus\noff-homepage Serper / ZoomInfo BYOK; if coverage compose fails\nit uses the accuracy winner and cannot miss where accuracy hit.\n`answer.confidence` is `high` | `medium` | `low` (high needs\nlive homepage + Jev identity ≥ 0.8) and is the write signal.\nVendor-returned text is at least medium unless `truncated` / `cta`\nkeep it low. Optional `min_confidence` overrides the path floor.\nAI Ark is the only\nfirmographic vendor. LeadMagic / Findymail / Wiza / Brandfetch /\nLemlist / Snov.io are off this capability. ZoomInfo is BYOK\nonly (never the house key). Billing is winner-only\n(`usage.credits.by_provider` / `usage.hops`); rejected / lost\nhops and misses are 0.\nCache 30 days in D1 (migration 0020). SoT: `docs/company-description.md`.\nES action `companyDescription` in `openapi.es.yaml`.\nBypass: `X-Router-Cache: bypass`, `Cache-Control: no-cache|no-store`,\nand/or `skip_cache: true` (skips cache **read**; still writes).\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"},{"$ref":"#/components/parameters/RouterCache"},{"$ref":"#/components/parameters/CacheControl"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyDescriptionRequest"},"example":{"domain":"outboundsync.com"}}}},"responses":{"200":{"description":"Decision envelope. `usage.hops` lists every attempt; billed amounts are `usage.credits.spent` / `usage.credits.by_provider` (winner only; 0 on a miss).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyDescriptionResponse"}}}},"400":{"description":"validation_failed | domain_website_mismatch | invalid_routing | invalid_compliance | invalid_explain | invalid_schema | unsupported_schema | invalid_cache | name_only_unsupported | zi_credentials_required"},"401":{"description":"unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/company/domain":{"post":{"operationId":"companyDomain","security":[{"bearer":[]}],"summary":"Decide a company domain (write-safe stamp + account aliases)","description":"One decision: what should the CRM domain field say, and is it safe to write?\nIntegrator walk-through (outcomes, HubSpot / Cap field map, path\nladder, worked examples): `docs/company-domain.md`.\n\nWrite HubSpot `domain` from `result.account_primary_domain` when the\noutcome is trusted (`verified` / `corrected` / `discovered`). Write\n`hs_additional_domains` from `result.additional_domains.join(\";\")`\n(ownership-proven only). Hold `review_domains` and `discovered_domain`\nfor a human. `additional_domains` is never “all domains found.”\n\nPaths: `speed` (legacy `fast` / `value` / `name_only`) = hygiene →\nDNS → site_probe (0 credits). `balance` (default; legacy `auto`) adds\n0-credit ZoomInfo. `accuracy` (cap 2) adds miss-only LeadMagic then\ncapped websearch when ZoomInfo misses (no BuiltWith). `coverage`\n(cap 3) is accuracy + BuiltWith Relationships recall (page 1);\nadditional lists are often `partial`. Responses echo canonical path\nnames after alias expand. Env/tenant `coverage` is ignored so shared\nresolve routing cannot leak BuiltWith. Outcomes:\n`verified` | `corrected` | `discovered` | `rejected` | `unverified` |\n`no_decision`. You are only charged when a domain answer is\ndelivered; a miss, reject, unverified, error, or timeout bills 0.\n`decision.path` is the `rule_id`. Harris opened\nES 2026-09-26 — stamp flats are in `openapi.es.yaml`\n(`operationId: companyDomain`). Nested entity /\nadditional_domains / hierarchy stay out of the ES subset.\nStale Account backfill reuses this POST (not a batch admin\nroute). Operator CLI: `npm run domain:backfill`. SoT:\n`docs/salesforce-named-cred.md`.\n\nShort-TTL isolate cache (default 300s) is keyed on\n`capability_version` + schema + normalized input + path — a deploy\nmisses prior trust-v1 rows. Eval / Janus first-100 re-smoke: send\n`X-Router-Cache: bypass` and/or `Cache-Control: no-cache|no-store`\nand/or body `skip_cache: true` (skips cache **read**; still writes).\nOmit or rotate `Idempotency-Key` or the same key replays\n`replayed: true` even with bypass. `400 invalid_cache` on a bad\n`X-Router-Cache` or non-boolean `skip_cache`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"},{"$ref":"#/components/parameters/IdempotencyKey"},{"$ref":"#/components/parameters/RouterCache"},{"$ref":"#/components/parameters/CacheControl"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyDomainRequest"},"example":{"domain":"facebook.com/acme","name":"Acme Roofing"}}}},"responses":{"200":{"description":"Decision envelope. Branch on decision.outcome / es_decision; map HubSpot from result.account_primary_domain + additional_domains.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyDomainResponse"}}}},"400":{"description":"validation_failed | domain_website_mismatch | invalid_routing | invalid_compliance | invalid_explain | invalid_schema | unsupported_schema | invalid_cache | builtwith_credentials_required | companyurlfinder_credentials_required"},"401":{"description":"unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/company/resolve":{"post":{"operationId":"companyResolve","security":[{"bearer":[]}],"summary":"Resolve a company (dry-run)","description":"Email alone (typical caller input), domain, or website → default `balance`\n(legacy `auto`; websearch first: Serper when configured, else xAI Grok two-pass;\ncompany_name only). OutboundSync sets `routing.path` on this call\n(`speed` | `balance` | `accuracy` | `coverage`; omit = `balance`).\nDefault linear order is `[websearch, zoominfo, leadmagic]` with\n`allow_fallbacks` and `free_first: false`. `balance` may run Wiza then\nFindymail **only** after a websearch miss/junk reject (not a longer\nlinear waterfall). `speed` (legacy `value`) never calls those paid name hops.\nDefault `max_provider_credits` is `19` on `balance` (`2` on `speed`;\n`accuracy` 21, `coverage` 23). You are only charged when a company\nname or id is delivered — the hops that contributed the name and id.\nA miss, error,\ntimeout, or below-floor withhold bills 0. Wiza name recovery bills 15 credits\n(8 per Wiza credit; a company lookup consumes 2).\nAI Ark is registered but **not** on company.resolve default paths;\n`routing.only: [\"aiark\"]` remains an opt-in escape (house key or BYOK).\nNon-default tenants resolve Findymail/Wiza/AI Ark from house Worker\nsecrets when set, else the OutboundSync credential broker\n(`docs/os-router-credentials.md`).\nProspeo is BYOK opt-in and off every default path\n(`routing.only: [\"prospeo\"]`; `POST /enrich-company`).\nHouse `PROSPEO_API_KEY` is dogfood (tenant `default`) only.\nApollo is BYOK opt-in and off every default path\n(`routing.only: [\"apollo\"]`; `GET /organizations/enrich`).\nHouse `APOLLO_API_KEY` is dogfood (tenant `default`) only.\nLemlist is BYOK opt-in and off every default path\n(`routing.only: [\"lemlist\"]`; `POST /database/companies`).\nHouse `LEMLIST_API_KEY` is dogfood (tenant `default`) only.\nA LeadMagic-only or websearch-only hit is `resolved_without_primary_id`.\nA websearch name is not terminal while ZoomInfo remains in `effectiveOrder`.\nName-only input is `no_decision` / `no_domain`.\n`routing.preset` is a back-compat alias (`name_only` → `value`).\n`{ \"preset\": \"zi_stamp\" }` is optional ZoomInfo-first.\nPrefer `X-Router-Explain: minimal` on CRM callouts.\nSoT: `docs/routing-presets.md`.\nUnit tests drive this path with `tests/fixtures/zoominfo-search-{one,zero,many}.json`, `tests/fixtures/leadmagic-search-{one,zero}.json`, `tests/fixtures/websearch-xai-{one,unknown}.json`, and `tests/fixtures/websearch-serper-{one,empty,tektonscale,ambiguous}.json`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyResolveRequest"},"example":{"email":"contact@outboundsync.com","external_ref":"account-demo-1"}}}},"responses":{"200":{"description":"Decision envelope (provider errors stay 200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyResolveResponse"}}}},"400":{"description":"validation_failed | domain_website_mismatch | invalid_routing | invalid_compliance | invalid_explain | invalid_schema | unsupported_schema | zi_credentials_required | findymail_credentials_required | wiza_credentials_required | aiark_credentials_required | builtwith_credentials_required | prospeo_credentials_required | apollo_credentials_required | peopledatalabs_credentials_required | hginsights_credentials_required | enrichcrm_credentials_required | lemlist_credentials_required"},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`). missing usually means Salesforce Custom\nHeaders empty. See docs/salesforce-named-cred.md.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted.\nIngress limiter, rolling 24h tenant credit cap, or per-key cap.\nThe run was not started; the Idempotency-Key is not spent.\nBack off and retry the same key. See docs/salesforce-named-cred.md.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"credential_broker_unavailable | credential_broker_unauthorized | store_unavailable (retryable true)"}}}},"/v1/person/verify-employment":{"post":{"operationId":"personVerifyEmployment","security":[{"bearer":[]}],"summary":"Verify whether to send to this person (dry-run)","description":"Pre-flight before email. Two-field shape (2026-09-22): **employment_status**\n(LeadMagic stack) plus optional **`zoominfo_contact_id`**.\nLocked stack (2026-09-21): **one Layer-1 verifier**.\nDefault is LeadMagic `POST /v1/people/email-validation`. Findymail `POST /api/verify`\nis a BYOK alternate only when the tenant/request explicitly selects it\n(`routing.only: [\"findymail\"]` or Findymail-first `order`). Never call both\nvalidators on the same request. No Serper/websearch, Jev, or Wiza on this capability.\nOutboundSync sets `routing.path` (`speed` | `balance` | `accuracy`; omit = `balance`).\n`value` / `auto` / `coverage` / `fast` / `name_only` are `400 invalid_routing`.\n`speed`: LeadMagic validate + mailbox-vs-expected-company domain check\n(the corrected or the as-sent domain matches, so a legacy mailbox at a\nrebranded company is not a mismatch). No `email_to_profile`, no\njob-change hop. Cap 0.5. Surfaces bounce status + catch-all + SEG + role\nin `result.email_validation` and `decision.summary`; `result.employment`\ncarries the low-confidence speed verdict (`still_there` / `unknown`;\nbusiness mailbox on another domain is `unknown` with\n`explain.speed_domain_check: domain_mismatch`, not `left`) with its basis\nin `explain.speed_domain_check`. Confidence caps at low on speed — only\na hard-bounce block is high. Use balance/accuracy to block on employment\nleft.\n`balance`: validate + LeadMagic `email_to_profile` when no LinkedIn URL,\nthen `job-change-detector` when expected company is present. Cap 13.5\n(validate 0.5 + profile 10 + job-change 3). `allow_paid_linkedin_lookup` is\ndefault-on; set `false` to opt out. Flag still requires\n`max_provider_credits` ≥ 13 (find + job-change) or `400 invalid_routing`.\n`accuracy`: same + **AI Ark** reverse-lookup corroboration (cap 14.5).\nEmpty/error AI Ark lowers confidence — no separate abstain SKU.\nAfter the employment decision, if ZoomInfo can run (house tenant `default` or BYOK)\nand we still lack a contact id, continue with ZoomInfo contact-search-by-email\n(`continueForContactId`) and attach `result.zoominfo_contact_id`. Id attach only —\ndoes not overwrite person name / title / company / LinkedIn or change\n`decision.outcome` / `result.employment` / confidence. ZI miss/error/`not_configured`\nkeeps the LeadMagic answer. Contact id is **not** required for `es_decision: hit`\n(company stamp-safe hit still needs `zoominfo_company_id`).\nDecision outcomes: `high_confidence_send` | `low_confidence_send` |\n`low_confidence_block` | `high_confidence_block` | `no_decision`.\nFour-way answers map `es_decision` → `hit`. Catch-all / SEG / employment inform\nthe bucket and live in explain, not as fifth outcomes.\nDefault `max_provider_credits` is 13.5 (`balance`). `speed` 0.5. `accuracy` 14.5.\nYou are only charged when an answer is delivered: `still_there`,\n`left`, or a definitive invalid mailbox. `unknown`, errors, timeouts,\nand empty results bill 0. Any hop served by your own key (BYOK) bills a flat 0.1 credit.\nZoomInfo contact search estimate is 0 — does not lower LinkedIn caps.\nHouse `LEADMAGIC_API_KEY`, `WIZA_API_KEY`, and `FINDYMAIL_API_KEY` are\nresale for any authenticated tenant. AI Ark uses the\nOutboundSync credential broker + house keys for default (`docs/os-router-credentials.md`).\nZoomInfo uses the same house/BYOK rules as company (`docs/byok-zoominfo.md`).\nSoT: `docs/routing-presets.md`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonVerifyEmploymentRequest"},"examples":{"default":{"summary":"Default (omit routing → balance)","value":{"email":"harris@outboundsync.com","expected_company":{"domain":"outboundsync.com"},"external_ref":"contact-demo-1"}},"speed_path":{"summary":"Cheap LeadMagic email-validate only","value":{"email":"harris@outboundsync.com","expected_company":{"domain":"outboundsync.com"},"routing":{"path":"speed"}}},"opt_out_paid_linkedin":{"summary":"Balance path but skip paid email→LinkedIn","value":{"email":"harris@outboundsync.com","expected_company":{"domain":"outboundsync.com"},"routing":{"path":"balance","allow_paid_linkedin_lookup":false}}}}}}},"responses":{"200":{"description":"Decision envelope (provider errors stay 200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonVerifyEmploymentResponse"}}}},"400":{"description":"validation_failed | invalid_routing | invalid_compliance | invalid_explain | zi_credentials_required"},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`). See docs/salesforce-named-cred.md.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted.\nIngress limiter, rolling 24h tenant credit cap, or per-key cap.\nThe run was not started; the Idempotency-Key is not spent.\nBack off and retry the same key. See docs/salesforce-named-cred.md.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"451":{"description":"subject_suppressed — this person asked OutboundSync not to be looked up\n(privacy request; `email` or `linkedin_url` is on the global suppression\nlist). Checked before any cache read, vendor hop, run row, or credit hold:\n0 credits. Do not retry; suppress the person in your own systems.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/person/language":{"post":{"operationId":"personLanguage","security":[{"bearer":[]}],"summary":"Can we engage this person in English — and if not, in what language?","description":"For sales and marketing outbound and advertising. Input is an `email` or a\n`linkedin_url` (one is required); `first_name` / `last_name` are used only to\nreject a profile that belongs to someone else (`person_mismatch`) — a name\nnever infers a language.\n\nAnswer: `english` is `yes` (English evidence at medium or high), `likely`\n(only low English evidence), `unconfirmed` (other languages found, no English —\nnever \"no\"), or `unknown` (nothing detected; `detected=false`).\n`recommended_language` is the language to engage in — English whenever\n`english=yes`, otherwise the strongest other language. `local_language` is the\nstrongest non-English language (localized sequences / ads). Both are set only\nat medium or high confidence; low guesses stay in `languages`.\nConfidence is a 0–1 heuristic score with a `high` / `medium` / `low` level,\nnot a calibrated probability.\n\nEvidence, strongest first: language the person wrote in their profile, listed\nlanguages, LinkedIn locale settings, location country, then support-only\ncallouts (company languages, work-email country domain). Locked ladder, cap 3:\nLeadMagic profile-search by LinkedIn URL (1) → AI Ark reverse-lookup by email\n(1, miss 0) → Findymail reverse-email (2) only when AI Ark found no one →\nLeadMagic on a URL the email hop resolved. `routing.path`: speed (cap 1; one profile read + Jev→Clef language judge) | balance (default, cap 3) | accuracy (cap 4); `coverage` is `400 invalid_routing`. Business-English tune: a profile located in IN, SG, PH, PK, MY, CA, ZA or HK gets its English location row at medium. Profile text is\nread from what those hops return — no extra spend. Under `compliance.soc2`,\nLeadMagic and AI Ark are skipped and Findymail is the only source.\n\n`decision.outcome` `hit` iff `recommended_language` is set; low-only evidence\n→ `no_decision` / `ambiguous` (`es_decision=ambiguous`); a vendor answered\nwith nothing → `zero_hits` (`miss`); no vendor answered → `provider_error`\n(`error`) or the skip reason (`compliance_blocked` / `status_down` /\n`max_credits_reached` / `not_configured` → `noop`). Each returned profile is\nchecked on its own; one that belongs to someone else is dropped, and\n`person_mismatch` (`person: null`) means none survived. Conflict\nkeeps the best-guess recommendation at low confidence. Optional\n`min_confidence` overrides the path floor; below-floor blanks\nheadline fields (`recommended_*`, `local_*`, `english=unknown`)\nand keeps candidates in `languages`. You are only charged when a\nrecommendation is delivered; a miss, ambiguous, below-floor\nwithhold, timeout, or error bills 0.\n30-day D1 cache: a repeat call replays with `usage.cached: true` at 0 credits;\n`skip_cache`, `X-Router-Cache: bypass`, or `Cache-Control: no-cache` skip the read\n(the fresh answer is still written).\n`DELETE /v1/subjects/{subject_key}` purges cached answers.\nSoT: `docs/person-language.md`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"},{"$ref":"#/components/parameters/RouterCache"},{"$ref":"#/components/parameters/CacheControl"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonLanguageRequest"},"examples":{"linkedin":{"summary":"LinkedIn URL (listed languages + bio)","value":{"linkedin_url":"https://www.linkedin.com/in/example","first_name":"Anna","last_name":"Schmidt"}},"email":{"summary":"Work email only","value":{"email":"anna@example.de","external_ref":"contact-demo-1"}}}}}},"responses":{"200":{"description":"Decision envelope (provider errors stay 200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonLanguageResponse"}}}},"400":{"description":"validation_failed (email_or_linkedin_required | invalid_linkedin_url) | invalid_compliance | invalid_explain"},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted.\nThe run was not started; the Idempotency-Key is not spent.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"451":{"description":"subject_suppressed — this person asked OutboundSync not to be looked up\n(privacy request; `email` or `linkedin_url` is on the global suppression\nlist). Checked before any cache read, vendor hop, run row, or credit hold:\n0 credits. Do not retry; suppress the person in your own systems.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/person/phone":{"post":{"operationId":"personPhone","security":[{"bearer":[]}],"summary":"One dialable phone number for a person, plus the ranked rest","description":"For outbound dialing and contactability gates. Input is an `email` (the\nSmartlead / Clay primary) or a `linkedin_url` (one is required);\n`first_name` / `last_name` are used only to reject a profile that\nbelongs to someone else (`person_mismatch`) — a name never infers\nanything.\n\nClay waterfall doctrine (clay.com/guides/waterfall-enrichment): query\nproviders in sequence and keep the first valid match. \"A wrong value is\nworse than a missing one\" — numbers without a `+` country code are\ndropped, never guessed. Conflicting numbers return the best guess\nat low confidence with all candidates in `phones[]`.\n\nAgent-managed router, answers in < 10 s (Harris 2026-10-13 /\n2026-10-23; docs/person-phone.md). Vendors: ZoomInfo / QuickEnrich /\nBlitz (BYOK, 0.1 KM), LeadMagic, AI Ark, Findymail (US only), Wiza.\nCheaper vendors run first under a ~8.5 s budget; house Wiza is\nsequential (miss / conflict / non-US only) and never launches in\nparallel with them. A vendor not back in time is dropped (bills 0).\n`routing.path` is the product:\n`speed` — LeadMagic first with a 3 s head start, then the cheapest fallbacks race it, cap 10, never Wiza;\n`balance` (default) — cheaper vendors first, best single number by vendor prior, region and\nline type, no waiting for agreement (house Wiza only on miss / conflict / non-US);\n`accuracy` — cheaper vendors first, house Wiza only on miss / conflict / non-US, exactly one number (`conflicting_numbers` → low + verify);\n`coverage` — cheaper vendors first, house Wiza only on miss / conflict / non-US, up to 5 ranked numbers, each returned\nnumber's vendor bills, cap 58. `consensus` and `do_not_call` are on every\nnumber (consensus gates safety only in accuracy). Only vendors behind\nreturned numbers bill, capped at the path cap (speed 10 / balance and accuracy 38 / coverage 58); a miss bills 0.\n\nOptionality (Harris 2026-10-12): `phone_types` filters the answer\n(`mobile` | `landline` | `other` | `unknown`; omit = any) and\n`max_results` (1–10, default 3) caps the ranked list. The response\nreturns the typed ranked `phones` (mobile first) plus a `best_phone`\nhonoring the filter. A vendor that can return none of the requested\ntypes is not called (every Wiza number is a mobile): 0 credits,\n`insufficient_input` (`noop`).\n\nOutput format (Harris 2026-10-13): every phone carries `e164`,\n`formats` (`e164`, `international`, `national`, `digits`,\n`digits_national`), `country_code` and `calling_code`; the optional\n`format` request field (default `e164`) picks which one fills\n`number`. Backward compatible: omit it and `number` stays E.164.\n\nVerification (Harris 2026-10-13, no extra vendor cost): every number\nis checked with libphonenumber (valid + possible; invalid numbers are\ndropped), typed from the numbering plan where definitive\n(`line_type`), screened for junk (repeated / sequential digits, NANP\n555-01xx), and checked against the person's or company's known\ncountry. Toll-free, premium, shared-cost, pager and company\nswitchboard numbers are not a direct dial and are dropped. Each phone\ncarries `confidence`, `flags` and `sources`; a hit\nwhose only numbers were dropped is a miss and bills 0. No carrier /\nHLR lookup is made — `line_type` is not proof the line is live.\n\n`decision.outcome` `hit` iff a matching number was found; nothing →\n`zero_hits` (`miss`, `result.person` null); conflicting numbers across\nvendors → `conflicting_numbers` flag; no vendor answered →\n`provider_error` (`error`) or the skip reason (`noop`).\n30-day D1 cache keyed on the request filter: a repeat call replays with\n`usage.cached: true` at 0 credits; `skip_cache`,\n`X-Router-Cache: bypass`, or `Cache-Control: no-cache` skip the read\n(the fresh answer is still written). `DELETE /v1/subjects/{subject_key}`\npurges cached answers. API tenants only — not in `openapi.es.yaml`, no\nContact/Lead stamping. SoT: `docs/person-phone.md`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"},{"$ref":"#/components/parameters/RouterCache"},{"$ref":"#/components/parameters/CacheControl"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonPhoneRequest"},"examples":{"email":{"summary":"Work email only (Smartlead / Clay column)","value":{"email":"anna@example.de","phone_types":["mobile"]}},"linkedin":{"summary":"LinkedIn URL with a mobile-only filter","value":{"linkedin_url":"https://www.linkedin.com/in/example","first_name":"Anna","last_name":"Schmidt","phone_types":["mobile"],"max_results":3}}}}}},"responses":{"200":{"description":"Decision envelope (provider errors stay 200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonPhoneResponse"}}}},"400":{"description":"validation_failed (email_or_linkedin_required | invalid_linkedin_url | invalid_phone_type | max_results_out_of_range) | invalid_compliance | invalid_explain"},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted.\nThe run was not started; the Idempotency-Key is not spent.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"451":{"description":"subject_suppressed — this person asked OutboundSync not to be looked up\n(privacy request; `email` or `linkedin_url` is on the global suppression\nlist). Checked before any cache read, vendor hop, run row, or credit hold:\n0 credits. Do not retry; suppress the person in your own systems.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/person/title":{"post":{"operationId":"personTitle","security":[{"bearer":[]}],"summary":"A person's current job title, seniority and function","description":"For persona routing, sequencing and CRM title stamps. Input is an\n`email` or a `linkedin_url` (one is required); `first_name` /\n`last_name` are used only to reject a profile that belongs to someone\nelse (`person_mismatch`) — a name never infers anything.\n`company_domain` is match-only: a vendor title at a different current\nemployer is never a hit (`ambiguous` + `employer_mismatch`).\n\n`fields` (Harris 2026-10-15) picks the answer: any of `title`,\n`seniority`, `function` (omit = all three). It drives the ladder — a\nvendor that can feed none of them is not called, and the ladder walks\nuntil every requested field is answered. v1 vendors return `title`;\n`seniority` / `function` are derived from it (keyword tables, then the\nJev judge on balance and above). `result.field_sources` says how each\nfield was produced (`vendor` | `derived` | `jev`).\n\nPaths (`routing.path`, omit = `balance`; `value` / `fast` /\n`name_only` → speed, `auto` → balance):\n- `speed` — one LinkedIn or email profile read, keyword\n  classification only, never `high`. 1 credit on a hit.\n- `balance` — ZoomInfo `contacts/search` (workspace BYOK key only),\n  LeadMagic profile-search, AI Ark reverse-lookup, Findymail only\n  when AI Ark found no one; stops at the first usable title. 2\n  credits on a hit.\n- `accuracy` — two independent sources (LinkedIn- and email-keyed) +\n  one judge call; agreement is `high`, a disagreement keeps the newer\n  role and flags `conflicting_titles`. 10 s cap. 3 credits on a hit.\n- `coverage` — accuracy + AI Ark people search + Findymail whenever\n  a second source is still wanted. 5 credits on a hit.\n\nOutcome-priced (Harris 2026-10-15): a hit bills the path price once;\na miss, a partial (`fields_incomplete`) and a timeout bill 0. House\nZoomInfo is never called; BYOK keys for every vendor swap in and bill\nthe same price. `decision.outcome` `hit` iff every requested field is\nanswered at medium or high confidence; some but not all →\n`fields_incomplete` (`ambiguous`, values withheld, names in\n`result.answered_fields`); headline-only or wrong-employer evidence →\n`ambiguous`; nothing → `zero_hits` (`miss`, `result.person` null).\nConflicting titles return the newer role at low confidence.\nOptional `min_confidence` overrides the path floor; below-floor\nblanks the fields and bills as a miss.\n30-day D1 cache keyed on path + `fields` + `company_domain` +\n`routing.only` / `ignore` (replay 0 credits; `skip_cache`,\n`X-Router-Cache: bypass`, `Cache-Control: no-cache` skip the read).\n`DELETE /v1/subjects/{subject_key}` purges cached answers. API tenants\nonly — not in `openapi.es.yaml`, no Contact/Lead stamping. SoT:\n`docs/person-title.md`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"},{"$ref":"#/components/parameters/RouterCache"},{"$ref":"#/components/parameters/CacheControl"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonTitleRequest"},"examples":{"persona":{"summary":"Persona routing (seniority + function) from a work email","value":{"email":"anna@example.com","fields":["seniority","function"]}},"stamp":{"summary":"Accuracy CRM title stamp for a known account","value":{"linkedin_url":"https://www.linkedin.com/in/example","email":"anna@example.com","company_domain":"example.com","routing":{"path":"accuracy"}}}}}}},"responses":{"200":{"description":"Decision envelope (provider errors stay 200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonTitleResponse"}}}},"400":{"description":"validation_failed (email_or_linkedin_required | invalid_linkedin_url | invalid_domain | invalid_title_field) | invalid_routing | unknown_provider | invalid_compliance | invalid_explain"},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted.\nThe run was not started; the Idempotency-Key is not spent.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"451":{"description":"subject_suppressed — this person asked OutboundSync not to be looked up\n(privacy request; `email` or `linkedin_url` is on the global suppression\nlist). Checked before any cache read, vendor hop, run row, or credit hold:\n0 credits. Do not retry; suppress the person in your own systems.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/person/name":{"post":{"operationId":"personName","security":[{"bearer":[]}],"summary":"A person's proper-cased first / last / full name by email or LinkedIn URL","description":"For CRM name stamps and personalization. Input is an `email` or a\n`linkedin_url` (one is required); `phone` / `x_handle` are accepted\nbut never looked up (`unsupported_input`, 0 credits).\n\nSalutations, titles, honorifics, suffixes, pronouns, emoji and URLs\nare stripped deterministically (`cleanPersonName`) on every vendor\ncandidate — zero spend. The Jev/clef judge classifies only an\nambiguous multi-token split (≥2 kept words with no last name, or\n≥3 words), on balance and above, with ≥1.5 s remaining: it fills a\nnull side at ≥0.7 and overrides the deterministic split only at\n≥0.85. A judge failure never fails the hop.\n\nPaths (`routing.path`, omit = `balance`; `value` / `fast` /\n`name_only` → speed, `auto` → balance):\n- `speed` — LinkedIn- or email-keyed profile read (LeadMagic, then\n  AI Ark); never `high`. 1 credit on a hit.\n- `balance` — LeadMagic, AI Ark, Findymail only when AI Ark found\n  no one; stops at the first usable name. 2 credits on a hit.\n- `accuracy` — same ladder + the judge on ambiguous splits;\n  `high` requires independent confirmation (two agreeing vendors\n  alone stay below `high` and are withheld at the default high\n  floor, billing 0), a disagreement is `conflict` (bills 0, flags\n  `conflicting_names`). 3 credits on a hit. A ZoomInfo workspace\n  key is the usual independent confirmer; public web-search verify\n  also runs on this path.\n- `coverage` — balance ladder + Findymail as a second source. 5\n  credits on a hit.\n\nOutcome-priced: a hit bills the path price once on the deciding\nattempt; a miss, a conflict, a below-floor withhold and a timeout bill\n0. An email local-part split is a last resort only (`parsed_only`, low\nconfidence, flagged `email_local_parse`); a role address (`admin@`,\n`info@`, …) is never a person (`role_address`, 0 credits). `matched_on`\nsays which request identifier(s) the name came from. Write signal is\n`answer.confidence` only (no `safe_to_write` fields). Default floors:\nspeed/coverage low, balance medium, accuracy high; optional\n`min_confidence` overrides. Below-floor blanks the name fields and keeps\ncandidates in `result.evidence`. 30-day D1 cache keyed on path +\n`min_confidence` + `routing.only` / `ignore` (replay 0 credits;\n`skip_cache`,\n`X-Router-Cache: bypass`, `Cache-Control: no-cache` skip the read).\n`DELETE /v1/subjects/{subject_key}` purges cached answers. A\nsuppressed `email` / `linkedin_url` is `451 subject_suppressed` at 0\ncredits before any cache, hop, run, or hold.\n\nOptional `last_name_fallback` (recommended `\"Unknown\"` for a direct\nSalesforce import) fills `result.last_name` when none was found,\nflags `last_name_placeholder`, and never changes outcome or credits.\nIt is decided per call and is not in the cache key. Opt-in\n`output.salesforce_safe` adds `result.salesforce` (FirstName ≤ 40,\nLastName ≤ 80 UTF-16 units) without changing the canonical name\nfields. API tenants only — not in `openapi.es.yaml`, no Contact/Lead\nstamping.\n\nBest results: connect your own ZoomInfo credentials (highly\nrecommended; `PUT /v1/credentials/zoominfo`; `accuracy` `high`\nessentially needs it) and send `email` and `linkedin_url` together\nwhen you have both, which improves fill. There is no company,\ndomain, or name-hint input. `speed` is the fastest and accepts\nvendor answers at a lower bar with some checks, never `high`.\n`balance` blends speed with more error fixing and more coverage,\ndefault floor `medium`, never `high`. `accuracy` strives for no wrong\nanswers: `high` only with independent confirmation, withheld answers\nbill 0, fill is lower. `coverage` fills the most rows from any\nevidence and flags low confidence. `min_confidence` decides which\nnames you receive more than the path does.\n`first_name_confidence` / `last_name_confidence` are optional; fall\nback to `answer.confidence`. In our small evaluations, roughly\n(samples were small, uncertainty is wide, and results depend on a\nconnected ZoomInfo key, which is highly recommended): `balance` at\n`medium` fills about 80–90% of rows with roughly 1–8% of delivered\nanswers wrong; `accuracy` at `high` fills roughly 30–55% with an\nemail only and up to about 75% when a `linkedin_url` is also sent,\nwith wrong answers rare but not zero; `coverage` fills the most\nrows, flags low confidence, and returns noticeably more wrong\nanswers; `speed` is the lowest bar and never `high`. Handling of first-name-only, surname-change,\ntruncated and role-address answers: `docs/person-name.md`, \"Getting\nthe best results\". SoT: `docs/person-name.md`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"},{"$ref":"#/components/parameters/RouterCache"},{"$ref":"#/components/parameters/CacheControl"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonNameRequest"},"examples":{"crm_stamp":{"summary":"CRM name stamp from a work email","value":{"email":"ada.lovelace@example.test"}},"linkedin":{"summary":"Name from a LinkedIn URL on accuracy","value":{"linkedin_url":"https://www.linkedin.com/in/ada-lovelace-0000","routing":{"path":"accuracy"}}},"salesforce_import":{"summary":"Direct Salesforce import — LastName fallback + Salesforce-safe view","value":{"email":"grace.h@example.test","last_name_fallback":"Unknown","output":{"salesforce_safe":true}}}}}}},"responses":{"200":{"description":"Decision envelope (provider errors stay 200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonNameResponse"}}}},"400":{"description":"validation_failed (email_or_linkedin_required | invalid_linkedin_url) | invalid_routing | unknown_provider | invalid_compliance | invalid_explain"},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted | max_credits_reached.\nThe run was not started; the Idempotency-Key is not spent.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"451":{"description":"subject_suppressed — this person asked OutboundSync not to be looked up\n(privacy request; `email` or `linkedin_url` is on the global suppression\nlist). Checked before any cache read, vendor hop, run row, or credit hold:\n0 credits. Do not retry; suppress the person in your own systems.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/person/b2b-social":{"post":{"operationId":"personB2bSocial","security":[{"bearer":[]}],"summary":"A person's LinkedIn profile URL from an email address","description":"For CRM profile-URL stamps and social sequencing. `email` is required\n(the one discovery key); `linkedin_url`, `first_name` and `last_name`\nare match-only — a vendor profile whose slug differs from the\nrequested `linkedin_url` is rejected (`person_mismatch`, 0 credits).\nThe name never discovers or scores anything.\n\nPaths (`routing.path`, omit = `balance`; `value` / `fast` /\n`name_only` → speed, `auto` → balance):\n- `speed` — one AI Ark reverse lookup by email; Findymail only when\n  AI Ark answered nothing. Floor low. 2 credits on a hit.\n- `balance` — AI Ark and Findymail (second source always runs,\n  including after a first-hop timeout or error). Floor medium. 3\n  credits on a hit.\n- `accuracy` — both email hops (the second still runs after a\n  first-hop timeout or error), then ZoomInfo (workspace key) when\n  one URL or sources disagree, then a public-web hop when still\n  unconfirmed. Floor high. Withholds rather than guesses: a\n  LinkedIn URL is returned only when independently confirmed\n  (two sources, exact same slug, no conflicting flag). Otherwise\n  `linkedin_url` is blank, `reason` `below_min_confidence`, 0\n  credits; `next_steps` may suggest coverage. Fill is lower than\n  balance. 10 s cap from request start. 5 credits on a hit.\n- `coverage` — both email hops, ZoomInfo when a workspace key is\n  present, and a public-web hop on every row. Floor low. 5 credits\n  on a hit.\n\nConfidence is the only write signal. High requires independent\nconfirmation: two agreeing sources (identity-equivalent slugs;\naccent-insensitive), or a slug that contains both requested name\ntokens. A public-web hop queried from the requested name is not\nindependent confirmation — a websearch-only hit stays `medium`\nwith `single_source`. One source is `medium`; disagreeing sources\nare the best guess at `low` with `conflicting_profiles`.\nOptional `company_domain` is a match-only employer hint\n(`employer_mismatch` caps high at medium — not a hard miss). The\npath floor (speed low / balance medium / accuracy high / coverage\nlow) or `min_confidence` withholds a below-floor answer:\n`linkedin_url` blank, `reason` `below_min_confidence`, 0 credits.\nOn any non-delivery `candidates[]` is provider + hop status only —\nnever a URL, slug, or name. `dry_run: true` is offline (no vendor\nhops). A timeout with no hit is `provider_error` and is not cached.\nOutcome-priced: only a delivered floor-passing hit bills the path\nprice, once, on the deciding attempt; a miss, a person mismatch, a\nwithheld answer and a timeout bill 0. Last names that share no\ntoken (after accent folding and umlaut transliteration, ignoring a\nparenthetical maiden name) are `person_mismatch`. Nickname groups\napply on the given name; two request initials consistent with the\nvendor first name are not a conflict; explicit surname\nabbreviations are compatible, a bare different surname is not.\nA packed full name on `first_name` (last blank) is split the same\nway as a `full` name. `X-Router-Explain` / `?explain=` `full`\nadds `trace.explain.rejected` for each identity-gate reject\n(gate + compared normalised first/last tokens only — no URL,\nslug, or email). Default responses omit it.\nThe returned slug is percent-decoded NFC and never gains or loses\nseparators. A suppressed `email` / `linkedin_url` is `451\nsubject_suppressed` (0 credits). 30-day D1 cache keyed on path +\n`routing.only` / `ignore` + `min_confidence` (cache replay 0\ncredits; `Idempotency-Key` is not in the cache key; replay of that\nheader returns the original spent with `usage.cached: true`;\n`skip_cache`, `X-Router-Cache: bypass`, `Cache-Control: no-cache`\nskip the read only — a completed house-key run still writes).\n`GET /v1/runs/{id}` is tenant-scoped for the key that created the\nrun. `DELETE /v1/subjects/{subject_key}` purges cached answers. API\ntenants only — not in `openapi.es.yaml`, no Contact/Lead stamping.\nSoT: `docs/person-b2b-social.md`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"},{"$ref":"#/components/parameters/RouterCache"},{"$ref":"#/components/parameters/CacheControl"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonB2bSocialRequest"},"examples":{"crm_stamp":{"summary":"LinkedIn profile URL from a work email","value":{"email":"anna@example.com"}},"confirm":{"summary":"Confirm a known profile on accuracy","value":{"email":"anna@example.com","linkedin_url":"https://www.linkedin.com/in/example","routing":{"path":"accuracy"}}}}}}},"responses":{"200":{"description":"Decision envelope (provider errors stay 200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonB2bSocialResponse"}}}},"400":{"description":"validation_failed (email_required | invalid_linkedin_url | min_confidence) | invalid_routing | unknown_provider | invalid_compliance | invalid_explain"},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted | max_credits_reached.\nThe run was not started; the Idempotency-Key is not spent.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"451":{"description":"subject_suppressed — this person asked OutboundSync not to be looked up\n(privacy request; `email` or `linkedin_url` is on the global suppression\nlist). Checked before any cache read, vendor hop, run row, or credit hold:\n0 credits. Do not retry; suppress the person in your own systems.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/email/verify":{"post":{"operationId":"emailVerify","security":[{"bearer":[]}],"summary":"Is this mailbox deliverable?","description":"For list hygiene before a send. `email` is the only input. The\nanswer is `result.status`: `valid` | `invalid` | `catch_all` |\n`unknown`.\n\nLocal checks run first and are free: a malformed address, a\nreserved domain, a domain that does not exist, or a domain with no\nmail host is `invalid` (`source: local`, 0 credits). Then vendor\nemail validation (`source: vendor`). Paths (`routing.path`, omit =\n`balance`):\n- `speed` — one vendor check (a second only when the first did not\n  answer). Floor low. 0.5 credits on a delivered answer.\n- `balance` — a second opinion when the first check is\n  inconclusive. Floor medium. 1 credit.\n- `accuracy` — two independent checks; delivers only when both\n  agree. Floor high. 1.5 credits.\n- `coverage` — two independent checks; delivers a single\n  conclusive check too. Floor low. 1.5 credits.\n\nTwo agreeing checks are `high` confidence, one is `medium`.\nConflicting checks are `unknown` with `vendor_disagreement`\n(confidence `low`). `catch_all` means the domain accepts every\naddress, so a \"valid\" there cannot be proven; it is withheld and\nbills 0. The path floor or `min_confidence` withholds a below-floor\nstatus as `unknown` (`below_min_confidence`, 0 credits). Flags\n(`role_based`, `free_mailbox`, `disposable`, `relay`,\n`security_gateway`) describe the mailbox and never change the\nstatus. Outcome-priced: only a delivered vendor `valid` / `invalid`\nbills the path price, once; `catch_all`, `unknown`, a local\n`invalid`, a withheld answer, and a timeout bill 0. Vendor checks\nstop at 8 s (speed / balance) or 10 s (accuracy / coverage) from\nrequest start. `dry_run: true` is offline (no DNS or vendor\ncheck). Not cached. A suppressed `email` is `451\nsubject_suppressed` (0 credits). API tenants only — not in\n`openapi.es.yaml`, no Contact/Lead stamping.\nSoT: `docs/email-verify.md`.\n","parameters":[{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailVerifyRequest"},"examples":{"hygiene":{"summary":"Verify one mailbox before a send","value":{"email":"anna@example.com"}},"strict":{"summary":"Only accept two agreeing checks","value":{"email":"anna@example.com","routing":{"path":"accuracy"}}}}}}},"responses":{"200":{"description":"Decision envelope (provider errors stay 200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailVerifyResponse"}}}},"400":{"description":"validation_failed (email_required | email_too_long | min_confidence) | invalid_routing | unknown_provider | invalid_compliance | invalid_explain"},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied"},"409":{"description":"idempotency_conflict | idempotency_in_flight | idempotency_spent | idempotency_replay_unavailable | run_not_running"},"429":{"description":"rate_limit_exceeded | tenant_budget_exhausted | key_budget_exhausted | max_credits_reached.\nThe run was not started; the Idempotency-Key is not spent.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"451":{"description":"subject_suppressed — this person asked OutboundSync not to be looked up\n(privacy request; `email` is on the global suppression list). Checked\nbefore any vendor check, run row, or credit hold: 0 credits. Do not\nretry; suppress the address in your own systems.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"store_unavailable (D1 operational; retryable true). Same Idempotency-Key."}}}},"/v1/runs/{id}":{"get":{"operationId":"getRun","security":[{"bearer":[]}],"summary":"Fetch a recorded run","description":"Same envelope as capability POST responses, plus `candidates`, for completed runs.\n`running` / `failed` runs return a subset envelope (`status`, `error`, `usage`,\n`attempts`; `decision` / `result` / `es_decision` are null). 404 for missing\nor another tenant's run. Session and `km_` callers also 404 unless the run's\nworkspace equals the caller's current workspace. Honors `X-Router-Explain` /\n`?explain=` the same way as capability POSTs.\nNot in `openapi.es.yaml` (Flow only reads completed POST responses).\n","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/RouterExplain"},{"$ref":"#/components/parameters/RouterExplainQuery"},{"$ref":"#/components/parameters/RouterSchema"},{"$ref":"#/components/parameters/RouterSchemaQuery"}],"responses":{"200":{"description":"Run envelope (completed, or running/failed subset)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunGetResponse"}}}},"400":{"description":"invalid_explain | invalid_schema | unsupported_schema"},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`). See docs/salesforce-named-cred.md.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"not_found"}}}},"/v1/credentials":{"get":{"operationId":"listCredentials","security":[{"bearer":[]}],"summary":"List Your Keys status (never the secret)","description":"Masked catalog for the six data providers. A signed-in workspace\nlists `workspace_provider_credentials` for the current workspace.\nA tenant Bearer lists `tenant_provider_credentials`. Responses\nnever include the raw key. LeadMagic, Wiza, Findymail, or AI Ark without a\nstored row is `managed` (house key). Other vendors are `unset` until stored.\n","responses":{"200":{"description":"Masked credential catalog","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialListResponse"}}}},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"rate_limit_exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/credentials/{provider}":{"parameters":[{"in":"path","name":"provider","required":true,"schema":{"type":"string","enum":["zoominfo","leadmagic","findymail","wiza","aiark","builtwith","prospeo","apollo","companyurlfinder","peopledatalabs","hginsights","enrichcrm","lemlist","snovio","cognism","lusha","starbridge","clay","databar","quickenrich","getleads","blitzapi"]},"description":"Vendor to store. ZoomInfo body is `{ clientId, clientSecret }`.\nAPI-key providers use `{ apiKey }` (PUT also accepts `api_key`).\n`snovio` is OAuth (`{ clientId, clientSecret }`) — credential vendor only, no hop.\n`cognism` / `lusha` / `starbridge` / `clay` / `databar` /\n`quickenrich` / `getleads` / `blitzapi` are API-key credential\nvendors only — no routing hop yet.\n`websearch` is rejected (`byok_not_supported`).\n"}],"put":{"operationId":"putCredentials","security":[{"bearer":[]}],"summary":"Upsert Your Keys (ciphertext)","description":"Checks the secret with the vendor (auth or account call) before\nstoring. A rejected key is not stored (`400 credential_rejected`).\nEncrypts with `CREDENTIALS_KEK`. Signed-in workspace owners write\n`workspace_provider_credentials` (AAD binds the workspace id).\nTenant Bearers write `tenant_provider_credentials`. Live writes\nneed `CREDENTIALS_KEK` already on Worker `kingminos-api-prod`.\nResponse never includes the raw key. SoT: `docs/byok-zoominfo.md`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["clientId","clientSecret"],"additionalProperties":false,"properties":{"clientId":{"type":"string"},"clientSecret":{"type":"string"}}},{"type":"object","required":["apiKey"],"additionalProperties":false,"properties":{"apiKey":{"type":"string"}}},{"type":"object","required":["api_key"],"additionalProperties":false,"properties":{"api_key":{"type":"string"}}}]}}}},"responses":{"200":{"description":"Stored (ciphertext only)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialStoreResponse"}}}},"400":{"description":"validation_failed | unknown_provider | credential_rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"rate_limit_exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"credentials_kek_missing | credentials_kek_invalid |\nbyok_kek_version_unsupported | byok_decrypt_failed\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}},"delete":{"operationId":"deleteCredentials","security":[{"bearer":[]}],"summary":"Revoke tenant Your Keys","description":"Sets `revoked_at` on the live row. 404 when no live row.\nResponse never includes the raw key.\n","responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialRevokeResponse"}}}},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"not_found — no live row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"rate_limit_exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/feedback":{"post":{"operationId":"submitFeedback","security":[{"bearer":[]}],"summary":"File product feedback (free, never billed)","description":"One feedback channel for agents and users — not a support channel.\nUse it when a call misses or comes back low-confidence, when you had\nto work around KingMinos, or when you wanted a field or provider we\ndo not have. Every report is tied to the authenticated caller\n(workspace when there is one, else the tenant). ~20 reports per\n24h per rate key (`429 feedback_rate_limited`); a same-day retry of\nan identical report replays the original id (`deduped: true`) and\ndoes not consume a slot. `requested` is required for\n`provider_request` / `endpoint_request`; `capability` is required\nfor `data_quality` and must be a live capability id. No personal\ndata or secrets — secret-shaped content is rejected\n(`400 secret_like_content`). Never echoes submitted values in\nerrors. SoT: `docs/feedback.md`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackCreate"}}}},"responses":{"200":{"description":"Filed (or replayed dedup)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackCreated"}}}},"400":{"description":"invalid_feedback (unknown/missing fields, bounds, enums,\nrequested_required, capability_required) | secret_like_content |\nunknown_run_id — detail names the failing field, never the value.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"feedback_rate_limited (retryable, Retry-After 3600)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/webhooks":{"get":{"operationId":"listWebhooks","security":[{"bearer":[]}],"summary":"List workspace webhook endpoints (docs/platform-webhooks.md)","description":"`403 webhooks_not_enabled` while the workspace gate is off. Secrets never returned after create/rotate.","responses":{"200":{"description":"Endpoints for the current workspace"},"401":{"description":"Not signed in"},"403":{"description":"webhook_scope_required | webhooks_not_enabled"}}},"post":{"operationId":"createWebhook","security":[{"bearer":[]}],"summary":"Register a webhook endpoint","description":"Owner only. HTTPS-only; private/loopback/link-local targets (incl. DNS-resolved) rejected. Max 20 per workspace. `enabled_events` holds active types only (empty = all active); reserved names are `400 invalid_webhook_events`. The `secret` (`kmwhsec_…`) is returned once.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string"},"description":{"type":"string"},"enabled_events":{"type":"array","items":{"type":"string"}}}},"example":{"url":"https://example.com/kingminos/webhooks","description":"Run Health alerts","enabled_events":[]}}}},"responses":{"201":{"description":"Created; secret shown once"},"400":{"description":"validation_failed | webhook_endpoint_limit | invalid_webhook_url | blocked_webhook_host | invalid_webhook_events"},"401":{"description":"Not signed in"},"403":{"description":"webhook_scope_required | webhook_owner_required | webhooks_not_enabled"}}}},"/v1/webhooks/{id}":{"get":{"operationId":"getWebhook","security":[{"bearer":[]}],"summary":"Get one webhook endpoint (no secret)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Endpoint"},"404":{"description":"Not found in this workspace"}}},"patch":{"operationId":"patchWebhook","security":[{"bearer":[]}],"summary":"Update url / description / enabled_events / is_active","description":"Owner only. Partial update: send only the fields to change (an empty object is a no-op). Re-enabling (`is_active: true`) clears the auto-disable state. Changed URLs are revalidated (HTTPS + DNS). `description: null` clears it (max 300 chars, longer is truncated).\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"HTTPS URL; private/loopback/link-local targets rejected"},"description":{"type":["string","null"],"description":"null clears; truncated to 300 chars"},"enabled_events":{"type":"array","items":{"type":"string"},"description":"Active event types only; empty = all active"},"is_active":{"type":"boolean"}}},"example":{"is_active":true}}}},"responses":{"200":{"description":"Updated"},"400":{"description":"validation_failed | invalid_webhook_url | blocked_webhook_host | invalid_webhook_events"},"401":{"description":"Not signed in"},"403":{"description":"webhook_scope_required | webhook_owner_required | webhooks_not_enabled"},"404":{"description":"Not found"}}},"delete":{"operationId":"deleteWebhook","security":[{"bearer":[]}],"summary":"Soft-delete the endpoint","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/v1/webhooks/{id}/rotate-secret":{"post":{"operationId":"rotateWebhookSecret","security":[{"bearer":[]}],"summary":"Rotate the signing secret (new secret shown once)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Rotated; secret shown once"},"404":{"description":"Not found"}}}},"/v1/webhooks/{id}/test":{"post":{"operationId":"testWebhook","security":[{"bearer":[]}],"summary":"Send a test.ping delivery","description":"`202 { event_id }`; ignores `enabled_events`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Test event queued"},"404":{"description":"Not found"},"409":{"description":"webhook_endpoint_inactive"}}}},"/v1/webhooks/{id}/deliveries":{"get":{"operationId":"listWebhookDeliveries","security":[{"bearer":[]}],"summary":"Delivery log for one endpoint (status filter, limit ≤ 100)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Deliveries (newest first)"},"404":{"description":"Not found"}}}},"/v1/webhooks/{id}/deliveries/{delivery_id}/replay":{"post":{"operationId":"replayWebhookDelivery","security":[{"bearer":[]}],"summary":"Replay a delivery as a new attempt (replay_of_delivery_id link)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"delivery_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Replayed"},"404":{"description":"Not found"}}}},"/v1/events":{"get":{"operationId":"listEvents","security":[{"bearer":[]}],"summary":"Emitted platform events — the reconciliation log","description":"Queryable even when no webhook was active. Filters `type` (run.failed | run.recovered | test.ping), `delivered`, `limit` ≤ 200, keyset `cursor` from `next_cursor`.\n","parameters":[{"name":"type","in":"query","schema":{"type":"string"}},{"name":"delivered","in":"query","schema":{"type":"boolean"}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Events page"},"400":{"description":"invalid_webhook_events | invalid_cursor"},"401":{"description":"Not signed in"},"403":{"description":"webhook_scope_required | webhooks_not_enabled"}}}},"/v1/subjects/{subject_key}":{"delete":{"operationId":"deleteSubject","security":[{"bearer":[]}],"summary":"Erase a subject (tenant-scoped)","description":"Requires the `erase` key scope (`*` on the house env key also matches).\nSessions and `km_` user keys never receive `erase`. Normal Flow\ncapability keys cannot delete. Removes `entities`,\n`entity_identifiers`, and the `runs` subtree matched via\n`runs_subject_ix` (`tenant_id` + `subject_key`), plus R2 objects under\n`raw/…/{run}/`. Ledger rows are kept (no PII). Idempotent: unknown\nsubjects return zeros. Not in `openapi.es.yaml`.\n","parameters":[{"in":"path","name":"subject_key","required":true,"schema":{"type":"string"},"description":"Stable subject key (URL-encoded). Company: registrable domain.\nPerson: `email:<addr>` or `li:<slug>`.\n"}],"responses":{"200":{"description":"Erase counts (zeros when nothing matched)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubjectEraseResponse"}}}},"400":{"description":"invalid_subject_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"unauthorized. ErrorBody `detail`: missing | malformed | mismatch\n(plus static `hint`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"scope_denied — key lacks `erase`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"rate_limit_exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/admin/privacy/erase":{"post":{"operationId":"adminPrivacyErase","security":[{"bearer":[]}],"tags":["admin"],"summary":"Suppress a person globally and erase them in every tenant","description":"Operator only (`ADMIN_EMAILS` session or `kmadm_` token; `privacy:smoke`\ntokens may only use `*@smoke.kingminos.invalid`). Adds hashed `email:` /\n`li:` keys to `subject_suppressions`, then erases held runs / caches in\nevery tenant. Idempotent. Does not restore data. SoT: `docs/privacy-requests.md`.\nNot in `openapi.es.yaml`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPrivacyEraseRequest"}}}},"responses":{"200":{"description":"Suppressed (added may be 0 on a repeat)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPrivacyEraseResponse"}}}},"400":{"description":"invalid_body | missing_identifier | invalid_reason","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"not_signed_in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"admin_only | admin_scope_denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"not_configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/admin/privacy/suppressions":{"get":{"operationId":"adminListPrivacySuppressions","security":[{"bearer":[]}],"tags":["admin"],"summary":"List global subject suppressions","description":"Full admin only (not `privacy:smoke`). Paginated, newest first.\nEach row is `id` (64-char hex digest), `kind` (`email` | `li`;\n`liurl:` short links store as `li`), `reason`, `created_at`, `actor`.\n`hint` is present only when a non-PII hint is already stored\n(`email:@smoke.kingminos.invalid` for reserved smoke identities).\nNever returns raw emails or URLs. SoT: `docs/privacy-requests.md`.\n","parameters":[{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"cursor","required":false,"schema":{"type":"string"},"description":"Opaque keyset cursor from `next_cursor` (`created_at|id`)."}],"responses":{"200":{"description":"Page of suppressions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPrivacySuppressionListResponse"}}}},"400":{"description":"invalid_cursor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"not_signed_in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"admin_only | admin_scope_denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"not_configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/admin/privacy/suppressions/check":{"get":{"operationId":"adminCheckPrivacySuppression","security":[{"bearer":[]}],"tags":["admin"],"summary":"Check whether an identifier is suppressed","description":"Hashes `email` / `linkedin_url` server-side and reports whether that\ndigest is on the list. Never echoes the identifier. `privacy:smoke`\ntokens may only check `*@smoke.kingminos.invalid`.\n","parameters":[{"in":"query","name":"email","required":false,"schema":{"type":"string"}},{"in":"query","name":"linkedin_url","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"suppressed true or false (id is the digest when true)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPrivacySuppressionCheckResponse"}}}},"400":{"description":"missing_identifier","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"not_signed_in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"admin_only | admin_scope_denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"not_configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/admin/privacy/suppressions/{id}":{"delete":{"operationId":"adminDeletePrivacySuppression","security":[{"bearer":[]}],"tags":["admin"],"summary":"Lift a suppression (does not un-erase data)","description":"Removes the suppression row only. Previously erased caches and runs\nstay gone. Required `reason` is audited in logs. `privacy:smoke`\ntokens may only delete a reserved smoke-identity row.\n","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^[0-9a-f]{64}$"},"description":"64-char hex `subject_hash` from list or check."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPrivacySuppressionDeleteRequest"}}}},"responses":{"200":{"description":"Suppression removed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPrivacySuppressionDeleteResponse"}}}},"400":{"description":"invalid_id | invalid_body | missing_reason | invalid_reason","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"not_signed_in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"admin_only | admin_scope_denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"not_found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"503":{"description":"not_configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}}},"components":{"parameters":{"RouterExplain":{"in":"header","name":"X-Router-Explain","required":false,"schema":{"type":"string","enum":["default","full","minimal"]},"description":"Response verbosity. Omitted or `default` is the clean v2 envelope (no `trace`).\n`full` adds `trace` (decision, attempts, candidates, routing, writer, explain).\n`minimal` is accepted as an alias of `default` so Salesforce Flow\n`X-Router-Explain: minimal` keeps working. Same enum as `explain` query.\nIf both are set they must match after aliasing.\n"},"RouterExplainQuery":{"in":"query","name":"explain","required":false,"schema":{"type":"string","enum":["default","full","minimal"]},"description":"Same semantics as `X-Router-Explain`. Use when the caller cannot set a custom header\n(Clay HTTP columns). If both header and query are set they must match or the\nresponse is `400 invalid_explain`. A JSON body field `explain` is also\n`400 invalid_explain` (hint: use this query or `X-Router-Explain`, not the body).\n"},"RouterSchema":{"in":"header","name":"X-Router-Schema","required":false,"schema":{"type":"string","enum":["2"]},"description":"Request-pinned envelope version. Omit = current tip (today `2`).\nPin `2` locks the v2 envelope (`ok`, `schema_version`, `answer`,\n`es_decision`, `result` flats, `sources`, `usage` including `hops` /\n`providers_ran`). Unknown values are `400 unsupported_schema` (run\nnot started; Idempotency-Key not spent). Must agree with query\n`schema` and body `schema_version` when those are set.\nNamed Cred: optional static Custom Header `X-Router-Schema` = `2`.\n"},"RouterSchemaQuery":{"in":"query","name":"schema","required":false,"schema":{"type":"string","enum":["2"]},"description":"Same as `X-Router-Schema`. Use when the caller cannot set a custom header.\nHeader, query, and body `schema_version` must agree when more than one is set.\n"},"RouterCache":{"in":"header","name":"X-Router-Cache","required":false,"schema":{"type":"string","enum":["bypass"]},"description":"`company.domain` only. `bypass` skips the short-TTL in-isolate result-cache\nread so the ladder re-runs (still writes the fresh answer). Does **not**\nskip `Idempotency-Key` replay — omit or rotate that header on Janus\nfirst-100 re-smokes. Same effect as `Cache-Control: no-cache` / `no-store`\nor body `skip_cache: true`. Other values are `400 invalid_cache`.\n"},"IdempotencyKey":{"in":"header","name":"Idempotency-Key","required":false,"schema":{"type":"string"},"description":"Optional replay key. Same key + matching request hash replays a\ncompleted run (`replayed: true`). 429 / 503: back off and retry the\nsame key (the run was not started). After a paid hop, retry is\n`409 idempotency_spent`. Salesforce: declared on `companyDomain` so\nExternal Services can surface it; Named Cred Custom Header is the\nfallback if the import does not (`docs/salesforce-named-cred.md`).\n"},"CacheControl":{"in":"header","name":"Cache-Control","required":false,"schema":{"type":"string"},"description":"`company.domain`: a `no-cache` or `no-store` token (comma-separated)\nskips the result-cache read. Other tokens are ignored. Same effect as\n`X-Router-Cache: bypass` or body `skip_cache: true`.\n"}},"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"`Authorization: Bearer <ROUTER_API_KEY>` only. Bare keys are 401 malformed.\nSalesforce Named Credential Custom auth must emit this via Custom Headers\n(`docs/salesforce-named-cred.md`). Auth Parameters alone do not leave Salesforce.\n"}},"schemas":{"ProviderName":{"type":"string","enum":["zoominfo","leadmagic","websearch","findymail","wiza","aiark","dns","site_probe","favicon","wikidata","gleif","rdap","builtwith","prospeo","apollo","companyurlfinder","peopledatalabs","hginsights","brandfetch","enrichcrm","lemlist","snovio","cognism","lusha","starbridge","clay","databar","quickenrich","getleads","blitzapi"],"description":"Stable provider ids. `dns` / `site_probe` are keyless infrastructure hops on `company.domain` (0 credits). `favicon` is the keyless house favicon-fetch hop on `company.icon` (0 credits; hosted PNG on logos.kingminos.com).         `wikidata` / `gleif` / `rdap` are keyless registry hops on `company.domain` accuracy+ additional/family evidence (0 credits). GLEIF runs only after a Wikidata exact-one match with P1278. `builtwith` is paid coverage recall on `company.domain` (Relationships `rv5`, 1 credit / page 1; not on accuracy). `findymail` / `wiza` are miss-only company-name recovery hops on `auto` / `accuracy` / `coverage` (not in `value` linear order). `aiark` is registered but off company.resolve paths — `routing.only` / explicit `order` only. `prospeo` is BYOK company search (`POST /enrich-company`) and likewise off every default path — `routing.only` / explicit `order` only. `apollo` is BYOK company enrichment (`GET /organizations/enrich`, firmographic only) and likewise off every default path. `lemlist` is BYOK company search (`POST /api/database/companies`, company-website filter) and likewise off every default path. `cognism` / `lusha` / `starbridge` / `clay` / `databar` / `quickenrich` / `getleads` / `blitzapi` are credential vendors only (Your Keys store/verify; no routing hop yet) — `routing.only` / explicit order rejects them. Employment default presets are LeadMagic then ZoomInfo (`continueForContactId` id-append). Keys resolve from the OutboundSync credential broker (docs/os-router-credentials.md). House keys are tenant `default` only.\n"},"CapabilityId":{"type":"string","enum":["company.resolve","person.verify_employment","person.language","person.phone","person.title","company.domain","company.hierarchy","company.b2b_social","company.icon","company.description"]},"EngineId":{"type":"string","enum":["exact_one","rule_ladder","first_acceptable"]},"Strategy":{"type":"string","enum":["waterfall"]},"Confidence":{"type":["string","null"],"enum":["high","medium","low",null]},"MinConfidence":{"type":"string","enum":["high","medium","low"],"description":"Optional write floor. Overrides the path default (speed / coverage / no-path = low, balance = medium, accuracy = high). A candidate below the floor is withheld: the field is blank, `answer.confidence` stays the withheld value, candidates stay in evidence, and the run bills as that capability's miss.\n"},"EsDecision":{"type":"string","enum":["hit","miss","ambiguous","error","noop","reject"],"description":"Salesforce / stamp-safe branch. Mapped from capability outcome + reason. company.resolve `resolved` → hit only when a safe primary ZoomInfo id is present (`result.zoominfo_company_id`); `resolved_without_primary_id` → miss (do not stamp). Employment four-way send/block → hit even when `result.zoominfo_contact_id` is null (contact id is optional enrichment, not a stamp-safe-id gate). `hit` only stamps a primary ZoomInfo company id when `result.zoominfo_company_id` is present. Account Name fill-if-blank only when `es_decision=hit` **and** `result.company_name` non-blank. Budget / routing / compliance skips (`max_credits_reached`, `not_configured`, `insufficient_input`, `compliance_blocked`) → noop on both company.resolve and person.verify_employment — not error. company.hierarchy `subsidiary` / `ultimate_parent` → `hit`; `no_hierarchy_data` / `no_match` → `miss`; `sources_disagree` / `ambiguous` → `ambiguous`; `provider_unavailable` → `error`. company.domain `rejected` → `reject` (confident refusal, not an absence). Existing Account Flows branching `hit / miss / else` route `reject` to `else`. person.language `hit` → `hit`; `ambiguous` (low-only evidence) → `ambiguous`; `zero_hits` / `person_mismatch` → `miss`. person.title `hit` → `hit`; `fields_incomplete` / `ambiguous` → `ambiguous`; `zero_hits` / `person_mismatch` → `miss`. Do not use this field as the employment automation branch — use `decision.outcome`.\n"},"CompanyOutcome":{"type":"string","enum":["resolved","resolved_without_primary_id","no_decision"]},"EmploymentOutcome":{"type":"string","enum":["high_confidence_send","low_confidence_send","low_confidence_block","high_confidence_block","no_decision"]},"DomainOutcome":{"type":"string","enum":["verified","corrected","discovered","rejected","unverified","no_decision"],"description":"Product answer. `verified` / `corrected` / `discovered` are write-safe (`es_decision=hit`; `result.company_domain` / `verified_domain` stamped). `rejected` is a confident refusal of junk input (`es_decision=reject` — do not write `input_domain`). `unverified` is a hold (`es_decision=miss` — mismatch, unconfirmed, dead, parked, no_web, sources disagree). `no_decision` is an honest abstain. See docs/company-domain.md.\n"},"HierarchyOutcome":{"type":"string","enum":["subsidiary","ultimate_parent","no_decision"],"description":"Never “standalone”. No parent + no subsidiaries is `no_decision` / `no_hierarchy_data`."},"DomainClass":{"type":"string","enum":["clean","site_builder","directory","social_profile","personal_mailbox","isp_mailbox","invalid"]},"DecisionReason":{"type":"string","enum":["exact_one","zero_hits","multiple_hits","domain_mismatch","company_mismatch","no_domain","personal_mailbox","provider_error","max_credits_reached","insufficient_input","not_configured","compliance_blocked","email_validate","lm_job_change","ambiguous_employment","unresolvable_domain_class","hygiene_only","no_hierarchy_data","sources_disagree","no_match","ambiguous","provider_unavailable","person_mismatch","fields_incomplete"]},"ProviderOperation":{"type":"string","description":"Attempt operation ids. `company_search_by_name` is used by company.domain discover (ZoomInfo / LeadMagic / websearch) and Wikidata search. `entities_by_id` is Wikidata wbgetentities. `dns_lookup` / `site_probe` / RDAP `company_search_by_domain` are keyless company.domain hops. `corporate_hierarchy_enrich` is ZoomInfo `companies/corporate-hierarchy/enrich` on company.hierarchy. `company_linkedin_enrich` is scoped ZoomInfo `companies/enrich` (`socialMediaUrls`) on company.b2b_social (Harris 2026-10-06). `company_icon_by_domain` is house favicon-fetch, Brandfetch Brand API (icon>logo), and ZoomInfo scoped `companies/enrich` `logo` (Harris 2026-10-07). LeadMagic is not on the stamp path. `company_description_by_domain` is ZoomInfo BYOK scoped enrich (`description`) or websearch search (coverage default + `routing.only`). Default description firmographic is AI Ark `company_search_by_domain` only — LeadMagic is off this capability. Compose is `company_description_compose` (accuracy + coverage last-resort xAI). `person_profile_by_linkedin` is LeadMagic profile-search on person.language. `title_search_by_email` / `title_search_by_linkedin` are the person.title hops (ZoomInfo contacts/search BYOK, AI Ark, Findymail by email; LeadMagic / AI Ark by LinkedIn URL).\n","enum":["company_search_by_domain","company_search_by_name","contact_search_by_email","contact_search_by_name","email_validate","job_change_detector","email_to_profile","dns_lookup","site_probe","entities_by_id","corporate_hierarchy_enrich","company_linkedin_enrich","company_icon_by_domain","company_description_by_domain","person_profile_by_linkedin","title_search_by_email","title_search_by_linkedin"]},"AttemptStatus":{"type":"string","description":"Provider attempt ending. Maps to the four-way field vocabulary:\n- ok / cached ≈ completed (value found or replayed)\n- empty ≈ no_result (processing finished, nothing found)\n- error / rate_limited / timeout ≈ failed\n- skipped ≈ skipped (budget, not configured, or excluded by routing)\n","enum":["ok","empty","error","rate_limited","timeout","skipped","cached","rejected","lost"]},"BillingMode":{"type":"string","description":"Vendor billing for a hop. `always` = cost applies on each attempt (even empty);\n`on_success` = vendor bills only when a result is found. Request\n`max_provider_credits` is a reservation ceiling; `usage.provider_credits` is the\nreconciled bill after the run.\n","enum":["always","on_success"]},"CredentialSource":{"type":"string","enum":["house","byok"]},"WriterStatus":{"type":"string","enum":["skipped_dry_run","skipped_noop","skipped_no_decision","skipped_no_primary_id","skipped_not_found","skipped_already_set","written","error"]},"LinkedInHopSource":{"type":"string","enum":["request","zi_contact","identifiers","leadmagic_email_to_profile"]},"Identifier":{"type":"object","required":["provider","id_type","value"],"properties":{"provider":{"type":"string","description":"zoominfo | leadmagic | linkedin"},"id_type":{"type":"string","description":"company_id | contact_id | url (responses use snake_case id_type)"},"value":{"type":"string"}}},"IdentifierInput":{"type":"object","properties":{"provider":{"type":"string"},"id_type":{"type":"string","description":"Request alias for idType"},"idType":{"type":"string","description":"Request alias for id_type"},"value":{"type":"string"}}},"RoutingRequest":{"type":"object","properties":{"path":{"type":"string","enum":["speed","balance","value","auto","accuracy","coverage","fast"],"description":"Product path. Canonical names: `speed` | `balance` | `accuracy` | `coverage` (company.resolve / company.domain / company.b2b_social / company.description; omit = `balance` on each). Legacy aliases (deprecated): `value` / `fast` / `name_only` → `speed`, `auto` → `balance`. person.verify_employment omit = `balance` (`speed` | `balance` | `accuracy` only — legacy company aliases are `400 invalid_routing`). Company.resolve: `speed` = cheap names, no Wiza/Findymail; `accuracy` = gated corroboration; `coverage` = fill more empties. company.domain: Coverage = accuracy + BuiltWith Relationships (more ownership-proven additional_domains; often `partial`). Use CompanyDomainRoutingRequest on POST /v1/company/domain. Employment: `speed` = LeadMagic email validate only; `balance` = validate + paid LinkedIn lookup when no URL + LM job-change; `accuracy` = + AI Ark corroboration. After the employment layer, ZoomInfo may append `zoominfo_contact_id` on every path (estimate 0; does not lower LinkedIn caps). Findymail is BYOK Layer-1 via `only` / explicit order — never stacked after LeadMagic validate. `confidence` is not a path. Wins over `preset`. See docs/routing-presets.md.\n"},"preset":{"type":"string","enum":["speed","balance","value","auto","accuracy","coverage","name_only","zi_stamp","fast"],"description":"Back-compat alias of `path` plus `zi_stamp` (optional ZoomInfo-first). Company aliases: `name_only` and `fast` (deprecated → `speed`); `value` / `auto` (deprecated → `speed` / `balance`). Employment accepts `speed` | `balance` | `accuracy` only — `name_only` / `fast` / `zi_stamp` / `value` / `auto` / `coverage` are `400 invalid_routing`. Prefer `path` on new callers.\n"},"strategy":{"$ref":"#/components/schemas/Strategy"},"order":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"allow_fallbacks":{"type":"boolean"},"max_provider_credits":{"oneOf":[{"type":"number"},{"type":"object","additionalProperties":false,"properties":{"company.resolve":{"type":"number"},"person.verify_employment":{"type":"number"},"company.domain":{"type":"number"},"company.hierarchy":{"type":"number"},"company.b2b_social":{"type":"number"},"company.icon":{"type":"number"},"company.description":{"type":"number"}}}]},"free_first":{"type":"boolean"},"only":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"ignore":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"allow_paid_linkedin_lookup":{"type":"boolean","description":"LeadMagic email→profile when no LinkedIn URL is known, then job-change. Default-on for balance/accuracy. Speed stays off. Set false to opt out. Requires max_provider_credits ≥ 13 (find + job-change). Default balance reserves 14 (validate 1 + 10 + 3). See docs/routing-presets.md and STATUS Open / revisit.\n"}},"additionalProperties":false},"CompanyDomainRoutingRequest":{"type":"object","description":"company.domain routing. Canonical product paths are `speed` / `balance` / `accuracy` / `coverage`. Legacy aliases: `fast`/`value` → `speed`, `auto` → `balance`. Same other fields as RoutingRequest.\n","properties":{"path":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","fast","name_only"],"description":"company.domain product path. Omit = `balance`. Canonical: `speed` (hygiene → DNS → site_probe, 0 credits), `balance` (adds 0-credit ZoomInfo), `accuracy` (cap 2, LeadMagic + websearch when ZoomInfo misses; no BuiltWith), `coverage` (cap 3, accuracy + BuiltWith — more ownership-proven additional_domains, often `partial`). Legacy: `fast` / `value` / `name_only` → `speed`, `auto` → `balance`. See docs/company-domain.md.\n"},"preset":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","name_only","zi_stamp","fast"],"description":"Back-compat alias of `path` plus `zi_stamp` (optional ZoomInfo-first), `name_only` / `fast` (→ `speed`), and `auto` (→ `balance`). Prefer `path` on new callers.\n"},"strategy":{"$ref":"#/components/schemas/Strategy"},"order":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"allow_fallbacks":{"type":"boolean"},"max_provider_credits":{"oneOf":[{"type":"number"},{"type":"object","additionalProperties":false,"properties":{"company.resolve":{"type":"number"},"person.verify_employment":{"type":"number"},"company.domain":{"type":"number"},"company.hierarchy":{"type":"number"},"company.b2b_social":{"type":"number"},"company.icon":{"type":"number"},"company.description":{"type":"number"}}}]},"free_first":{"type":"boolean"},"only":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"ignore":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"allow_paid_linkedin_lookup":{"type":"boolean"}},"additionalProperties":false},"ComplianceRequest":{"type":"object","description":"Optional request-level vendor gate (not a routing path). Omit the object\nfor today's unconstrained behavior. Only keys set to `true` constrain;\nmissing keys and `false` are ignored. v1 fields: `soc2`, `data`.\nUnknown keys or non-boolean values → `400 invalid_compliance`.\nConstrained hops that lack a house attestation skip as\n`compliance_blocked` (not `provider_error`). Matrix: `docs/compliance.md`.\n","additionalProperties":false,"properties":{"soc2":{"type":"boolean","description":"When true, every considered hop must have a house SOC 2 attestation."},"data":{"type":"boolean","description":"When true, every considered hop must publish a customer DPA and/or a thorough public privacy policy / terms covering personal-data processing. Not a naked GDPR claim.\n"}}},"RoutingResponse":{"type":"object","required":["strategy","effective_order","allow_fallbacks","max_provider_credits","free_first","allow_paid_linkedin_lookup"],"properties":{"strategy":{"$ref":"#/components/schemas/Strategy"},"effective_order":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"allow_fallbacks":{"type":"boolean"},"max_provider_credits":{"type":"number"},"free_first":{"type":"boolean"},"allow_paid_linkedin_lookup":{"type":"boolean"},"path":{"type":"string","enum":["speed","balance","value","auto","accuracy","coverage"],"description":"Product path after expand (canonical). company.resolve, company.domain, company.b2b_social, company.icon, and company.description emit `speed` | `balance` | `accuracy` | `coverage`. person.verify_employment emits `speed` | `balance` | `accuracy`. Legacy request aliases are not echoed.\n"},"preset":{"type":"string","enum":["speed","balance","value","auto","accuracy","coverage","name_only","zi_stamp","fast"]},"miss_only_recovery":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"name_recovery":{"type":"string","enum":["none","miss_only","accuracy","coverage"]},"employment_recovery":{"type":"string","enum":["none","miss_only","accuracy","coverage"]}}},"AttributionField":{"type":"string","description":"Fixed allowlist of public result fields that may appear in `sources`. Rows are omitted when the value is null. Do not invent fields absent from this capability's public result.\n","enum":["company_name","company_domain","zoominfo_company_id","zoominfo_contact_id","person.email","person.first_name","person.last_name","person.job_title","person.linkedin_url","current_company.name","current_company.domain","expected_company_name","expected_company_domain","discovered_domain","canonical_domain","entity_key","immediate_parent_name","ultimate_parent_name","wikidata_id","linkedin_url","icon_url","icon_source","english","recommended_language","local_language","languages","description","email_status"]},"AttributionProvider":{"type":"string","enum":["zoominfo","leadmagic","websearch","findymail","wiza","aiark","dns","site_probe","favicon","wikidata","gleif","rdap","builtwith","prospeo","apollo","companyurlfinder","peopledatalabs","hginsights","brandfetch","enrichcrm","lemlist","snovio","cognism","lusha","starbridge","clay","databar","quickenrich","getleads","blitzapi","request"],"description":"Where the value came from. `request` is documented request-echo (expected_company_* only). Provider-sourced values use the ProviderName that filled the field (`zoominfo` | `leadmagic` | `websearch` | `findymail` | `wiza` | `aiark`).\n"},"AttributionOperation":{"oneOf":[{"$ref":"#/components/schemas/ProviderOperation"},{"type":"string","enum":["request"]}],"description":"Provider operation that produced the value, or `request` for request-echo."},"FieldAttribution":{"type":"object","description":"Explain-only: where a populated public result field came from (SEP / Clay / Flow). Not a dump of attempts. Machines must not use sources as control flow.\n","required":["field","provider","operation","observed_at"],"properties":{"field":{"$ref":"#/components/schemas/AttributionField"},"provider":{"$ref":"#/components/schemas/AttributionProvider"},"operation":{"$ref":"#/components/schemas/AttributionOperation"},"observed_at":{"type":"string","format":"date-time","description":"When this field was observed (attempt time, else run time)"}}},"Answer":{"type":"object","description":"The one block to read. Employment automations branch on `outcome`. Salesforce / stamp-safe callers use `es_decision` (also top-level). `summary` is explain-only — machines must not use it as control flow.\n","required":["outcome","reason_code","es_decision","confidence","summary"],"properties":{"outcome":{"type":"string","description":"Capability outcome. Employment automations branch here: high_confidence_send | low_confidence_send | low_confidence_block | high_confidence_block | no_decision. Company resolve: resolved | resolved_without_primary_id | no_decision.\n"},"reason_code":{"type":"string","description":"Closed reason (decision.reason, or company.domain domain_verdict / reject_reason when more specific).\n"},"es_decision":{"$ref":"#/components/schemas/EsDecision"},"confidence":{"allOf":[{"$ref":"#/components/schemas/Confidence"}],"nullable":true},"summary":{"type":"string","description":"Human one-liner (provider / path / outcome; notes material skips such as \"LeadMagic not called\" only when LeadMagic was planned). Present on every completed decision. Explain only — control stays `answer.outcome` / `es_decision` / reason_code.\n"},"operator":{"allOf":[{"$ref":"#/components/schemas/CompanyOperator"}],"nullable":true,"description":"company.resolve only — same block as `result.operator`."},"identity":{"type":"object","nullable":true,"description":"company.description only. Identity basis for the stamp. Null on an unjudged hit (still a hit when confidence meets the floor).\n","properties":{"method":{"type":"string","enum":["jev","name_domain","own_homepage"]},"score":{"type":"number","nullable":true}}}}},"Decision":{"type":"object","description":"Capability decision. Employment automations branch on `outcome`. Salesforce / stamp-safe callers use top-level `es_decision` instead. `summary` is explain-only — machines must not use it as control flow. Do not parse nested evidence or attempts for control flow.\n","required":["engine","outcome","reason","path","confidence","candidate_count","deciding_provider","deciding_attempt_seq","summary"],"properties":{"engine":{"$ref":"#/components/schemas/EngineId"},"outcome":{"type":"string","description":"Capability outcome. Employment automations branch here: high_confidence_send | low_confidence_send | low_confidence_block | high_confidence_block | no_decision. Company resolve: resolved | resolved_without_primary_id | no_decision. Do not parse nested evidence or attempts for control flow.\n"},"reason":{"$ref":"#/components/schemas/DecisionReason"},"path":{"type":"string","nullable":true,"description":"e.g. zi_domain, lm_email_validate, fm_email_validate, lm_job_change"},"confidence":{"allOf":[{"$ref":"#/components/schemas/Confidence"}],"nullable":true},"candidate_count":{"type":"integer","minimum":0},"deciding_provider":{"allOf":[{"$ref":"#/components/schemas/ProviderName"}],"nullable":true},"deciding_attempt_seq":{"type":"integer","nullable":true,"minimum":1}}},"Attempt":{"type":"object","required":["seq","provider","operation","credential_source","status","hit_count","http_status","latency_ms","provider_credits","price_micro_usd","r2_key","error_code","error_message"],"properties":{"seq":{"type":"integer","minimum":1},"provider":{"$ref":"#/components/schemas/ProviderName"},"operation":{"$ref":"#/components/schemas/ProviderOperation"},"credential_source":{"$ref":"#/components/schemas/CredentialSource"},"status":{"$ref":"#/components/schemas/AttemptStatus"},"hit_count":{"type":"integer","minimum":0},"http_status":{"type":"integer","nullable":true},"latency_ms":{"type":"integer","minimum":0},"provider_credits":{"type":"number","minimum":0},"price_micro_usd":{"type":"integer","minimum":0},"r2_key":{"type":"string","nullable":true},"error_code":{"type":"string","nullable":true},"error_message":{"type":"string","nullable":true},"rate_limit":{"type":"object","description":"Honest ZoomInfo `X-RateLimit-*` fields from this hop. Omitted when the vendor sent none (isolate-bucket skip has no headers). Never invents remaining/limit from the registry.\n","properties":{"source":{"type":"string","enum":["response_headers"]},"remaining_second":{"type":"number"},"remaining_hour":{"type":"number"},"remaining_day":{"type":"number"},"limit_second":{"type":"number"},"limit_hour":{"type":"number"},"limit_day":{"type":"number"},"retry_after":{"type":"number"}}}}},"Writer":{"type":"object","required":["name","status","ref","error"],"properties":{"name":{"type":"string","enum":["noop"]},"status":{"$ref":"#/components/schemas/WriterStatus"},"ref":{"type":"string","nullable":true},"error":{"type":"string","nullable":true}}},"Usage":{"type":"object","required":["credits","hops","providers_ran","cached","cached_at","cache_age_seconds"],"properties":{"credits":{"type":"object","required":["cap","spent","by_provider"],"description":"cap = resolved max_provider_credits (0 on company.domain speed/balance is the path reservation, not a broken counter). spent / by_provider are charged ledger amounts. ZoomInfo search is 0.\n","properties":{"cap":{"type":"number","minimum":0},"spent":{"type":"number","minimum":0},"by_provider":{"type":"object","additionalProperties":{"type":"number"}}}},"hops":{"type":"array","description":"Every attempt this run (including skip). ES / Named Cred callers without `trace` can tell ZoomInfo `ok` vs `empty` vs `skipped`. Errors include `error_code` / `error_message` (e.g. ZI0005) and `host` when the hop queried a domain.\n","items":{"type":"object","required":["provider","status","operation","credits","error_code","error_message","host"],"properties":{"provider":{"$ref":"#/components/schemas/ProviderName"},"status":{"type":"string","enum":["ok","empty","error","rate_limited","timeout","skipped","cached","rejected","lost"]},"operation":{"type":"string"},"credits":{"type":"number","minimum":0},"error_code":{"type":"string","nullable":true},"error_message":{"type":"string","nullable":true},"host":{"type":"string","nullable":true,"description":"Query host when present (stamped vs input)."}}}},"providers_ran":{"type":"array","description":"Providers that executed (vendor contact). 0-credit ZoomInfo still appears.","items":{"$ref":"#/components/schemas/ProviderName"}},"cached":{"type":"boolean"},"cached_at":{"type":"string","format":"date-time","nullable":true},"cache_age_seconds":{"type":"integer","nullable":true,"minimum":0}}},"CompanyEntity":{"type":"object","required":["domain","name","website","linkedin_url","identifiers"],"properties":{"domain":{"type":"string"},"name":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"CompanyResult":{"type":"object","required":["company_name","company_domain","zoominfo_company_id","website","linkedin_url","identifiers"],"properties":{"company_name":{"type":"string","nullable":true,"description":"Flat resolved company name (key always present; null on miss).\nAccount Name fill-if-blank when present — including on\n`es_decision=miss` (websearch-only name). Do not wait for hit.\nStamp id only on hit + non-blank `zoominfo_company_id`.\n"},"company_domain":{"type":"string","nullable":true,"description":"Flat resolved company domain (key always present; null on miss).\nOptional CRM hygiene — never clobber an existing Website/domain.\n"},"zoominfo_company_id":{"type":"string","nullable":true,"description":"Flat ZoomInfo company id (null when absent).\nSalesforce stamp only when `es_decision=hit` and this value is non-blank.\n"},"website":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}},"name_dissent":{"type":"array","items":{"type":"object","required":["provider","name"],"properties":{"provider":{"$ref":"#/components/schemas/ProviderName"},"name":{"type":"string"}}}},"operator":{"allOf":[{"$ref":"#/components/schemas/CompanyOperator"}],"nullable":true,"description":"Who owns / operates the site behind the brand (fizzy.do → 37signals).\nAdditive; never changes `company_name`. Null when the operator is the\nbrand itself or no aligned ownership claim was found. 0 credits.\nSoT: docs/company-operator.md.\n"}}},"CompanyOperator":{"type":"object","additionalProperties":false,"required":["operator_name","operator_domain","operator_source","operator_confidence","operator_zoominfo_company_id"],"properties":{"operator_name":{"type":"string","description":"Operator as written on the site, legal suffix dropped (`37signals`)."},"operator_domain":{"type":"string","description":"Registrable domain linked on the site whose label matches the name."},"operator_source":{"type":"string","enum":["copyright_footer","preheader","makers_of","footer_link","zoominfo"],"description":"Strongest name-bearing claim. `copyright_footer` (© YEAR X LLC/Inc),\n`preheader` (\"from X\" in the title or top of page), `makers_of`\n(\"X, makers of …\" / \"a X product\"). `footer_link` and `zoominfo`\nare reserved (supporting evidence today; not emitted as the source).\n"},"operator_confidence":{"type":"string","enum":["high","medium","low"],"description":"high = two independent claims (e.g. copyright + preheader / makers_of /\nfooter link). medium = one claim plus twitter:creator. low = one claim.\n"},"operator_zoominfo_company_id":{"type":"string","nullable":true,"description":"ZoomInfo company id for `operator_domain` (0-credit exact_one search; null when ZoomInfo is off this call or misses)."}}},"DomainResult":{"type":"object","required":["company_name","company_domain","zoominfo_company_id","discovered_domain","input_domain","corrected_via","domain_class","rule_id","canonical_domain","entity_key","alternate_domains","family_policy","account_primary_domain","account_key","additional_domains","review_domains","related_domains","parent_domain","additional_domains_status","website","linkedin_url","identifiers"],"properties":{"company_name":{"type":"string","nullable":true,"description":"CRM-safe display name when we have one. On a trusted domain stamp, prefers a gated site-claim brand (og:site_name or title brand keyword-strong / name-tied to the stamped host). One weak title alone does not stamp. Null when we have no writeable name.\n"},"company_domain":{"type":"string","nullable":true,"description":"Trusted host (replaces verified_domain). Null unless the outcome is trusted (`verified` / `corrected` / `discovered`). Overlay finds are discovered_domain only. Map HubSpot Company domain / website here only when non-null. HubSpot `domain` (account primary) maps from account_primary_domain — the two can differ under collapse_brand.\n"},"zoominfo_company_id":{"type":"string","nullable":true,"description":"ZoomInfo company id whenever this run used a ZI company hit (corroboration or discover). Not gated on trusted outcomes or `zi_stamp`. Optional id stamp. Null when ZoomInfo missed or the only ZI id belongs to a different host than the stamped domain (R10 corrected away from the ZI host).\n"},"discovered_domain":{"type":"string","nullable":true,"description":"Half-trusted name→domain find on overlay rows (junk reject, name mismatch, dead, or no-web host). Review / Cap / Clay side column only — do not write as company_domain or HubSpot domain. Null when the outcome is trusted (the write lives on company_domain) or when discovery abstained.\n"},"input_domain":{"type":"string","nullable":true,"description":"What the caller sent (normalized host when we could parse one). Never write this to HubSpot when outcome is rejected. Equals `input_domain` when `answer.reason_code` is `corrected`.\n"},"corrected_via":{"type":"string","nullable":true,"enum":["redirect","cname","canonical_link","og_url"],"description":"How the corrected host was proven (HTTP redirect, DNS CNAME, rel=canonical, or og:url). Null unless outcome is corrected.\n"},"domain_class":{"allOf":[{"$ref":"#/components/schemas/DomainClass"}],"nullable":true},"rule_id":{"type":"string","description":"Greppable rule id (e.g. R10_corrected, R04_social_profile). Audit / support — not a write gate. See docs/company-domain.md.\n"},"canonical_domain":{"type":"string","nullable":true,"description":"Read-only. Vendor primary, the stamped host, or the sole considered host. Not a write-gate substitute — do not treat as company_domain unless verified_domain is also stamped.\n"},"entity_key":{"type":"string","nullable":true,"description":"Read-only join key. `zoominfo:{id}` when a ZoomInfo company id is in hand, else `domain:{canonical_domain}`. Not a CRM write.\n"},"alternate_domains":{"type":"array","items":{"$ref":"#/components/schemas/AlternateDomain"},"description":"Read-only other hosts (input / vendor_primary / redirect / discovered) with attributions. Empty when only one host was considered. Never stamps company_domain.\n"},"family_policy":{"type":"string","enum":["collapse_brand","separate_regional"],"description":"Echo of the request policy (default collapse_brand). Controls whether regionals land in additional_domains or stay a child (`separate_regional` + parent_domain).\n"},"account_primary_domain":{"type":"string","nullable":true,"description":"Account primary under family_policy. Null when no trusted stamp. HubSpot `domain` maps here. company_domain / verified_domain stay the row's trusted host and can differ when collapse_brand lifts a regional to a stronger brand primary.\n"},"account_key":{"type":"string","nullable":true,"description":"List-level join key. `zoominfo:{id}` of the account primary when proven, else `domain:{account_primary_domain}`. Distinct from row-level entity_key. Not a CRM write.\n"},"additional_domains":{"type":"array","items":{"type":"string"},"description":"Ownership-proven write-safe aliases / regionals only. Caller joins: HubSpot `hs_additional_domains` ← additional_domains.join(\";\"). Brand-tied + live without ownership never appears here (see review_domains). Not “all domains found.”\n"},"review_domains":{"type":"array","items":{"$ref":"#/components/schemas/ReviewDomain"},"description":"Brand-tied and live but ownership not proven. Human review column — do not merge into HubSpot hs_additional_domains.\n"},"related_domains":{"type":"array","items":{"$ref":"#/components/schemas/RelatedDomain"},"description":"Subsidiary / parent / sister (and regional under separate_regional). Parent/child review — not a domain merge.\n"},"parent_domain":{"type":"string","nullable":true,"description":"HubSpot parent-company lookup key when this row is the child. Null unless a parent (or, under separate_regional, a brand primary) is set.\n"},"additional_domains_status":{"type":"string","enum":["complete","partial","skipped"],"description":"skipped = no trusted stamp (lists are empty). complete = the published alias sweep finished. partial = we stopped early (BuiltWith more_results, credit-cap skip, or time budget) — write the additional_domains we did prove; do not treat the list as exhaustive; not cached as complete.\n"},"website":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"AlternateDomain":{"type":"object","required":["domain","source","role","name_tied","relationship","evidence"],"properties":{"domain":{"type":"string"},"source":{"type":"string","enum":["domain","website","email","zoominfo","leadmagic","websearch","dns","site_probe","wikidata","gleif","rdap"]},"role":{"type":"string","enum":["input","vendor_primary","redirect","discovered","canonical_link","og_url","hreflang","tracking_id","variant","registry","vendor_family"]},"name_tied":{"type":"boolean"},"relationship":{"type":"string","enum":["alias","regional","subsidiary","parent","sister","unknown"]},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/DomainEvidence"}}}},"ReviewDomain":{"type":"object","description":"A host that looks brand-related and is live, but we could not prove ownership. Hold for a human — do not merge into HubSpot hs_additional_domains.\n","required":["domain","reason","evidence"],"properties":{"domain":{"type":"string"},"reason":{"type":"string","description":"Why this host is held. Today `brand_tied_live_unproven`.\n"},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/DomainEvidence"}}}},"RelatedDomain":{"type":"object","description":"A corporate-family host (subsidiary, parent, sister; regional when family_policy is separate_regional). Use for parent/child review, not a HubSpot additional-domain merge.\n","required":["domain","relationship","evidence"],"properties":{"domain":{"type":"string"},"relationship":{"type":"string","enum":["regional","subsidiary","parent","sister"]},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/DomainEvidence"}}}},"DomainEvidence":{"type":"string","description":"Why we classified a host. Ownership tags (redirect / canonical / og / shared tracking / same ZoomInfo id / self-hosted NS/MX / registry official website / same RDAP registrant) can put a host on additional_domains. `brand_tied` + `live` without ownership stay on review_domains. Vendor and registry claims are evidence, never the decision.\n","enum":["redirect_to_primary","canonical_to_primary","og_url_to_primary","shared_tracking_id","primary_hreflang_lists","hreflang_to_primary","same_zoominfo_company_id","self_hosted_ns_or_mx","registry_official_website","rdap_same_registrant_org","vendor_same_entity","different_zoominfo_company_id","registry_subsidiary","registry_parent","registry_shared_parent","vendor_subsidiary","vendor_parent","brand_tied","live"]},"DomainExplain":{"type":"object","description":"Structured company.domain explain payload. Machines must not use it as control flow (`decision.summary` is also explain-only). No 0–100 scores.\n","required":["rule_id","rule_why","sources","name_match","candidates","liveness"],"properties":{"rule_id":{"type":"string"},"rule_why":{"type":"string","description":"Deterministic one-liner for why this rule_id fired."},"sources":{"type":"array","items":{"$ref":"#/components/schemas/DomainExplainSource"}},"name_match":{"$ref":"#/components/schemas/DomainExplainNameMatch"},"candidates":{"type":"array","items":{"$ref":"#/components/schemas/AlternateDomain"}},"liveness":{"$ref":"#/components/schemas/DomainExplainLiveness"},"family_notes":{"type":"array","items":{"type":"string"},"description":"Explain-only lines for non-unknown account-identity domains."}}},"DomainExplainSource":{"type":"object","required":["source","raw","domain","domain_class","name_tied"],"properties":{"source":{"type":"string","enum":["domain","website","email"]},"raw":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true},"domain_class":{"allOf":[{"$ref":"#/components/schemas/DomainClass"}],"nullable":true},"name_tied":{"type":"boolean"}}},"DomainExplainNameMatch":{"type":"object","required":["input_name","domain_keyword","tied","keyword_strong","locale_tied","vendor_name","vendor_agrees","vendor_conflicts","title","title_agrees","title_conflicts","signals"],"properties":{"input_name":{"type":"string","nullable":true},"domain_keyword":{"type":"string","nullable":true},"tied":{"type":"boolean"},"keyword_strong":{"type":"boolean"},"locale_tied":{"type":"boolean"},"vendor_name":{"type":"string","nullable":true},"vendor_agrees":{"type":"boolean"},"vendor_conflicts":{"type":"boolean"},"title":{"type":"string","nullable":true},"title_agrees":{"type":"boolean"},"title_conflicts":{"type":"boolean"},"signals":{"type":"array","items":{"type":"string"},"description":"Named signals only (locale_tld, compact_equal, first_token_only, …)"}}},"DomainExplainLiveness":{"type":"object","required":["dns_status","dead","no_web","parked","nameservers","probe_title","probe_final_host"],"properties":{"dns_status":{"type":"string","nullable":true,"enum":["ok","nxdomain","no_records","error","skipped"]},"dead":{"type":"boolean","description":"True NXDOMAIN (DoH Status==3) only."},"no_web":{"type":"boolean","description":"Registered (not NXDOMAIN) with no usable A/AAAA/CNAME/MX at apex or www.\n"},"parked":{"type":"boolean"},"nameservers":{"type":"array","items":{"type":"string"}},"probe_title":{"type":"string","nullable":true},"probe_final_host":{"type":"string","nullable":true}}},"PersonEntity":{"type":"object","required":["email","first_name","last_name","job_title","linkedin_url","identifiers"],"properties":{"email":{"type":"string","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"job_title":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"CurrentCompany":{"type":"object","required":["name","domain","identifiers"],"properties":{"name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"EmploymentEvidenceMatchedCandidate":{"type":"object","required":["first_name","last_name","job_title","company_name","company_domain","company_identifiers","score","freshness_at","start_date","days_at_job"],"properties":{"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"job_title":{"type":"string","nullable":true},"company_name":{"type":"string","nullable":true},"company_domain":{"type":"string","nullable":true},"company_identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}},"score":{"type":"number","nullable":true},"freshness_at":{"type":"string","format":"date-time","nullable":true},"start_date":{"type":"string","nullable":true,"description":"Vendor current-position start date (LeadMagic `current_position.start_date`), when the hop returned one"},"days_at_job":{"type":"number","nullable":true,"description":"Vendor tenure in days (LeadMagic `current_position.days_at_job`), when the hop returned one"}}},"EmploymentEvidence":{"type":"object","required":["provider","operation","candidate_count","provider_status","provider_reason","summary","linkedin_source","matched_candidate"],"properties":{"provider":{"$ref":"#/components/schemas/ProviderName"},"operation":{"$ref":"#/components/schemas/ProviderOperation"},"candidate_count":{"type":"integer","minimum":0},"provider_status":{"type":"string","nullable":true},"provider_reason":{"type":"string","nullable":true},"summary":{"type":"string","nullable":true},"linkedin_source":{"allOf":[{"$ref":"#/components/schemas/LinkedInHopSource"}],"nullable":true,"description":"How the LinkedIn URL was resolved for LeadMagic job-change (when applicable)"},"matched_candidate":{"allOf":[{"$ref":"#/components/schemas/EmploymentEvidenceMatchedCandidate"}],"nullable":true}}},"EmploymentExplain":{"type":"object","description":"person.verify_employment explain. Present when `X-Router-Explain` or `?explain=` is `minimal` or `full`. Explain-only — not control flow.\n","required":["email_status","risks","employment","job_change_status","ai_ark","soft_miss","conflict","conflict_kind","speed_domain_check","expected_company_raw","expected_company_cleaned","expected_company_correction"],"properties":{"email_status":{"type":"string","nullable":true},"risks":{"type":"object","required":["catch_all","role","seg"],"properties":{"catch_all":{"type":"boolean"},"role":{"type":"boolean"},"seg":{"type":"boolean"}}},"employment":{"type":"string","enum":["still_there","left","unknown","skipped"]},"job_change_status":{"type":"string","nullable":true,"enum":["NO_CHANGE","JOB_CHANGE_DETECTED","NEVER_WORKED_THERE","AMBIGUOUS_CURRENT_EMPLOYMENT","CURRENT_EMPLOYMENT_UNKNOWN","PROFILE_NOT_FOUND",null]},"ai_ark":{"type":"string","enum":["corroborated","contradicted","empty","error","skipped"]},"soft_miss":{"type":"string"},"conflict":{"type":"boolean"},"conflict_kind":{"type":"string","nullable":true,"enum":["email_valid_employment_left","employment_still_aiark_contradicted","email_invalid_employment_still",null]},"speed_domain_check":{"type":"string","nullable":true,"description":"Speed path only — mailbox domain vs the corrected expected company domain","enum":["match","domain_mismatch","personal_mailbox","no_expected_domain","no_mailbox_domain","invalid_email",null]},"expected_company_raw":{"type":"object","required":["domain","name"],"properties":{"domain":{"type":"string","nullable":true},"name":{"type":"string","nullable":true}}},"expected_company_cleaned":{"type":"object","required":["domain","name"],"properties":{"domain":{"type":"string","nullable":true},"name":{"type":"string","nullable":true}}},"expected_company_correction":{"type":"object","required":["reason","via","rule_id","corrected_from"],"properties":{"reason":{"type":"string","enum":["unchanged","hygiene_normalized","corrected","unusable","name_only"]},"via":{"type":"string","nullable":true,"enum":["redirect","cname","canonical_link","og_url",null]},"rule_id":{"type":"string","nullable":true},"corrected_from":{"type":"string","nullable":true}}}}},"EmploymentResult":{"type":"object","required":["person","current_company","company_name","company_domain","expected_company_name","expected_company_domain","zoominfo_company_id","zoominfo_contact_id","employment","soft_miss","identifiers"],"properties":{"person":{"allOf":[{"$ref":"#/components/schemas/PersonEntity"}],"nullable":true,"description":"Known person fields (title / LinkedIn when present). Present on changed even when current_company is null."},"current_company":{"allOf":[{"$ref":"#/components/schemas/CurrentCompany"}],"nullable":true,"description":"Current employer when the provider supplied one. On `changed`, this is the new employer or explicitly null when unknown — key is always present; never invented.\n"},"company_name":{"type":"string","nullable":true,"description":"Flat current-company name; null when current employer is unknown"},"company_domain":{"type":"string","nullable":true,"description":"Flat current-company domain; null when current employer is unknown"},"expected_company_name":{"type":"string","nullable":true,"description":"Name from the request expected_company (nullable)"},"expected_company_domain":{"type":"string","nullable":true,"description":"Domain from the request expected_company (nullable)"},"evidence":{"allOf":[{"$ref":"#/components/schemas/EmploymentEvidence"}],"nullable":true,"description":"Audit/debug only — do not parse for automation control flow. Present on default / `X-Router-Explain: full`. Omitted on `minimal`.\n"},"zoominfo_company_id":{"type":"string","nullable":true},"zoominfo_contact_id":{"type":"string","nullable":true,"description":"Optional ZoomInfo contact id attached after the employment decision (`continueForContactId`). Presence is not required for `es_decision: hit`. Id-append only — does not change `decision.outcome`, `result.employment`, or confidence. Null when ZoomInfo misses, errors, or is not configured.\n"},"email_validation":{"type":"object","nullable":true,"description":"Layer-1 bounce / catch-all / SEG / role fields when present","properties":{"email_status":{"type":"string","nullable":true},"is_domain_catch_all":{"type":"boolean","nullable":true},"is_role_based":{"type":"boolean","nullable":true},"mx_security_gateway":{"type":"boolean","nullable":true},"mx_gateway":{"type":"string","nullable":true},"mx_gateway_type":{"type":"string","nullable":true},"mx_provider":{"type":"string","nullable":true},"mx_record":{"type":"string","nullable":true}}},"employment":{"type":"string","enum":["still_there","left","unknown"],"description":"Layer-2 employment signal from job-change when that hop ran. Send/block is `answer.outcome`. Catch-all / SEG / role live on `email_validation` and `answer.summary`.\n"},"soft_miss":{"type":"string","enum":["still_there_clear","left_clear","ambiguous_current","never_worked","missing_linkedin","email_ok_job_unknown","vendor_conflict","catch_all_or_seg_or_role","insufficient_input","not_configured","compliance_blocked","max_credits_reached","provider_error","other"],"description":"Deterministic label on every completed run. Not a fifth send/block outcome. `vendor_conflict` when email / employment / AI Ark disagree.\n"},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"Candidate":{"type":"object","required":["id","provider","provider_record_id","name","domain","website","identifiers"],"properties":{"id":{"type":"string"},"provider":{"$ref":"#/components/schemas/ProviderName"},"provider_record_id":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"RunEnvelopeBase":{"type":"object","description":"Live RunResponse envelope (schema_version 2). Automations branch on answer.outcome (employment) or es_decision (Salesforce / stamp-safe). `trace` is audit/debug only (`explain=full`). Business misses stay HTTP 200.\n","required":["ok","schema_version","run_id","capability","capability_version","dry_run","replayed","answer","es_decision","result","sources","usage","input","routing","provenance","external_ref","status","error","latency_ms","created_at"],"properties":{"ok":{"type":"boolean","enum":[true]},"schema_version":{"type":"string","enum":["2"]},"run_id":{"type":"string"},"capability":{"$ref":"#/components/schemas/CapabilityId"},"capability_version":{"type":"integer","minimum":1},"dry_run":{"type":"boolean"},"replayed":{"type":"boolean"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"}],"nullable":true},"es_decision":{"allOf":[{"$ref":"#/components/schemas/EsDecision"}],"nullable":true},"result":{"description":"Flat per-capability result. Null on incomplete GET /v1/runs/:id."},"input":{"type":"object","additionalProperties":true,"description":"Normalized request echo."},"sources":{"type":"array","items":{"$ref":"#/components/schemas/FieldAttribution"},"description":"Explain-only sources for populated public fields. Not control flow."},"provenance":{"type":"string","nullable":true,"description":"Stable compact provenance for Salesforce paste / future writer. Present on successful company.resolve, person.verify_employment, person.language, person.phone, and person.title decisions only. Example: router:run_abc;capability=company.resolve;provider=zoominfo;company_id=123456789;domain=outboundsync.com\n"},"compliance":{"$ref":"#/components/schemas/ComplianceRequest","description":"Echo of active request constraints (`true` keys only). Omitted when unconstrained."},"external_ref":{"type":"string","nullable":true},"routing":{"$ref":"#/components/schemas/RoutingResponse"},"status":{"type":"string","enum":["running","completed","failed"],"description":"Run lifecycle. Capability POST responses are always `completed`."},"error":{"type":"string","nullable":true,"description":"Failure message on `failed` GET /v1/runs/:id. Null otherwise."},"usage":{"$ref":"#/components/schemas/Usage"},"latency_ms":{"type":"integer","minimum":0},"created_at":{"type":"string","format":"date-time"},"guide":{"type":"string","description":"Tiny pointer to GET /v1/recipes/guidance. Usually `/v1/recipes/guidance`. A blank-below-floor or low answer may add a section hash (`#below_floor`, `#low`).\n"},"trace":{"$ref":"#/components/schemas/RunTrace","description":"Present only when `X-Router-Explain` / `?explain=` is `full`."},"next_steps":{"type":"array","items":{"$ref":"#/components/schemas/NextStep"},"description":"Machine-readable follow-ups for AI agents. Always present on completed runs (empty when there is nothing useful to do). Survives default explain. Actions: retry_with_dial | call_capability | supply_input | ask_user | review_candidates | verify_source | verify_linkedin_page | report_provider_error. Not in `openapi.es.yaml`.\n"},"candidates":{"type":"array","items":{"$ref":"#/components/schemas/GuidanceCandidate"},"description":"Miss / ambiguous candidates, including `linkedin_url`. Present only when the run did not hit and at least one candidate existed. Survives default explain — do not require `trace`.\n"},"feedback_hint":{"type":"string","description":"Agent clients only (requests that send `X-KingMinos-Client`, e.g. the KingMinos MCP server and CLI). One sentence pointing at `submitFeedback`, present on miss / ambiguous / error / reject and low-confidence answers; absent on hits and noop skips. Never sent to callers without the header (Salesforce Flow / External Services).\n"}}},"NextStep":{"type":"object","required":["action","reason"],"properties":{"action":{"type":"string","enum":["retry_with_dial","call_capability","supply_input","ask_user","review_candidates","verify_source","verify_linkedin_page","report_provider_error"]},"capability":{"$ref":"#/components/schemas/CapabilityId"},"suggested":{"type":"object","additionalProperties":true,"description":"Suggested request fields (domain, routing.path, …)."},"reason":{"type":"string"},"est_credits":{"type":"number","nullable":true,"description":"Expected credits for the suggested call (not the path ceiling)."},"max_credits":{"type":"number","nullable":true,"description":"Path credit ceiling for the suggested call."}}},"GuidanceCandidate":{"type":"object","required":["id","provider","provider_record_id","name","domain","website","linkedin_url","identifiers"],"properties":{"id":{"type":"string"},"provider":{"$ref":"#/components/schemas/ProviderName"},"provider_record_id":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"RecipesResponse":{"type":"object","required":["ok","guide","ask_before_bulk","recipes"],"properties":{"ok":{"type":"boolean","enum":[true]},"guide":{"type":"string","description":"Pointer to GET /v1/recipes/guidance","example":"/v1/recipes/guidance"},"ask_before_bulk":{"type":"object","required":["question","field","options"],"properties":{"question":{"type":"string"},"field":{"type":"string","enum":["priority"]},"options":{"type":"array","items":{"type":"object","required":["id","label","path","when","est_credits_per_row"],"properties":{"id":{"type":"string","enum":["speed","balance","accuracy","coverage"]},"label":{"type":"string"},"path":{"type":"string","enum":["speed","balance","accuracy","coverage"]},"when":{"type":"string"},"est_credits_per_row":{"type":"number"}}}}}},"recipes":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}}}},"Recipe":{"type":"object","required":["id","title","when","ask_user_first","dials","steps","notes"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"when":{"type":"string"},"ask_user_first":{"type":"boolean"},"dials":{"type":"array","items":{"$ref":"#/components/schemas/RecipeDial"}},"steps":{"type":"array","items":{"$ref":"#/components/schemas/RecipeStep"}},"notes":{"type":"array","items":{"type":"string"}}}},"RecipeDial":{"type":"object","required":["priority","path","est_credits_per_row","note"],"properties":{"priority":{"type":"string","enum":["speed","balance","accuracy","coverage"]},"path":{"type":"string","enum":["speed","balance","accuracy","coverage"]},"est_credits_per_row":{"type":"number"},"note":{"type":"string"}}},"RecipeStep":{"type":"object","required":["capability","when","required_input","pass","omit","est_credits"],"properties":{"capability":{"$ref":"#/components/schemas/CapabilityId"},"when":{"type":"string"},"required_input":{"type":"array","items":{"type":"string"}},"pass":{"type":"array","items":{"type":"string"}},"omit":{"type":"array","items":{"type":"string"}},"note":{"type":"string"},"est_credits":{"type":"object","required":["speed","balance","accuracy","coverage"],"properties":{"speed":{"type":"number"},"balance":{"type":"number"},"accuracy":{"type":"number"},"coverage":{"type":"number"}}}}},"GuidanceResponse":{"type":"object","description":"Public agent/caller guidance. Generated from `src/guidance.ts`. Section hashes on `guide` match `sections` keys.\n","required":["ok","guide","summary","ask_user","confidence","min_confidence","tiers","next_steps","billing","capabilities","sections"],"properties":{"ok":{"type":"boolean","enum":[true]},"guide":{"type":"string","example":"/v1/recipes/guidance"},"summary":{"type":"string"},"ask_user":{"type":"string"},"confidence":{"type":"object","additionalProperties":true,"description":"Levels plus `how` (the checks that ran) and `ask_user`. Confidence is corroboration found, not a quality hedge.\n"},"min_confidence":{"type":"object","additionalProperties":true},"tiers":{"type":"object","additionalProperties":true},"next_steps":{"type":"object","additionalProperties":true},"billing":{"type":"object","additionalProperties":true},"capabilities":{"type":"object","additionalProperties":true},"sections":{"type":"object","additionalProperties":{"type":"string"}}}},"RunTrace":{"type":"object","required":["decision","attempts","candidates","routing","writer"],"properties":{"decision":{"$ref":"#/components/schemas/Decision"},"attempts":{"type":"array","items":{"$ref":"#/components/schemas/Attempt"}},"candidates":{"type":"array","items":{"$ref":"#/components/schemas/Candidate"}},"routing":{"$ref":"#/components/schemas/RoutingResponse"},"writer":{"allOf":[{"$ref":"#/components/schemas/Writer"}],"nullable":true},"explain":{"oneOf":[{"$ref":"#/components/schemas/DomainExplain"},{"$ref":"#/components/schemas/EmploymentExplain"},{"$ref":"#/components/schemas/PersonB2bSocialExplain"},{"$ref":"#/components/schemas/EmailVerifyExplain"}]},"quota":{"type":"object","properties":{"zoominfo":{"type":"object","required":["source"],"properties":{"source":{"type":"string","enum":["response_headers"]},"remaining_second":{"type":"number"},"remaining_hour":{"type":"number"},"remaining_day":{"type":"number"},"limit_second":{"type":"number"},"limit_hour":{"type":"number"},"limit_day":{"type":"number"},"retry_after":{"type":"number"}}}}}}},"CompanyResolveInput":{"type":"object","required":["name","domain","website","email"],"properties":{"name":{"type":"string","nullable":true,"description":"Accepted on the request; name-only (no domain/email/website) is no_decision / no_domain and does not search"},"email":{"type":"string","nullable":true,"description":"Typical caller input; domain derived via normalizeDomain"},"domain":{"type":"string","nullable":true,"description":"Also accepts email-shaped strings (domain extracted after @)"},"website":{"type":"string","nullable":true},"min_confidence":{"$ref":"#/components/schemas/MinConfidence"}}},"PersonVerifyEmploymentInput":{"type":"object","required":["email","first_name","last_name","linkedin_url","expected_company"],"properties":{"email":{"type":"string","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true,"description":"Preferred LinkedIn for LeadMagic job-change; else person identifiers; else LM email-to-profile on auto/accuracy/coverage unless allow_paid_linkedin_lookup is false"},"expected_company":{"type":"object","required":["domain","name","identifiers"],"properties":{"domain":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/IdentifierInput"}}}},"min_confidence":{"$ref":"#/components/schemas/MinConfidence"}}},"CompanyDomainInput":{"type":"object","required":["name","domain","website","email","city","state","country","postal_code","phone","linkedin_url"],"properties":{"name":{"type":"string","nullable":true,"description":"Optional company name. Alias `company_name` is accepted. With a clean domain, the router checks that the name and host are the same company. Alone (or with a junk host) on `balance` or deeper, it may discover a unique domain — or abstain. `speed` does not discover.\n"},"company_name":{"type":"string","nullable":true,"description":"Alias of `name` (Clay / Workflows.io field name)"},"email":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"postal_code":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true,"description":"LinkedIn company page URL (`linkedin.com/company/...`); person `/in/` URLs are rejected"},"family_policy":{"type":"string","enum":["collapse_brand","separate_regional"],"description":"How alias / regional / family facts map to CRM shape. Omit = collapse_brand (alias and regional → additional_domains; HubSpot domain may lift to the brand primary). separate_regional keeps the regional as a child (`parent_domain` set). Included in the request hash and domain-cache key.\n"},"min_confidence":{"$ref":"#/components/schemas/MinConfidence"}}},"CompanyHierarchyInput":{"type":"object","required":["name","domain","website","email","country"],"properties":{"name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"min_confidence":{"$ref":"#/components/schemas/MinConfidence"}}},"CompanyB2bSocialInput":{"type":"object","required":["name","domain","website","email","linkedin_url"],"properties":{"name":{"type":"string","nullable":true,"description":"Unit/brand corroboration. Sufficient as the only subject (accuracy/coverage resolve name→domain). Speed/balance miss rather than guess when domain/website/email are absent.\n"},"domain":{"type":"string","nullable":true,"description":"Preferred subject (or website/email that hygiene-normalize to the same registrable)."},"website":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"country":{"type":"string","nullable":true,"description":"Optional ISO country for name→domain disambiguation."},"hint":{"type":"string","nullable":true,"description":"Optional list/industry context for name→domain (e.g. \"UK PR agency\"). Coverage uses it with `country` so short names like WPR resolve.\n"},"linkedin_url":{"type":"string","nullable":true,"description":"Optional company-page hint. Must already be `https://www.linkedin.com/company/{slug}` after `normalizeLinkedInCompanyUrl`. Person `/in/` is 400. Agree-check only — not authority to invent.\n"},"min_confidence":{"$ref":"#/components/schemas/MinConfidence"}}},"B2bSocialResult":{"type":"object","required":["company_name","company_domain","website","linkedin_url","identifiers","verification"],"properties":{"company_name":{"type":"string","nullable":true},"company_domain":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true,"description":"Accept form `https://www.linkedin.com/company/{slug}` (slug lowercased), or null. Homepage-confirmed, search-ranked, or vendor-accepted. Never `/in/`, school, showcase, or a parent page for a distinct unit.\n"},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}},"verification":{"$ref":"#/components/schemas/LinkedInPageVerification"},"unverified":{"type":"array","description":"Search candidates Serper could not confirm. Not hits.","items":{"$ref":"#/components/schemas/LinkedInUnverifiedPage"}},"evidence":{"$ref":"#/components/schemas/B2bSocialEvidence"}}},"LinkedInPageVerification":{"type":"object","required":["status","reason","url"],"properties":{"status":{"type":"string","enum":["confirmed","unverified","rejected"]},"reason":{"type":"string","description":"Stable code such as serp_title_snippet, serp_no_match, not_in_serp, person_profile, not_linkedin, serper_unavailable.\n"},"url":{"type":"string","nullable":true,"description":"Normalized company-page URL when structural check passed."},"query":{"type":"string","nullable":true,"description":"Unquoted Serper query used for confirmation (domain-keyed)."}}},"LinkedInUnverifiedPage":{"type":"object","required":["url","reason"],"properties":{"url":{"type":"string"},"reason":{"type":"string"}}},"B2bSocialEvidence":{"type":"object","required":["source"],"properties":{"source":{"type":"string","description":"homepage_link | homepage_rendered | linkedin_redirect | search_rank_N | vendor | name_domain\n"},"sources":{"type":"array","description":"Signals that agreed on result.linkedin_url (homepage_link, homepage_rendered, search, vendor, listed_website, cached_redirect). A local agent uses this to decide whether to self-verify.\n","items":{"type":"string"}},"homepage_link":{"type":"string","nullable":true},"serp_rank":{"type":"integer","nullable":true},"canonical_redirect":{"type":"object","nullable":true,"properties":{"from":{"type":"string"},"to":{"type":"string"}}},"homepage_fetch":{"type":"object","nullable":true,"description":"Per-step Worker homepage fetch diagnostics (status, URL, bytes, UA). `linkedin_check` is a cached LinkedIn outcome when we already have one (`status`, `http_status`, `reason`). The request path never GETs linkedin.com (`not_run` / `not_fetched`). `rendered` is set when accuracy/coverage ran Browser Rendering (`status`, `latency_ms`, `browser_ms`, `links_found`).\n"},"listed_website":{"type":"string","nullable":true},"listed_website_matches":{"type":"boolean"},"domain_resolution":{"type":"object","nullable":true,"properties":{"domain":{"type":"string"},"source":{"type":"string"}}},"provider_errors":{"type":"array","items":{"type":"object","required":["provider","code","message"],"properties":{"provider":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"}}}},"candidates":{"type":"array","description":"Every considered company page with its source (homepage / search / vendor / rejected). Present on hits so a local agent can self-verify.\n","items":{"type":"object","required":["url","source"],"properties":{"url":{"type":"string"},"source":{"type":"string"},"reason":{"type":"string"}}}},"hint":{"type":"string","nullable":true,"description":"Operator hint on a miss (never a top-level next_steps field)."}}},"B2bSocialOutcome":{"type":"string","enum":["hit","no_decision"]},"CompanyHierarchyMember":{"type":"object","required":["name","domain","zoominfo_company_id","wikidata_id"],"properties":{"name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true,"description":"Null on ZoomInfo-only answers (hierarchy payload has no websites)."},"zoominfo_company_id":{"type":"string","nullable":true},"wikidata_id":{"type":"string","nullable":true}}},"HierarchyResult":{"type":"object","required":["company_name","company_domain","zoominfo_company_id","wikidata_id","lei","immediate_parent_name","immediate_parent_domain","immediate_parent_wikidata_id","ultimate_parent_name","ultimate_parent_domain","ultimate_parent_wikidata_id","hierarchy_depth","subsidiaries","sources","source_modified_at","family_tree_size","truncated","website","linkedin_url","identifiers"],"properties":{"company_name":{"type":"string","nullable":true},"company_domain":{"type":"string","nullable":true},"zoominfo_company_id":{"type":"string","nullable":true},"wikidata_id":{"type":"string","nullable":true},"lei":{"type":"string","nullable":true},"immediate_parent_name":{"type":"string","nullable":true},"immediate_parent_domain":{"type":"string","nullable":true},"immediate_parent_wikidata_id":{"type":"string","nullable":true},"ultimate_parent_name":{"type":"string","nullable":true},"ultimate_parent_domain":{"type":"string","nullable":true},"ultimate_parent_wikidata_id":{"type":"string","nullable":true},"hierarchy_depth":{"type":"integer","nullable":true},"subsidiaries":{"type":"array","items":{"$ref":"#/components/schemas/CompanyHierarchyMember"}},"sources":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"source_modified_at":{"type":"string","nullable":true},"family_tree_size":{"type":"integer","nullable":true,"description":"Raw ZoomInfo node count before truncate (null on speed)."},"truncated":{"type":"boolean"},"website":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"CompanyResolveRequest":{"allOf":[{"$ref":"#/components/schemas/CompanyResolveInput"},{"type":"object","properties":{"dry_run":{"type":"boolean","default":false},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"routing":{"$ref":"#/components/schemas/RoutingRequest"},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"},"schema_version":{"type":"string","enum":["2"],"description":"Optional envelope pin. Same as `X-Router-Schema`."}}}]},"CompanyIconInput":{"type":"object","required":["name","domain","website","email"],"properties":{"name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true,"description":"Required subject (or website/email that hygiene-normalize to the same registrable)."},"website":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"min_confidence":{"$ref":"#/components/schemas/MinConfidence"}}},"CompanyDescriptionInput":{"type":"object","required":["name","domain","website","email","max_chars"],"properties":{"name":{"type":"string","nullable":true,"description":"Optional hint for accuracy compose. Not returned."},"domain":{"type":"string","nullable":true,"description":"Required subject (or website/email that hygiene-normalize to the same registrable)."},"website":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"max_chars":{"type":"integer","minimum":80,"maximum":500,"default":300,"description":"Truncate at a sentence boundary. Never cut mid-sentence."},"min_confidence":{"$ref":"#/components/schemas/MinConfidence"}}},"IconResult":{"type":"object","required":["company_name","company_domain","website","icon_url","identifiers"],"properties":{"company_name":{"type":"string","nullable":true},"company_domain":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"icon_url":{"type":"string","nullable":true,"description":"Hosted HTTPS 256×256 PNG on logos.kingminos.com (`/i/<sha256>.png`). Null on miss. Never a third-party URL."},"icon_source_url":{"type":"string","nullable":true,"description":"Original third-party URL the copy was fetched from. Audit only — do not stamp this."},"icon_source_width":{"type":"integer","nullable":true,"description":"Source raster width in pixels before the 256 canvas / 2× cap."},"icon_source_height":{"type":"integer","nullable":true,"description":"Source raster height in pixels before the 256 canvas / 2× cap."},"flags":{"type":"object","nullable":true,"additionalProperties":true,"description":"Hit-only. `low_res` when the source short side is below 64. Not a miss; confidence is `low` and the default floor still returns it.","properties":{"low_res":{"type":"boolean"},"pending_render":{"type":"boolean","description":"URL is final (`/i/d/<sha256(domain)>.png`); image still rendering. Placeholder (short TTL) until it lands; monogram if rendering fails."}}},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"DescriptionResult":{"type":"object","required":["description","source_kind","language_note","company_domain"],"properties":{"description":{"type":"string","nullable":true,"description":"English, JSON-safe description. Null on miss."},"source_kind":{"type":["string","null"],"enum":["quoted","composed",null]},"language_note":{"type":["string","null"],"enum":["filtered","translated","interpreted",null]},"company_domain":{"type":"string","nullable":true},"compose_echo":{"type":"boolean","description":"Compose was near-identical to a cheaper quoted hit. The compose may still stamp."},"homepage_meta_only":{"type":"boolean","description":"Homepage evidence was only a meta/og tag (no distinct JSON-LD / hero / about)."},"flags":{"type":"object","nullable":true,"additionalProperties":true,"description":"Hit-only quality flags. Not hard rejects. `hype` when Jev hype > 0.7 or leftover superlatives. `first_person` on any We/Our/I sentence. `tagline` covers slogans, all-caps headlines, blog headlines, and article summaries. `same_as_speed` marks a ranked fallback that matches the speed winner. `truncated` means the stamp was trimmed to the last whole word. `cta` marks marketing / imperative lines (Join N+, trusted-by, exclamation, Get started). `truncated` and `cta` force confidence `low`. `grammar` is informational and does not affect confidence. Confidence lives on `answer.confidence` (`high` | `medium` | `low`) and is the write signal.\n","properties":{"hype":{"type":"boolean"},"first_person":{"type":"boolean"},"tagline":{"type":"boolean"},"same_as_speed":{"type":"boolean"},"truncated":{"type":"boolean"},"cta":{"type":"boolean"},"grammar":{"type":"boolean"}}},"candidates":{"type":"array","description":"Ranked candidates on conflict or a below-floor withhold.","items":{"type":"object","required":["description","source","confidence"],"properties":{"description":{"type":"string"},"source":{"type":"string"},"confidence":{"$ref":"#/components/schemas/Confidence"}}}}}},"IconOutcome":{"type":"string","enum":["hit","no_decision"]},"DescriptionOutcome":{"type":"string","enum":["hit","no_decision"]},"CompanyIconRequest":{"allOf":[{"$ref":"#/components/schemas/CompanyIconInput"},{"type":"object","properties":{"dry_run":{"type":"boolean","default":false},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}}]},"CompanyDescriptionRequest":{"allOf":[{"$ref":"#/components/schemas/CompanyDescriptionInput"},{"type":"object","properties":{"dry_run":{"type":"boolean","default":false},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"routing":{"$ref":"#/components/schemas/CompanyDomainRoutingRequest"},"path":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","fast","name_only"],"description":"Flattened alias of `routing.path`. Omit = `balance`."},"preset":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","name_only","zi_stamp","fast"]},"skip_cache":{"type":"boolean","description":"When true, skip the 30-day `company.description` result-cache read and re-run the ladder. Still writes the fresh answer. Same effect as `X-Router-Cache: bypass` or `Cache-Control: no-cache|no-store`. Non-boolean is `400 invalid_cache`.\n"},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}}]},"CompanyB2bSocialRequest":{"allOf":[{"$ref":"#/components/schemas/CompanyB2bSocialInput"},{"type":"object","properties":{"dry_run":{"type":"boolean","default":false},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"routing":{"$ref":"#/components/schemas/CompanyDomainRoutingRequest"},"path":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","fast","name_only"],"description":"Flattened alias of `routing.path`. Omit = `balance`. Nested `routing.path` wins when both are sent.\n"},"preset":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","name_only","zi_stamp","fast"],"description":"Flattened alias of `routing.preset`. Nested wins when both are sent."},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}}]},"CompanyHierarchyRequest":{"allOf":[{"$ref":"#/components/schemas/CompanyHierarchyInput"},{"type":"object","properties":{"dry_run":{"type":"boolean","default":false},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"routing":{"$ref":"#/components/schemas/CompanyDomainRoutingRequest"},"path":{"type":"string","enum":["speed","balance","accuracy","value","auto","fast"],"description":"Flattened alias of `routing.path`. Nested wins when both are sent."},"preset":{"type":"string","enum":["speed","balance","accuracy","value","auto","name_only","zi_stamp","fast"],"description":"Flattened alias of `routing.preset`. Nested wins when both are sent."},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}}]},"CompanyDomainRequest":{"allOf":[{"$ref":"#/components/schemas/CompanyDomainInput"},{"type":"object","properties":{"dry_run":{"type":"boolean","default":false},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"routing":{"$ref":"#/components/schemas/CompanyDomainRoutingRequest"},"path":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","fast","name_only"],"description":"Flattened alias of `routing.path` (Workflows / Clay / Cap). Same expand and `400 invalid_routing` rules. Nested `routing.path` wins when both are sent. `coverage` is a live product path (accuracy + BuiltWith).\n"},"preset":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","name_only","zi_stamp","fast"],"description":"Flattened alias of `routing.preset`. Nested wins when both are sent."},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"},"skip_cache":{"type":"boolean","description":"When true, skip the short-TTL `company.domain` result-cache read and re-run the ladder. Still writes the fresh answer. Does not skip `Idempotency-Key` replay — omit or rotate that header on Janus first-100 re-smokes. Same effect as `X-Router-Cache: bypass` or `Cache-Control: no-cache|no-store`. Non-boolean is `400 invalid_cache`.\n"},"schema_version":{"type":"string","enum":["2"],"description":"Optional envelope pin. Same as `X-Router-Schema`. Omit = current tip. Must agree with the header / query when those are set.\n"}}}]},"PersonVerifyEmploymentRequest":{"type":"object","properties":{"min_confidence":{"$ref":"#/components/schemas/MinConfidence"},"email":{"type":"string","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"person":{"type":"object","properties":{"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"linkedin_url":{"type":"string"}}},"expected_company":{"type":"object","properties":{"domain":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/IdentifierInput"}}}},"dry_run":{"type":"boolean","default":false},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"routing":{"$ref":"#/components/schemas/RoutingRequest"},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}},"CompanyResolveResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["company.resolve"]},"input":{"$ref":"#/components/schemas/CompanyResolveInput"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/CompanyOutcome"}}}]},"result":{"$ref":"#/components/schemas/CompanyResult"}}}]},"CompanyDomainResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["company.domain"]},"input":{"$ref":"#/components/schemas/CompanyDomainInput"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/DomainOutcome"}}}]},"result":{"$ref":"#/components/schemas/DomainResult"}}}]},"CompanyB2bSocialResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["company.b2b_social"]},"input":{"$ref":"#/components/schemas/CompanyB2bSocialInput"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/B2bSocialOutcome"}}}]},"result":{"$ref":"#/components/schemas/B2bSocialResult"}}}]},"CompanyXAccountInput":{"type":"object","required":["name","domain","website","email","x_url"],"properties":{"name":{"type":"string","nullable":true,"description":"Unit/brand corroboration. Sufficient as the only subject (accuracy/coverage resolve name→domain). Speed/balance miss rather than guess when domain/website/email are absent.\n"},"domain":{"type":"string","nullable":true,"description":"Preferred subject (or website/email that hygiene-normalize to the same registrable)."},"website":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"country":{"type":"string","nullable":true},"hint":{"type":"string","nullable":true},"x_url":{"type":"string","nullable":true,"description":"Optional account hint. Must already be `https://x.com/{handle}` after `normalizeXAccountUrl`. Agree-check only.\n"},"min_confidence":{"type":"string","nullable":true,"enum":["high","medium","low"],"description":"Overrides the path confidence floor (speed/coverage `low`, balance `medium`, accuracy `high`). Omit = path default.\n"}}},"CompanyXAccountRequest":{"allOf":[{"$ref":"#/components/schemas/CompanyXAccountInput"},{"type":"object","properties":{"dry_run":{"type":"boolean","default":false},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"routing":{"$ref":"#/components/schemas/CompanyDomainRoutingRequest"},"path":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","fast","name_only"],"description":"Flattened alias of `routing.path`. Omit = `balance`."},"preset":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","name_only","zi_stamp","fast"]},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}}]},"XAccountResult":{"type":"object","required":["company_name","company_domain","website","x_url","x_handle","identifiers","verification"],"properties":{"company_name":{"type":"string","nullable":true},"company_domain":{"type":"string","nullable":true},"website":{"type":"string","nullable":true},"x_url":{"type":"string","nullable":true,"description":"Accept form `https://x.com/{handle}` (lowercase), or null. Never twitter.com."},"x_handle":{"type":"string","nullable":true,"description":"Lowercase handle without `@`, or null."},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}},"verification":{"type":"object","required":["status","reason","url"],"properties":{"status":{"type":"string","enum":["confirmed","unverified","rejected"]},"reason":{"type":"string"},"url":{"type":"string","nullable":true}}},"evidence":{"type":"object","additionalProperties":true}}},"XAccountOutcome":{"type":"string","enum":["hit","no_decision"]},"CompanyXAccountResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["company.x_account"]},"input":{"$ref":"#/components/schemas/CompanyXAccountInput"},"answer":{"type":"object","description":"Same control block as Answer, without `safe_to_write`. Confidence is the only write signal.\n","required":["outcome","reason_code","es_decision","confidence","summary"],"properties":{"outcome":{"$ref":"#/components/schemas/XAccountOutcome"},"reason_code":{"type":"string"},"es_decision":{"$ref":"#/components/schemas/EsDecision"},"confidence":{"allOf":[{"$ref":"#/components/schemas/Confidence"}],"nullable":true},"summary":{"type":"string"}}},"result":{"$ref":"#/components/schemas/XAccountResult"}}}]},"CompanyIconResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["company.icon"]},"input":{"$ref":"#/components/schemas/CompanyIconInput"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/IconOutcome"}}}]},"result":{"$ref":"#/components/schemas/IconResult"}}}]},"CompanyDescriptionResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["company.description"]},"input":{"$ref":"#/components/schemas/CompanyDescriptionInput"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/DescriptionOutcome"}}}]},"result":{"$ref":"#/components/schemas/DescriptionResult"}}}]},"CompanyHierarchyResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["company.hierarchy"]},"input":{"$ref":"#/components/schemas/CompanyHierarchyInput"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/HierarchyOutcome"}}}]},"result":{"$ref":"#/components/schemas/HierarchyResult"}}}]},"PersonLanguageOutcome":{"type":"string","enum":["hit","no_decision"]},"PersonLanguageInput":{"type":"object","required":["email","first_name","last_name","linkedin_url"],"properties":{"email":{"type":"string","nullable":true},"first_name":{"type":"string","nullable":true,"description":"Match-only — rejects a profile that belongs to someone else. Never a language signal."},"last_name":{"type":"string","nullable":true,"description":"Match-only — rejects a profile that belongs to someone else. Never a language signal."},"linkedin_url":{"type":"string","nullable":true}}},"PersonLanguageRequest":{"type":"object","description":"One of `email` or `linkedin_url` is required (top level or under `person`).","properties":{"min_confidence":{"$ref":"#/components/schemas/MinConfidence"},"email":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"person":{"type":"object","properties":{"email":{"type":"string"},"linkedin_url":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"}}},"dry_run":{"type":"boolean","default":false},"skip_cache":{"type":"boolean","description":"Skip the 30-day cache read (same as `X-Router-Cache` bypass); the fresh answer is still written."},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"routing":{"type":"object","description":"Only `path` (or `preset`) is read.","properties":{"path":{"type":"string","enum":["speed","balance","accuracy"],"default":"balance"}}},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}},"PersonLanguageLevel":{"type":"string","enum":["high","medium","low"]},"PersonLanguageEvidence":{"type":"object","required":["source","signal"],"properties":{"source":{"type":"string","description":"ProviderName that supplied the signal, or `derived` (work-email country domain)."},"signal":{"type":"string","enum":["listed_languages","profile_locale","profile_text","location_country","company_language","email_cctld","text_judge"]}}},"PersonLanguageCandidate":{"type":"object","required":["code","name","confidence","level","evidence"],"properties":{"code":{"type":"string","description":"ISO 639-1 (lowercase)"},"name":{"type":"string","description":"English display name (e.g. German); the code when unknown"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Heuristic score","not a calibrated probability":null},"level":{"$ref":"#/components/schemas/PersonLanguageLevel"},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/PersonLanguageEvidence"}}}},"PersonLanguageResult":{"type":"object","required":["person","detected","english","english_confidence","recommended_language","recommended_language_name","recommended_confidence","recommended_level","local_language","local_language_name","languages","flags","identifiers"],"properties":{"person":{"allOf":[{"$ref":"#/components/schemas/PersonEntity"}],"nullable":true},"detected":{"type":"boolean","description":"False when no language evidence was found at all."},"english":{"type":"string","enum":["yes","likely","unconfirmed","unknown"],"description":"Can we engage in English? `yes` = English evidence at medium or high; `likely` = only low English evidence; `unconfirmed` = other languages found, no English (never \"no\"); `unknown` = nothing detected.\n"},"english_confidence":{"type":"number","nullable":true},"recommended_language":{"type":"string","nullable":true,"description":"Language to engage in (ISO 639-1). English whenever `english=yes`. Medium or high only, except conflict / unconfirmed_identity keep the best guess at low.\n"},"recommended_language_name":{"type":"string","nullable":true},"recommended_confidence":{"type":"number","nullable":true},"recommended_level":{"type":"string","nullable":true,"enum":["high","medium","low",null]},"local_language":{"type":"string","nullable":true,"description":"Strongest non-English language (localized sequences / ads). Medium or high only."},"local_language_name":{"type":"string","nullable":true},"languages":{"type":"array","description":"Every candidate, ranked, with evidence. Low guesses appear only here.","items":{"$ref":"#/components/schemas/PersonLanguageCandidate"}},"flags":{"type":"array","items":{"type":"string","enum":["weak_signals","multilingual_country","conflicting_signals","unconfirmed_identity"]}},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"PersonLanguageResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["person.language"]},"input":{"$ref":"#/components/schemas/PersonLanguageInput"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/PersonLanguageOutcome"}}}]},"result":{"$ref":"#/components/schemas/PersonLanguageResult"}}}]},"PersonPhoneOutcome":{"type":"string","enum":["hit","no_decision"]},"PersonPhoneInput":{"type":"object","required":["email","first_name","last_name","linkedin_url","phone_types","max_results"],"properties":{"email":{"type":"string","nullable":true},"first_name":{"type":"string","nullable":true,"description":"Match-only — rejects a profile that belongs to someone else. Never infers."},"last_name":{"type":"string","nullable":true,"description":"Match-only — rejects a profile that belongs to someone else. Never infers."},"linkedin_url":{"type":"string","nullable":true},"phone_types":{"type":"array","nullable":true,"description":"Request filter (Harris 2026-10-12). Omit or [] = any type.","items":{"type":"string","enum":["mobile","landline","other","unknown"]}},"max_results":{"type":"integer","nullable":true,"minimum":1,"maximum":10,"description":"Ranked-list cap, default 3. Never limits the ladder."},"format":{"type":"string","nullable":true,"enum":["e164","international","national","digits","digits_national",null],"description":"Requested output format; null = e164."}}},"PersonPhoneRequest":{"type":"object","description":"One of `email` or `linkedin_url` is required (top level or under `person`).","properties":{"min_confidence":{"$ref":"#/components/schemas/MinConfidence"},"email":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true,"description":"Company domain (alias `company_domain`); with first+last name unlocks name+domain finders. Defaults to the email domain (not free-mail)."},"phone_types":{"type":"array","description":"Omit or [] = any type. Shapes `best_phone` / `phones`; a vendor that can return none of these types is not called (0 credits).\n","items":{"type":"string","enum":["mobile","landline","other","unknown"]}},"max_results":{"type":"integer","minimum":1,"maximum":10,"default":3},"format":{"type":"string","enum":["e164","international","national","digits","digits_national"],"default":"e164","description":"Which format fills `number` on `best_phone` and every `phones[]` entry. Every entry also carries `e164` and all formats in `formats`, whatever you pick. Presentation only: it is not part of the cache key, so switching format replays the cached answer at 0 credits.\n"},"routing":{"type":"object","description":"`path` picks the router mode (speed = cheapest/fastest; balance = best single number, default; accuracy = agreement required; coverage = several numbers). `only` / `ignore` filter the vendor pool (an unknown provider is `400 unknown_provider`). `order` is ignored — the planner orders vendors per request.\n","properties":{"path":{"type":"string","enum":["speed","balance","accuracy","coverage"],"default":"balance"},"only":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"ignore":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}}}},"person":{"type":"object","properties":{"email":{"type":"string"},"linkedin_url":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"}}},"dry_run":{"type":"boolean","default":false},"skip_cache":{"type":"boolean","description":"Skip the 30-day cache read (same as `X-Router-Cache` bypass); the fresh answer is still written."},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}},"PhoneFormats":{"type":"object","required":["e164","international","national","digits","digits_national"],"properties":{"e164":{"type":"string","example":"+14155552672"},"international":{"type":"string","example":"+1 415 555 2672"},"national":{"type":"string","example":"(415) 555-2672"},"digits":{"type":"string","example":"14155552672"},"digits_national":{"type":"string","example":"4155552672"}}},"PhoneEntry":{"type":"object","required":["number","type","source"],"properties":{"number":{"type":"string","description":"The number in the requested `format` (default E.164 without spaces or punctuation, e.g. +13133383662 — the original contract).\n"},"type":{"type":"string","enum":["mobile","landline","other","unknown"],"description":"Answer type. The numbering plan wins when it is definitive (libphonenumber `mobile` → mobile, `fixed_line` → landline, VoIP / toll-free / premium → other); otherwise the vendor label (Wiza `phones[]` entries are mobile — `type: \"mobile\"` and `type: \"other\"` alike). `phone_types` filters on this field.\n"},"source":{"type":"string","description":"ProviderName that supplied the number first."},"e164":{"type":"string","description":"Always E.164, whatever `format` was requested."},"formats":{"$ref":"#/components/schemas/PhoneFormats"},"country_code":{"type":"string","nullable":true,"description":"ISO 3166-1 alpha-2 of the number's region; null for non-geographic numbers."},"calling_code":{"type":"string","description":"Country calling code without `+` (e.g. \"1\", \"44\")."},"line_type":{"type":"string","enum":["mobile","fixed_line","fixed_line_or_mobile","voip","toll_free","premium_rate","shared_cost","personal_number","pager","uan","voicemail","unknown"],"description":"libphonenumber line type from the numbering plan. US/Canada numbers are `fixed_line_or_mobile` — the plan cannot tell; this is not a carrier lookup.\n"},"sources":{"type":"array","items":{"type":"string"},"description":"Every provider that returned this number (cross-source agreement)."},"confidence":{"type":"string","enum":["high","medium","low"],"description":"Per-number trust. Vendor primary = high, other vendor numbers = medium, independent multi-source agreement = high; a country or line-type contradiction lowers it one level.\n"},"flags":{"type":"array","items":{"type":"string","enum":["toll_free","premium_rate","shared_cost","pager","voip","line_type_mismatch","country_mismatch","email_country_mismatch","company_switchboard","secondary_number","multi_source"]},"description":"Verification flags. `country_mismatch`: the number's calling code differs from the person's (or company's) known location. `email_country_mismatch`: only the email ccTLD disagrees (informational). `line_type_mismatch`: the vendor said mobile, the numbering plan says otherwise. Toll-free / premium / shared-cost / pager numbers and the company switchboard are never returned as a person's number (dropped; see `numbers_dropped`).\n"},"consensus":{"type":"string","enum":["unanimous","majority","single","conflict"],"description":"Vendor agreement on this number among the vendors that answered."},"do_not_call":{"type":"boolean","nullable":true,"description":"Vendor-native do-not-call flag (ZoomInfo); null when no vendor says."}}},"PersonPhoneResult":{"type":"object","required":["person","best_phone","phones","flags","identifiers"],"properties":{"person":{"allOf":[{"$ref":"#/components/schemas/PersonEntity"}],"nullable":true,"description":"The matched profile on a hit; always null on a miss (no free reverse lookup)."},"best_phone":{"allOf":[{"$ref":"#/components/schemas/PhoneEntry"}],"nullable":true,"description":"Best number honoring the request filter; null on a miss."},"phones":{"type":"array","description":"Matching numbers, deduped by E.164, ranked safe-to-write first, then mobile, then confidence, then vendor order; capped at `max_results`. Invalid (libphonenumber), junk (repeated / sequential digits, NANP 555-01xx), not-direct-dial and switchboard numbers are dropped.\n","items":{"$ref":"#/components/schemas/PhoneEntry"}},"flags":{"type":"array","nullable":true,"description":"`conflicting_numbers`: two or more providers answered and the best number is not corroborated by all of them. `numbers_dropped`: at least one returned number was invalid, junk, or not a direct dial.\n","items":{"type":"string","enum":["conflicting_numbers","numbers_dropped"]}},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"PersonPhoneResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["person.phone"]},"input":{"$ref":"#/components/schemas/PersonPhoneInput"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/PersonPhoneOutcome"}}}]},"result":{"$ref":"#/components/schemas/PersonPhoneResult"}}}]},"PersonTitleOutcome":{"type":"string","enum":["hit","no_decision"]},"PersonTitleField":{"type":"string","enum":["title","seniority","function"]},"TitleSeniority":{"type":"string","enum":["founder","c_suite","partner","vp","head","director","manager","senior","ic","entry","intern"]},"TitleFunction":{"type":"string","enum":["sales","marketing","revenue_operations","customer_success","engineering","product","design","data","it","security","finance","people","legal","operations","executive","other"]},"PersonTitleInput":{"type":"object","required":["email","first_name","last_name","linkedin_url","company_domain","fields"],"properties":{"email":{"type":"string","nullable":true},"first_name":{"type":"string","nullable":true,"description":"Match-only — rejects a profile that belongs to someone else. Never infers."},"last_name":{"type":"string","nullable":true,"description":"Match-only — rejects a profile that belongs to someone else. Never infers."},"linkedin_url":{"type":"string","nullable":true},"company_domain":{"type":"string","nullable":true,"description":"Match-only current employer. Never steers routing."},"fields":{"type":"array","description":"Canonical requested fields (deduped, in title / seniority / function order).","items":{"$ref":"#/components/schemas/PersonTitleField"}}}},"PersonTitleRequest":{"type":"object","description":"One of `email` or `linkedin_url` is required (top level or under `person`).","properties":{"min_confidence":{"$ref":"#/components/schemas/MinConfidence"},"email":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"company_domain":{"type":"string","nullable":true,"description":"Match-only current employer. A vendor title at a different employer is never a hit (`employer_mismatch`).\n"},"fields":{"type":"array","description":"Omit or [] = all three. The ladder walks until every requested field is answered; a vendor that can feed none of them is not called (0 credits).\n","items":{"$ref":"#/components/schemas/PersonTitleField"}},"routing":{"type":"object","description":"`path` picks the ladder (speed | balance | accuracy | coverage; omit = balance). `only` / `ignore` isolate vendors (an unknown provider is `400 unknown_provider`). `max_provider_credits` below the path price is `max_credits_reached` (noop, 0 credits).\n","properties":{"path":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","fast","name_only"]},"only":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"ignore":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"max_provider_credits":{"type":"number","minimum":0}}},"person":{"type":"object","properties":{"email":{"type":"string"},"linkedin_url":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"company_domain":{"type":"string"}}},"dry_run":{"type":"boolean","default":false},"skip_cache":{"type":"boolean","description":"Skip the 30-day cache read (same as `X-Router-Cache` bypass); the fresh answer is still written."},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}},"TitleFieldSource":{"type":"object","required":["method","provider"],"properties":{"method":{"type":"string","enum":["vendor","derived","jev"],"description":"vendor = the vendor's title; derived = keyword tables on that title; jev = the judge."},"provider":{"type":"string","nullable":true,"description":"ProviderName whose title the field came from (null for a judge classification)."}}},"TitleSourceEntry":{"type":"object","required":["provider","title","agrees"],"properties":{"provider":{"type":"string"},"title":{"type":"string"},"agrees":{"type":"boolean","description":"Same role as `result.title` (wording-insensitive)."}}},"PersonTitleResult":{"type":"object","required":["person","company","fields","title","seniority","function","field_sources","answered_fields","sources","flags","identifiers"],"properties":{"person":{"allOf":[{"$ref":"#/components/schemas/PersonEntity"}],"nullable":true,"description":"The matched profile on a hit; always null otherwise (no free reverse lookup)."},"company":{"type":"object","nullable":true,"description":"Current employer the title belongs to; hit only.","properties":{"name":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true}}},"fields":{"type":"array","items":{"$ref":"#/components/schemas/PersonTitleField"}},"title":{"type":"string","nullable":true,"description":"Cleaned current title (headline tail, emoji and ` at Company` stripped); null unless requested and hit."},"seniority":{"allOf":[{"$ref":"#/components/schemas/TitleSeniority"}],"nullable":true},"function":{"allOf":[{"$ref":"#/components/schemas/TitleFunction"}],"nullable":true},"field_sources":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TitleFieldSource"}},"answered_fields":{"type":"array","description":"Requested fields that were answered. On `fields_incomplete` this names what was found while the values stay withheld (bills 0).\n","items":{"$ref":"#/components/schemas/PersonTitleField"}},"sources":{"type":"array","description":"accuracy / coverage with `title` requested — every usable vendor title and whether it agrees.","items":{"$ref":"#/components/schemas/TitleSourceEntry"}},"flags":{"type":"array","nullable":true,"items":{"type":"string","enum":["headline_derived","conflicting_titles","stale_title","employer_mismatch","unconfirmed_identity","single_source","unclassified"]}},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"PersonTitleResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["person.title"]},"input":{"$ref":"#/components/schemas/PersonTitleInput"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/PersonTitleOutcome"}}}]},"result":{"$ref":"#/components/schemas/PersonTitleResult"}}}]},"PersonNameRequest":{"type":"object","description":"One of `email` or `linkedin_url` is required (top level or under `person`).","properties":{"min_confidence":{"$ref":"#/components/schemas/MinConfidence"},"email":{"type":"string","nullable":true},"linkedin_url":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true,"description":"Accepted, never looked up — phone-only marks `unsupported_input` (0 credits)."},"x_handle":{"type":"string","nullable":true,"description":"Accepted, never looked up — X-only marks `unsupported_input` (0 credits)."},"routing":{"type":"object","description":"`path` picks the ladder (speed | balance | accuracy | coverage; omit = balance). `only` / `ignore` isolate vendors (an unknown provider is `400 unknown_provider`). `zoominfo` is honored — contacts/search by email on a workspace BYOK key; the house key is skipped. `websearch` is honored as an opt-in public web-search name verify (off every default path except `accuracy`, which also runs it; blocked under `compliance: soc2`). `max_provider_credits` below the path price is `max_credits_reached` (noop, 0 credits).\n","properties":{"path":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","fast","name_only"]},"only":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"ignore":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"max_provider_credits":{"type":"number","minimum":0}}},"person":{"type":"object","properties":{"email":{"type":"string"},"linkedin_url":{"type":"string"}}},"dry_run":{"type":"boolean","default":false},"skip_cache":{"type":"boolean","description":"Skip the 30-day cache read (same as `X-Router-Cache` bypass); the fresh answer is still written."},"last_name_fallback":{"type":["string","null"],"minLength":1,"maxLength":80,"description":"Optional. Omit (default) and last_name stays null when none is found. When set, this exact string fills result.last_name if no last name is found, the response is flagged last_name_placeholder, and it is never billed or counted as a hit. Decided per call; never cached. Recommended for direct Salesforce import: \"Unknown\".\n","example":"Unknown"},"output":{"type":"object","additionalProperties":false,"properties":{"salesforce_safe":{"type":"boolean","default":false,"description":"Adds result.salesforce with Salesforce-ready first/last (FirstName <= 40, LastName <= 80 UTF-16 units, control / zero-width / emoji removed). Canonical name fields are unchanged.\n"}}},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}},"PersonNameResult":{"type":"object","required":["first_name","last_name","full_name","name_source","matched_on","flags"],"properties":{"first_name":{"type":"string","nullable":true,"description":"Proper-cased (`properCaseName`); salutations, honorifics, suffixes, pronouns, emoji and URLs stripped."},"last_name":{"type":"string","nullable":true},"full_name":{"type":"string","nullable":true},"first_name_confidence":{"allOf":[{"$ref":"#/components/schemas/Confidence"}],"nullable":true,"description":"Optional write signal for first_name when first and last are scored separately. Omitted when first_name is not returned. Overall confidence is the lower of the two component scores. High requires independent confirmation.\n"},"last_name_confidence":{"allOf":[{"$ref":"#/components/schemas/Confidence"}],"nullable":true,"description":"Optional write signal for last_name when first and last are scored separately. Omitted when last_name is not returned. Overall confidence is the lower of the two component scores. High requires independent confirmation.\n"},"name_source":{"type":"string","nullable":true,"enum":["vendor","email_local_parse"],"description":"vendor = a matched profile; email_local_parse = low-confidence local-part guess (`parsed_only`)."},"matched_on":{"type":"array","description":"Which request identifier(s) the name came from.","items":{"type":"string","enum":["email","linkedin_url"]}},"email_type":{"type":"string","nullable":true,"enum":["role","freemail","work"]},"person":{"allOf":[{"$ref":"#/components/schemas/PersonEntity"}],"nullable":true,"description":"The matched profile on a hit; null otherwise (no free reverse lookup)."},"flags":{"type":"array","nullable":true,"items":{"type":"string","enum":["email_local_parse","first_name_only","freemail","role_address","conflicting_names","unconfirmed_identity","single_source","email_mismatch","truncated_name","surname_form_variant","surname_changed","last_name_placeholder","truncated_for_salesforce"]}},"salesforce":{"type":"object","nullable":true,"description":"Present only when output.salesforce_safe is true.","properties":{"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"ready":{"type":"boolean","description":"True iff last_name is non-empty after the Salesforce transform (fallback counts)."},"changes":{"type":"array","items":{"type":"string","enum":["control_chars_removed","truncated_for_salesforce","last_name_placeholder"]}},"limits":{"type":"object","properties":{"first_name":{"type":"integer","enum":[40]},"last_name":{"type":"integer","enum":[80]}}}}},"would_apply_last_name_fallback":{"type":["boolean","string"],"enum":[true,"unknown"],"description":"Dry-run echo. true when last_name_fallback is set; unknown when omitted. Live runs omit this."},"evidence":{"type":"object","nullable":true,"description":"Present when a writeable name was withheld below the confidence floor.","properties":{"hint":{"type":"string","nullable":true},"candidates":{"type":"array","items":{"type":"object","required":["first_name","last_name","full_name","confidence"],"properties":{"provider":{"allOf":[{"$ref":"#/components/schemas/ProviderName"}],"nullable":true},"name_source":{"type":"string","nullable":true,"enum":["vendor","email_local_parse"]},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"full_name":{"type":"string","nullable":true},"confidence":{"$ref":"#/components/schemas/Confidence"}}}}}}}},"PersonNameResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["person.name"]},"input":{"$ref":"#/components/schemas/PersonNameRequest"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"type":"string","enum":["resolved","parsed_only","role_address","not_found","conflict","unsupported_input"]},"first_name_confidence":{"allOf":[{"$ref":"#/components/schemas/Confidence"}],"nullable":true,"description":"Optional. Present when first and last are scored separately. Overall answer.confidence is the lower of the two. High requires independent confirmation.\n"},"last_name_confidence":{"allOf":[{"$ref":"#/components/schemas/Confidence"}],"nullable":true,"description":"Optional. Present when first and last are scored separately. Overall answer.confidence is the lower of the two. High requires independent confirmation.\n"}}}]},"result":{"$ref":"#/components/schemas/PersonNameResult"}}}]},"PersonB2bSocialRequest":{"type":"object","description":"`email` is required (top level or under `person`). `linkedin_url`, `first_name`, `last_name`, and `company_domain` are match-only identity gates — never a discovery key.\n","required":["email"],"properties":{"email":{"type":"string"},"linkedin_url":{"type":"string","nullable":true,"description":"Match-only. A resolved profile with another slug is rejected (`person_mismatch`). Company pages are 400."},"first_name":{"type":"string","nullable":true,"description":"Match-only; never infers or scores anything."},"last_name":{"type":"string","nullable":true,"description":"Match-only; never infers or scores anything. Both last names present and sharing no token (accent fold, umlaut transliteration, ignore a parenthetical maiden name) is `person_mismatch`.\n"},"company_domain":{"type":"string","nullable":true,"description":"Match-only employer hint. A vendor profile at another registrable domain flags `employer_mismatch` and caps high at medium — not a hard miss.\n"},"min_confidence":{"$ref":"#/components/schemas/MinConfidence"},"routing":{"type":"object","description":"`path` picks the ladder (speed | balance | accuracy | coverage; omit = balance). `only` / `ignore` isolate vendors (an unknown provider is `400 unknown_provider`). `max_provider_credits` below the path price is `max_credits_reached` (noop, 0 credits).\n","properties":{"path":{"type":"string","enum":["speed","balance","accuracy","coverage","value","auto","fast","name_only"]},"only":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"ignore":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"max_provider_credits":{"type":"number","minimum":0}}},"person":{"type":"object","properties":{"email":{"type":"string"},"linkedin_url":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"company_domain":{"type":"string"}}},"dry_run":{"type":"boolean","default":false,"description":"Offline and instant — no vendor hops, not cached."},"skip_cache":{"type":"boolean","description":"Skip the 30-day cache read (same as `X-Router-Cache` bypass); the fresh answer is still written."},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}},"PersonB2bSocialExplain":{"type":"object","description":"Present on `trace.explain` only when `X-Router-Explain` / `?explain=` is `full`. Default responses omit it. Rejected rows are compared normalised name tokens — no URL, slug, or email.\n","required":["notes","path"],"properties":{"notes":{"type":"array","items":{"type":"string"}},"path":{"type":"string","enum":["speed","balance","accuracy","coverage"]},"rejected":{"type":"array","items":{"type":"object","required":["gate","request_first","request_last","candidate_first","candidate_last"],"properties":{"gate":{"type":"string","enum":["first_name","surname","slug_lacks_surname","slug","other"]},"request_first":{"type":"array","items":{"type":"string"}},"request_last":{"type":"array","items":{"type":"string"}},"candidate_first":{"type":"array","items":{"type":"string"}},"candidate_last":{"type":"array","items":{"type":"string"}}}}}}},"PersonB2bSocialResult":{"type":"object","required":["status","linkedin_url","linkedin_slug","source","candidates","confidence","flags","person","identifiers"],"properties":{"status":{"type":"string","enum":["found","not_found"],"description":"`not_found` carries every non-answer; the answer's `reason_code` says why (zero_hits, person_mismatch, below_min_confidence, …)."},"linkedin_url":{"type":"string","nullable":true,"description":"Canonical `https://www.linkedin.com/in/<slug>` of the winning profile; hit only."},"linkedin_slug":{"type":"string","nullable":true},"source":{"type":"string","nullable":true,"enum":["vendor_email"],"description":"How the winning URL was keyed at the vendor."},"candidates":{"type":"array","description":"Hit — profile URLs, winner first (`linkedin_url` + `agrees`). Non-delivery — `provider` + hop `status` only; never a URL, slug, or person name.\n","items":{"type":"object","required":["provider"],"properties":{"provider":{"$ref":"#/components/schemas/ProviderName"},"status":{"type":"string"},"linkedin_url":{"type":"string"},"agrees":{"type":"boolean"}}}},"confidence":{"type":"string","nullable":true,"enum":["high","medium","low"],"description":"High requires independent confirmation (two agreeing sources on identity-equivalent slugs, or a slug that contains both requested name tokens). One source = medium; a disagreement = low. Kept on a below-floor withhold.\n"},"flags":{"type":"array","nullable":true,"items":{"type":"string","enum":["single_source","conflicting_profiles","person_mismatch","unconfirmed_identity","employer_mismatch"]}},"person":{"allOf":[{"$ref":"#/components/schemas/PersonEntity"}],"nullable":true,"description":"The matched profile on a hit; null otherwise (no free reverse lookup)."},"identifiers":{"type":"array","items":{"$ref":"#/components/schemas/Identifier"}}}},"PersonB2bSocialResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["person.b2b_social"]},"input":{"$ref":"#/components/schemas/PersonB2bSocialRequest"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"type":"string","enum":["found","not_found"]}}}]},"result":{"$ref":"#/components/schemas/PersonB2bSocialResult"}}}]},"EmailVerifyRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","maxLength":320,"description":"The mailbox to check. Canonicalized (lower-cased, look-alike host spellings folded) before the suppression check and the vendor checks. A malformed address is answered (`invalid`, `source: local`, 0 credits), not rejected.\n"},"min_confidence":{"$ref":"#/components/schemas/MinConfidence"},"routing":{"type":"object","description":"`path` picks the ladder (speed | balance | accuracy | coverage; omit = balance). `only` / `ignore` isolate vendors (an unknown provider is `400 unknown_provider`). `max_provider_credits` below the path price is `max_credits_reached` (noop, 0 credits).\n","properties":{"path":{"type":"string","enum":["speed","balance","accuracy","coverage"]},"only":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"ignore":{"type":"array","items":{"$ref":"#/components/schemas/ProviderName"}},"max_provider_credits":{"type":"number","minimum":0}}},"dry_run":{"type":"boolean","default":false,"description":"Offline and instant — no DNS or vendor check."},"external_ref":{"type":"string","description":"Client correlation key (round-trip on the run response)."},"compliance":{"$ref":"#/components/schemas/ComplianceRequest"}}},"EmailVerifyExplain":{"type":"object","description":"Present on `trace.explain` only when `X-Router-Explain` / `?explain=` is `full`. Default responses omit it.\n","required":["notes","path"],"properties":{"notes":{"type":"array","items":{"type":"string"}},"path":{"type":"string","enum":["speed","balance","accuracy","coverage"]},"local_check":{"type":"string","enum":["syntax","nxdomain","no_mail_host"],"description":"The local check that decided a free `invalid`."}}},"EmailVerifyResult":{"type":"object","required":["status","email","source","confidence","flags","mx_provider","checked_at","candidates"],"properties":{"status":{"type":"string","enum":["valid","invalid","catch_all","unknown"],"description":"`catch_all`, `unknown`, and a withheld below-floor answer bill 0; the answer's `reason_code` says why (ambiguous, zero_hits, sources_disagree, below_min_confidence, provider_error, …).\n"},"email":{"type":"string","description":"The normalized address (lower-cased), or the trimmed input when it did not parse."},"source":{"type":"string","nullable":true,"enum":["local","vendor"],"description":"`local` = a free DNS / syntax `invalid`; `vendor` = a delivered vendor check."},"confidence":{"type":"string","nullable":true,"enum":["high","medium","low"],"description":"Two agreeing checks = high; one = medium; conflicting checks = low (status `unknown`). Kept on a below-floor withhold.\n"},"flags":{"type":"array","items":{"type":"string","enum":["role_based","free_mailbox","disposable","relay","security_gateway","vendor_disagreement"]}},"mx_provider":{"type":"string","nullable":true,"description":"Mailbox provider reported by the check (for example `google`, `microsoft`)."},"checked_at":{"type":"string","format":"date-time"},"candidates":{"type":"array","description":"One row per vendor check — provider, hop status, and the normalized verdict when it answered.","items":{"type":"object","required":["provider","status"],"properties":{"provider":{"$ref":"#/components/schemas/ProviderName"},"status":{"type":"string"},"verdict":{"type":"string","enum":["valid","invalid","catch_all","unknown"]}}}}}},"EmailVerifyResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["email.verify"]},"input":{"$ref":"#/components/schemas/EmailVerifyRequest"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"type":"string","enum":["valid","invalid","catch_all","unknown"]}}}]},"result":{"$ref":"#/components/schemas/EmailVerifyResult"}}}]},"PersonVerifyEmploymentResponse":{"allOf":[{"$ref":"#/components/schemas/RunEnvelopeBase"},{"type":"object","required":["input","result"],"properties":{"capability":{"type":"string","enum":["person.verify_employment"]},"input":{"$ref":"#/components/schemas/PersonVerifyEmploymentInput"},"answer":{"allOf":[{"$ref":"#/components/schemas/Answer"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/EmploymentOutcome"}}}]},"result":{"$ref":"#/components/schemas/EmploymentResult"}}}]},"IncompleteRunResponse":{"type":"object","description":"GET /v1/runs/:id while the run is `running` or `failed` (no decision row). Inspect status, error, usage. answer / result / es_decision are null. `trace` only on `explain=full`. Not imported by External Services.\n","required":["ok","schema_version","run_id","status","error","answer","usage"],"properties":{"ok":{"type":"boolean","enum":[true]},"schema_version":{"type":"string","enum":["2"]},"run_id":{"type":"string"},"status":{"type":"string","enum":["running","failed"]},"error":{"type":"string","nullable":true},"answer":{"type":"null"},"result":{"type":"null"},"es_decision":{"type":"null"},"sources":{"type":"array","items":{"$ref":"#/components/schemas/FieldAttribution"}},"usage":{"$ref":"#/components/schemas/Usage"},"trace":{"$ref":"#/components/schemas/RunTrace"}}},"RunGetResponse":{"oneOf":[{"$ref":"#/components/schemas/IncompleteRunResponse"},{"allOf":[{"$ref":"#/components/schemas/CompanyResolveResponse"},{"type":"object","required":["candidates"],"properties":{"candidates":{"type":"array","items":{"$ref":"#/components/schemas/Candidate"}}}}]},{"allOf":[{"$ref":"#/components/schemas/PersonVerifyEmploymentResponse"},{"type":"object","required":["candidates"],"properties":{"candidates":{"type":"array","items":{"$ref":"#/components/schemas/Candidate"}}}}]},{"allOf":[{"$ref":"#/components/schemas/CompanyDomainResponse"},{"type":"object","required":["candidates"],"properties":{"candidates":{"type":"array","items":{"$ref":"#/components/schemas/Candidate"}}}}]}]},"SubjectEraseCounts":{"type":"object","required":["runs","attempts","candidates","decisions","entities","entity_identifiers","raw_objects","person_language_cache","employment_cache","person_phone_cache","person_title_cache","person_name_cache"],"properties":{"runs":{"type":"integer"},"attempts":{"type":"integer"},"candidates":{"type":"integer"},"decisions":{"type":"integer"},"entities":{"type":"integer"},"entity_identifiers":{"type":"integer"},"raw_objects":{"type":"integer"},"person_language_cache":{"type":"integer","description":"person.language cached answers reachable from this subject (indexed by a SHA-256 of every email / LinkedIn slug the answer exposes)."},"employment_cache":{"type":"integer","description":"person.verify_employment cached verdicts reachable from this subject (same hashed-subject indexing; 0 when D1 is not bound)."},"person_phone_cache":{"type":"integer","description":"person.phone cached answers reachable from this subject (same hashed subject index)."},"person_title_cache":{"type":"integer","description":"person.title cached answers reachable from this subject (same hashed subject index)."},"person_name_cache":{"type":"integer","description":"person.name cached answers reachable from this subject (same hashed subject index)."},"person_b2b_social_cache":{"type":"integer","description":"person.b2b_social cached answers reachable from this subject (same hashed subject index)."},"person_email_cache":{"type":"integer","description":"person.email cached answers reachable from this subject (same hashed subject index)."}}},"FeedbackCreate":{"type":"object","required":["category","summary","details","source"],"additionalProperties":false,"properties":{"category":{"type":"string","enum":["api_schema","mcp","data_quality","provider_request","endpoint_request","workflow_friction","product_idea"],"description":"One fixed category per report."},"summary":{"type":"string","minLength":15,"maxLength":150,"description":"Plain-English headline with no IDs or error text, so similar\nreports group together. This is what triage shows.\n"},"details":{"type":"string","maxLength":4000,"description":"Free text. Stays in KingMinos (admin review only); keep personal data and secrets out."},"source":{"type":"string","enum":["user","agent"],"description":"user = the person's own words; agent = something the agent noticed."},"capability":{"type":"string","description":"A live KingMinos capability id (e.g. company.description).\nRequired for `data_quality`.\n","enum":["company.resolve","person.verify_employment","person.language","person.phone","person.title","company.domain","company.hierarchy","company.b2b_social","company.icon","company.description"]},"run_id":{"type":"string","description":"Links the report to the exact call. Required in practice for data-quality reports; must be a run the caller's tenant owns."},"requested":{"type":"string","maxLength":200,"description":"The provider, endpoint or field they wanted. Required for `provider_request` / `endpoint_request`."},"task_context":{"type":"string","maxLength":1000,"description":"What they were trying to do, without the actual values."},"sentiment":{"type":"string","enum":["positive","negative","neutral"]}}},"FeedbackCreated":{"type":"object","required":["ok","feedback_id","message"],"properties":{"ok":{"type":"boolean","enum":[true]},"feedback_id":{"type":"string","description":"`fb_…` id. Quote it when following up."},"deduped":{"type":"boolean","description":"True when an identical same-day report was replayed instead of filed again."},"message":{"type":"string","enum":["Thanks, the KingMinos team reviews this."]}}},"CredentialListResponse":{"type":"object","required":["ok","schema_version","credentials"],"properties":{"ok":{"type":"boolean","enum":[true]},"schema_version":{"type":"string","enum":["2"]},"workspace_id":{"type":["string","null"]},"credentials":{"type":"array","items":{"type":"object","required":["provider","label","kind","status","configured","house_managed","mask"],"properties":{"provider":{"type":"string","enum":["zoominfo","leadmagic","findymail","wiza","aiark","builtwith","prospeo","apollo","companyurlfinder","peopledatalabs","hginsights","enrichcrm","lemlist","snovio","cognism","lusha","starbridge","clay","databar","quickenrich","getleads","blitzapi"]},"label":{"type":"string"},"kind":{"type":"string","enum":["api_key","oauth"]},"status":{"type":"string","enum":["set","managed","unset"]},"configured":{"type":"boolean"},"house_managed":{"type":"boolean"},"mask":{"type":["string","null"]}}}}}},"CredentialStoreResponse":{"type":"object","required":["ok","schema_version","provider","kek_version"],"properties":{"ok":{"type":"boolean","enum":[true]},"schema_version":{"type":"string","enum":["2"]},"provider":{"type":"string","enum":["zoominfo","leadmagic","findymail","wiza","aiark","builtwith","prospeo","apollo","companyurlfinder","peopledatalabs","hginsights","enrichcrm","lemlist","snovio","cognism","lusha","starbridge","clay","databar","quickenrich","getleads","blitzapi"]},"kek_version":{"type":"integer","enum":[1]}}},"CredentialRevokeResponse":{"type":"object","required":["ok","schema_version","provider","revoked"],"properties":{"ok":{"type":"boolean","enum":[true]},"schema_version":{"type":"string","enum":["2"]},"provider":{"type":"string","enum":["zoominfo","leadmagic","findymail","wiza","aiark","builtwith","prospeo","apollo","companyurlfinder","peopledatalabs","hginsights","enrichcrm","lemlist","snovio","cognism","lusha","starbridge","clay","databar","quickenrich","getleads","blitzapi"]},"revoked":{"type":"boolean","enum":[true]}}},"AdminPrivacyEraseRequest":{"type":"object","required":["reason"],"additionalProperties":false,"properties":{"email":{"type":"string"},"linkedin_url":{"type":"string"},"reason":{"type":"string","enum":["deletion","objection","opt_out"]}}},"AdminPrivacyEraseResponse":{"type":"object","required":["ok","schema_version","reason","subject_keys","suppressed","tenants","customer_tenants_to_notify"],"properties":{"ok":{"type":"boolean","enum":[true]},"schema_version":{"type":"string","enum":["2"]},"reason":{"type":"string","enum":["deletion","objection","opt_out"]},"subject_keys":{"type":"array","items":{"type":"string"}},"suppressed":{"type":"object","required":["added","total_identifiers","ids"],"properties":{"added":{"type":"integer"},"total_identifiers":{"type":"integer"},"ids":{"type":"array","items":{"type":"string"},"description":"64-char hex digests (never the identifier)."}}},"tenants":{"type":"array","items":{"type":"object","additionalProperties":true}},"customer_tenants_to_notify":{"type":"array","items":{"type":"string"}}}},"AdminPrivacySuppression":{"type":"object","required":["id","kind","reason","created_at","actor"],"properties":{"id":{"type":"string","description":"64-char hex subject_hash. Use as DELETE /{id}."},"kind":{"type":"string","enum":["email","li"],"description":"liurl short links and /pub/ URLs store as li."},"reason":{"type":"string","enum":["deletion","objection","opt_out"]},"created_at":{"type":"string"},"actor":{"type":"string"},"hint":{"type":["string","null"],"description":"Non-PII hint when already stored (e.g. email:@smoke.kingminos.invalid). Absent or null otherwise. Never a raw email or URL."}}},"AdminPrivacySuppressionListResponse":{"type":"object","required":["ok","schema_version","suppressions","next_cursor"],"properties":{"ok":{"type":"boolean","enum":[true]},"schema_version":{"type":"string","enum":["2"]},"suppressions":{"type":"array","items":{"$ref":"#/components/schemas/AdminPrivacySuppression"}},"next_cursor":{"type":["string","null"]}}},"AdminPrivacySuppressionCheckResponse":{"type":"object","required":["ok","schema_version","suppressed","id"],"properties":{"ok":{"type":"boolean","enum":[true]},"schema_version":{"type":"string","enum":["2"]},"suppressed":{"type":"boolean"},"id":{"type":["string","null"]},"kind":{"type":["string","null"],"enum":["email","li",null]},"reason":{"type":["string","null"],"enum":["deletion","objection","opt_out",null]},"created_at":{"type":["string","null"]},"actor":{"type":["string","null"]},"hint":{"type":["string","null"]}}},"AdminPrivacySuppressionDeleteRequest":{"type":"object","required":["reason"],"additionalProperties":false,"properties":{"reason":{"type":"string","minLength":1,"maxLength":500,"description":"Why the suppression is being lifted (audited)."}}},"AdminPrivacySuppressionDeleteResponse":{"type":"object","required":["ok","schema_version","deleted","id","kind","reason"],"properties":{"ok":{"type":"boolean","enum":[true]},"schema_version":{"type":"string","enum":["2"]},"deleted":{"type":"boolean","enum":[true]},"id":{"type":"string"},"kind":{"type":"string","enum":["email","li"]},"reason":{"type":"string"},"note":{"type":"string"}}},"SubjectEraseResponse":{"type":"object","required":["ok","schema_version","subject_key","erased"],"properties":{"ok":{"type":"boolean","enum":[true]},"schema_version":{"type":"string","enum":["2"]},"subject_key":{"type":"string"},"erased":{"$ref":"#/components/schemas/SubjectEraseCounts"}}},"ErrorBody":{"type":"object","description":"Error envelope. Every error has `ok:false`, `schema_version`, `error`,\n`retryable`, `request_id` (from `cf-ray`). Optional `run_id` when a run\nexists, plus `detail` / `hint` / `details`. 401 unauthorized includes\n`detail` (missing | malformed | mismatch) and a static `hint`. Never\nechoes the API key. 429s send `Retry-After`. Aligned with\n`openapi.es.yaml` ErrorBody — no composition.\n","required":["ok","schema_version","error","retryable","request_id"],"properties":{"ok":{"type":"boolean","enum":[false]},"schema_version":{"type":"string","enum":["2"]},"error":{"type":"string"},"retryable":{"type":"boolean","description":"True on 429 and 5xx (including credential_broker_* 503s and store_unavailable). Flow may retry the same Idempotency-Key.\n"},"request_id":{"type":"string","nullable":true,"description":"Cloudflare `cf-ray` when present."},"run_id":{"type":"string","description":"Present when the error names a run."},"detail":{"type":"string","enum":["missing","malformed","mismatch"],"description":"401 only. missing — no Authorization (Salesforce Custom Headers\nempty); malformed — not Bearer <token>; mismatch — key did not match.\n"},"hint":{"type":"string","description":"Static one-liner on 401 (no secrets). Diagnosis: `docs/salesforce-named-cred.md`.\n"},"details":{"description":"Optional structured extras on non-401 errors."}}}}}}
