{"openapi":"3.0.3","info":{"title":"Sparhawk Agent API","version":"1.0.0","description":"Read and write sales leads: companies, people, sources (public channels such as blogs, X accounts, LinkedIn pages, Substacks), posts, articles, opportunities, pipeline deals and Discover suggestions. Sources are read for posts, Claude writes articles from the posts and matches them to saved themes, and opportunities and Discover suggestions are found in the articles. The older paths /signals (now /posts) and /highlights (now /articles) still work, and JSON field names are unchanged. Authenticate with `Authorization: Bearer sk375_…` (create keys at /settings/api-keys). Key scopes (read, write, review, delete) are intersected with the key owner's role."},"servers":[{"url":"https://sparhawk.io"}],"security":[{"bearer":[]}],"tags":[{"name":"Companies","description":"Target accounts and sales leads."},{"name":"People","description":"Contacts and decision makers."},{"name":"Sources","description":"Public channels we read for posts: company blogs, X accounts, Substacks, RSS feeds. New sources need approval. Only admins can read them on demand."},{"name":"Posts","description":"What sources and agents find about your accounts. Posts are dated, de-duplicated and screened, then grouped into articles. /signals is the older path."},{"name":"Articles","description":"Stories Claude writes from posts, matched to saved themes: the feed. /highlights is the older path."},{"name":"Opportunities","description":"Leads found in the articles: sales, partnership and product openings, with outreach and follow-up."},{"name":"Pipeline","description":"Deals: opportunities once engaged, tracked through stages to won or lost, kept current from Gmail and Calendar."},{"name":"Discover","description":"Companies, people and themes the news keeps mentioning that the team doesn't track yet."},{"name":"Themes","description":"Saved natural-language questions, matched to companies, people, posts and articles."},{"name":"Lookup","description":"Check what is already here, or look a company or person up on the web."},{"name":"Review","description":"Approve, reject or merge suggested companies, people and sources."},{"name":"Documents","description":"Imported documents and the companies and people AI extracts from them."},{"name":"Gmail","description":"Gmail drafts and reply detection for outreach."},{"name":"Meta","description":"Who you are."}],"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer"}},"schemas":{"CompanyInput":{"type":"object","required":["name"],"properties":{"name":{"description":"Name","type":"string","nullable":false},"domain":{"description":"Derived from website if empty","type":"string","nullable":true},"website":{"description":"Website","type":"string","format":"uri","nullable":true},"linkedinUrl":{"description":"LinkedIn","type":"string","format":"uri","nullable":true},"email":{"description":"A generic contact address (partnerships@, info@…). Filled in automatically when the company is approved and has none: found on the web, else guessed as info@<domain>; metadata.emailBasis says which ({tier: found|guess, source?, at}). Never overwritten automatically","type":"string","format":"email","nullable":true},"industry":{"description":"Industry","type":"string","nullable":true},"employeeRange":{"description":"Employees","type":"string","nullable":true},"stage":{"description":"Stage","type":"string","enum":["new","researching","qualified","contacted","meeting","proposal","won","lost","not_a_fit"]},"hqCity":{"description":"HQ city","type":"string","nullable":true},"hqCountry":{"description":"HQ country","type":"string","nullable":true},"description":{"description":"Description","type":"string","nullable":true},"tags":{"description":"Tags","type":"array","items":{"type":"string"}},"notes":{"description":"Notes","type":"string","nullable":true},"metadata":{"description":"Free-form JSON for agent-enriched data","type":"object","additionalProperties":true}}},"Company":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"description":"Name","type":"string","nullable":false},"domain":{"description":"Derived from website if empty","type":"string","nullable":true},"website":{"description":"Website","type":"string","format":"uri","nullable":true},"linkedinUrl":{"description":"LinkedIn","type":"string","format":"uri","nullable":true},"email":{"description":"A generic contact address (partnerships@, info@…). Filled in automatically when the company is approved and has none: found on the web, else guessed as info@<domain>; metadata.emailBasis says which ({tier: found|guess, source?, at}). Never overwritten automatically","type":"string","format":"email","nullable":true},"industry":{"description":"Industry","type":"string","nullable":true},"employeeRange":{"description":"Employees","type":"string","nullable":true},"stage":{"description":"Stage","type":"string","enum":["new","researching","qualified","contacted","meeting","proposal","won","lost","not_a_fit"]},"hqCity":{"description":"HQ city","type":"string","nullable":true},"hqCountry":{"description":"HQ country","type":"string","nullable":true},"description":{"description":"Description","type":"string","nullable":true},"tags":{"description":"Tags","type":"array","items":{"type":"string"}},"notes":{"description":"Notes","type":"string","nullable":true},"metadata":{"description":"Free-form JSON for agent-enriched data","type":"object","additionalProperties":true},"reviewStatus":{"type":"string","enum":["pending","approved","rejected"]},"createdVia":{"type":"string","enum":["ui","api","extraction","monitor","discovery"],"description":"How it was created (`monitor`: read from a source)"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PersonInput":{"type":"object","required":["fullName"],"properties":{"fullName":{"description":"Full name","type":"string","nullable":false},"title":{"description":"Title","type":"string","nullable":true},"companyId":{"description":"ID of a record in /companies","type":"string","format":"uuid","nullable":true},"email":{"description":"Email","type":"string","format":"email","nullable":true},"phone":{"description":"Phone","type":"string","nullable":true},"linkedinUrl":{"description":"LinkedIn","type":"string","format":"uri","nullable":true},"location":{"description":"Location","type":"string","nullable":true},"tags":{"description":"Tags","type":"array","items":{"type":"string"}},"notes":{"description":"Notes","type":"string","nullable":true},"metadata":{"description":"Free-form JSON for agent-enriched data","type":"object","additionalProperties":true}}},"Person":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"fullName":{"description":"Full name","type":"string","nullable":false},"title":{"description":"Title","type":"string","nullable":true},"companyId":{"description":"ID of a record in /companies","type":"string","format":"uuid","nullable":true},"email":{"description":"Email","type":"string","format":"email","nullable":true},"phone":{"description":"Phone","type":"string","nullable":true},"linkedinUrl":{"description":"LinkedIn","type":"string","format":"uri","nullable":true},"location":{"description":"Location","type":"string","nullable":true},"tags":{"description":"Tags","type":"array","items":{"type":"string"}},"notes":{"description":"Notes","type":"string","nullable":true},"metadata":{"description":"Free-form JSON for agent-enriched data","type":"object","additionalProperties":true},"reviewStatus":{"type":"string","enum":["pending","approved","rejected"]},"createdVia":{"type":"string","enum":["ui","api","extraction","monitor","discovery"],"description":"How it was created (`monitor`: read from a source)"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SourceInput":{"type":"object","required":["name","url"],"properties":{"name":{"description":"Name","type":"string","nullable":false},"url":{"description":"URL","type":"string","format":"uri","nullable":false},"kind":{"description":"Channel","type":"string","enum":["blog","x","substack","rss","youtube","linkedin","news","website","other"]},"companyId":{"description":"ID of a record in /companies","type":"string","format":"uuid","nullable":true},"personId":{"description":"ID of a record in /people","type":"string","format":"uuid","nullable":true},"crawlFrequency":{"description":"Check frequency","type":"string","enum":["hourly","daily","weekly","monthly","manual"]},"active":{"description":"Active","type":"boolean"},"lastCrawledAt":{"description":"Last run","type":"string","format":"date-time","nullable":true},"lastCrawlStatus":{"description":"Last run result","type":"string","nullable":true},"description":{"description":"What to look for (instructions for the AI / agents)","type":"string","nullable":true},"tags":{"description":"Tags","type":"array","items":{"type":"string"}},"metadata":{"description":"Free-form JSON for agent-enriched data","type":"object","additionalProperties":true}}},"Source":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"description":"Name","type":"string","nullable":false},"url":{"description":"URL","type":"string","format":"uri","nullable":false},"kind":{"description":"Channel","type":"string","enum":["blog","x","substack","rss","youtube","linkedin","news","website","other"]},"companyId":{"description":"ID of a record in /companies","type":"string","format":"uuid","nullable":true},"personId":{"description":"ID of a record in /people","type":"string","format":"uuid","nullable":true},"crawlFrequency":{"description":"Check frequency","type":"string","enum":["hourly","daily","weekly","monthly","manual"]},"active":{"description":"Active","type":"boolean"},"lastCrawledAt":{"description":"Last run","type":"string","format":"date-time","nullable":true},"lastCrawlStatus":{"description":"Last run result","type":"string","nullable":true},"description":{"description":"What to look for (instructions for the AI / agents)","type":"string","nullable":true},"tags":{"description":"Tags","type":"array","items":{"type":"string"}},"metadata":{"description":"Free-form JSON for agent-enriched data","type":"object","additionalProperties":true},"reviewStatus":{"type":"string","enum":["pending","approved","rejected"]},"createdVia":{"type":"string","enum":["ui","api","extraction","monitor","discovery"],"description":"How it was created (`monitor`: read from a source)"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PostInput":{"type":"object","required":["title"],"properties":{"title":{"description":"Title","type":"string","nullable":false},"kind":{"description":"Kind","type":"string","enum":["news","funding","hiring","leadership_change","product","partnership","event","social","regulatory","other","email","meeting"]},"url":{"description":"URL","type":"string","format":"uri","nullable":true},"publishedAt":{"description":"Published","type":"string","format":"date-time","nullable":true},"relevance":{"description":"Relevance (0-100)","type":"integer","nullable":true,"minimum":0,"maximum":100},"companyId":{"description":"ID of a record in /companies","type":"string","format":"uuid","nullable":true},"personId":{"description":"ID of a record in /people","type":"string","format":"uuid","nullable":true},"sourceId":{"description":"ID of a record in /sources","type":"string","format":"uuid","nullable":true},"summary":{"description":"Summary","type":"string","nullable":true},"tags":{"description":"Tags","type":"array","items":{"type":"string"}},"raw":{"description":"Raw data (JSON)","type":"object","additionalProperties":true}}},"Post":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"description":"Title","type":"string","nullable":false},"kind":{"description":"Kind","type":"string","enum":["news","funding","hiring","leadership_change","product","partnership","event","social","regulatory","other","email","meeting"]},"url":{"description":"URL","type":"string","format":"uri","nullable":true},"publishedAt":{"description":"Published","type":"string","format":"date-time","nullable":true},"relevance":{"description":"Relevance (0-100)","type":"integer","nullable":true,"minimum":0,"maximum":100},"companyId":{"description":"ID of a record in /companies","type":"string","format":"uuid","nullable":true},"personId":{"description":"ID of a record in /people","type":"string","format":"uuid","nullable":true},"sourceId":{"description":"ID of a record in /sources","type":"string","format":"uuid","nullable":true},"summary":{"description":"Summary","type":"string","nullable":true},"tags":{"description":"Tags","type":"array","items":{"type":"string"}},"raw":{"description":"Raw data (JSON)","type":"object","additionalProperties":true},"createdVia":{"type":"string","enum":["ui","api","extraction","monitor","discovery"],"description":"How it was created (`monitor`: read from a source)"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Article":{"type":"object","description":"A story Claude writes from posts about approved companies and people, matched to saved themes.","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"summary":{"type":"string"},"keyPoints":{"type":"array","items":{"type":"string"}},"whyItMatters":{"type":"string","nullable":true},"category":{"type":"string","enum":["product","strategy","mna","funding","partnership","leadership","financials","legal","event","other"]},"importance":{"type":"integer","minimum":0,"maximum":100},"happenedAt":{"type":"string","format":"date-time"},"read":{"type":"boolean","description":"For the caller"},"starred":{"type":"boolean","description":"For the caller"},"archived":{"type":"boolean","description":"For the caller: archived by them, or older than the archive cutoff (2 weeks by default)"},"themes":{"type":"array","description":"The saved themes it relates to (score 50-100, with a reason). Only theme-matched articles are returned.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"score":{"type":"integer"},"reason":{"type":"string","nullable":true}}}},"entities":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["company","person"]},"id":{"type":"string"},"name":{"type":"string"}}}},"sources":{"type":"array","description":"The posts this article was built from","items":{"type":"object","properties":{"signalId":{"type":"string","format":"uuid","description":"The post's id"},"title":{"type":"string"},"url":{"type":"string","nullable":true},"host":{"type":"string","nullable":true},"date":{"type":"string","description":"When the post was published (or found), as a display date such as 12 Oct 2026"},"at":{"type":"string","format":"date-time","description":"The same moment as `date`, as an ISO timestamp (sort by this, not by `date`)"},"visibility":{"type":"string","enum":["public","proprietary"]},"monitorId":{"type":"string","format":"uuid","nullable":true,"description":"The source that found the post"},"monitor":{"type":"string","nullable":true,"description":"That source's name"},"channel":{"type":"string","nullable":true,"description":"That source's channel (blog, x, substack, rss, …)"}}}},"shares":{"type":"array","description":"Shares to or from the caller","items":{"type":"object"}},"opportunities":{"type":"array","description":"The opportunities that cite this article (open ones first); start one with POST /opportunities","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"status":{"type":"string","enum":["new","working","won","dropped"]},"kind":{"type":"string","enum":["sales","partnership","product"]},"owner":{"type":"string","nullable":true,"description":"The owner's name"}}}}}},"ArticleInput":{"type":"object","required":["title","summary"],"description":"At least one post is required: signalIds, signals and/or url. A post belongs to at most one article (409 names the article that has it).","properties":{"title":{"type":"string","maxLength":200,"description":"Headline, leading with the company or person"},"summary":{"type":"string","maxLength":1000,"description":"1-2 sentences: what happened"},"category":{"type":"string","enum":["product","strategy","mna","funding","partnership","leadership","financials","legal","event","other"],"default":"other"},"importance":{"type":"integer","minimum":0,"maximum":100,"default":60,"description":"80-100 must know; 50-79 worth knowing; below 50 minor"},"keyPoints":{"type":"array","maxItems":6,"items":{"type":"string","maxLength":400},"description":"Specifics the summary doesn't already say"},"whyItMatters":{"type":"string","maxLength":800,"nullable":true},"happenedAt":{"type":"string","format":"date-time","description":"When the story broke (default: the earliest post's publishedAt, else now)"},"signalIds":{"type":"array","maxItems":50,"items":{"type":"string","format":"uuid"},"description":"Existing posts this article is written from"},"signals":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/PostInput"},"description":"New posts to create for it (upserted on url, like POST /posts?upsert=true)"},"url":{"type":"string","description":"Shorthand for one post: a post for this url (made from the article's title and summary unless one exists)"},"companyId":{"type":"string","format":"uuid","nullable":true,"description":"With url: the company that post is about"},"personId":{"type":"string","format":"uuid","nullable":true,"description":"With url: the person that post is about"}}},"ArticleUpdate":{"type":"object","description":"Partial: only what you send changes. Changing the text, category or posts re-matches it against the themes.","properties":{"title":{"type":"string","maxLength":200,"description":"Headline, leading with the company or person"},"summary":{"type":"string","maxLength":1000,"description":"1-2 sentences: what happened"},"category":{"type":"string","enum":["product","strategy","mna","funding","partnership","leadership","financials","legal","event","other"],"default":"other"},"importance":{"type":"integer","minimum":0,"maximum":100,"default":60,"description":"80-100 must know; 50-79 worth knowing; below 50 minor"},"keyPoints":{"type":"array","maxItems":6,"items":{"type":"string","maxLength":400},"description":"Specifics the summary doesn't already say"},"whyItMatters":{"type":"string","maxLength":800,"nullable":true},"happenedAt":{"type":"string","format":"date-time","description":"When the story broke (default: the earliest post's publishedAt, else now)"},"addSignalIds":{"type":"array","maxItems":50,"items":{"type":"string","format":"uuid"},"description":"Existing posts to add"},"addSignals":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/PostInput"},"description":"New posts to add (upserted on url)"},"removeSignalIds":{"type":"array","maxItems":50,"items":{"type":"string","format":"uuid"},"description":"Posts to remove (it must keep at least one)"}}},"Outreach":{"type":"object","description":"Who to contact and what to say, researched on request. Contact details come only from the web sources found: a LinkedIn URL or email the sources don't show is null.","properties":{"contact":{"type":"object","nullable":true,"description":"The best person to approach; null when the sources don't clearly identify one","properties":{"name":{"type":"string"},"title":{"type":"string","nullable":true},"linkedinUrl":{"type":"string","format":"uri","nullable":true},"email":{"type":"string","format":"email","nullable":true},"confidence":{"type":"string","enum":["high","medium","low"]},"basis":{"type":"string","description":"Which source says this person holds this role"}}},"alternates":{"type":"array","maxItems":3,"items":{"type":"object","properties":{"name":{"type":"string"},"title":{"type":"string","nullable":true}}}},"subject":{"type":"string","description":"An email subject line"},"pitch":{"type":"string","description":"An elevator pitch (90-130 words) in the team's voice"},"talkingPoints":{"type":"array","maxItems":3,"items":{"type":"string"}},"sources":{"type":"array","items":{"type":"string","format":"uri"},"description":"The web pages the research drew on"},"at":{"type":"string","format":"date-time"},"byId":{"type":"string","nullable":true,"description":"The user who asked for it"}}},"Opportunity":{"type":"object","description":"A lead found in the day's articles, read against the team's guidance (Resources page), its themes and the companies on file.","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["sales","partnership","product"],"description":"sales: an account whose news makes a pitch timely; partnership: an account to work with; product: something to evaluate or build"},"status":{"type":"string","enum":["new","working","won","dropped"]},"account":{"type":"string","description":"The company or person to act on, as named in the news"},"company":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string","nullable":true}}},"person":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"companyId":{"type":"string","format":"uuid","nullable":true},"personId":{"type":"string","format":"uuid","nullable":true},"title":{"type":"string","description":"The account and the opening, in a headline"},"trigger":{"type":"string","description":"The news that makes it timely"},"whoToApproach":{"type":"string","description":"Which roles or people at the account to approach"},"angle":{"type":"string","description":"Sales: the pitch angle. Partnership: why it matters for the team. Product: what to evaluate or build"},"firstMove":{"type":"string","nullable":true,"description":"Partnership: a cheap first move. Product: the effort (S, M or L). Sales: null"},"score":{"type":"integer","minimum":0,"maximum":100,"description":"How strong and timely the opening is"},"highlightIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The articles it came from"},"highlights":{"type":"array","description":"The articles it came from, each with the pages (`sources`) its posts came from","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"date":{"type":"string","format":"date"},"sources":{"type":"array","items":{"type":"object","properties":{"host":{"type":"string","nullable":true},"url":{"type":"string","nullable":true},"title":{"type":"string"}}}}}}},"outreach":{"allOf":[{"$ref":"#/components/schemas/Outreach"}],"nullable":true,"description":"Set once POST /opportunities/{id}/outreach has run"},"ownerId":{"type":"string","nullable":true},"owner":{"type":"string","nullable":true,"description":"The owner's name"},"nextStep":{"type":"string","nullable":true,"maxLength":500,"description":"What happens next on this lead (set by PATCH, or automatically to a follow-up after a send)"},"dueAt":{"type":"string","format":"date-time","nullable":true,"description":"When the next step is due; due or overdue leads lead GET /today"},"lastTouchAt":{"type":"string","format":"date-time","nullable":true,"description":"The last time anyone did something to it (status, owner, next step, note, send, reply)"},"lastSeenAt":{"type":"string","format":"date-time","description":"Bumped when the daily run finds the same opening again"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"OpportunityEvent":{"type":"object","description":"Something that happened to an opportunity, newest first on the detail (the latest 200)","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["found","status","owner","next_step","note","outreach","drafted","sent","replied","bounced","touch","meeting_notes"],"description":"found: by the daily run; status/owner/next_step: changed; note: free text; outreach: research prepared; drafted: an email was saved to the rep's Gmail drafts; sent: an email went out (from Sparhawk, or a Gmail draft the sync saw delivered); replied: a reply was recorded; bounced: the Gmail sync saw a delivery-failure notice for an email; touch: a LinkedIn message, call, intro or meeting was logged by hand; meeting_notes: notes were written from a call transcript"},"body":{"type":"string","nullable":true,"description":"The note text, the new status or next step, the subject sent, the reply snippet, or a touch's note"},"meta":{"type":"object","additionalProperties":true,"description":"Details by kind, e.g. { from } for status, { ownerId } for owner, { dueAt } for next_step, { to, messageId } for sent and replied, { to, messageId, draftId } for drafted, { to, via: \"gmail\", messageId } for bounced, { channel } for touch, { dealId, activityId, source } for meeting_notes, plus { via: \"gmail\" } when the Gmail sync recorded it"},"author":{"type":"string","nullable":true,"description":"Who did it (null for the daily run)"},"authorId":{"type":"string","nullable":true},"at":{"type":"string","format":"date-time"}}},"OutreachMessage":{"type":"object","description":"An outreach email sent from Sparhawk, or saved to the rep's Gmail drafts","properties":{"id":{"type":"string","format":"uuid"},"to":{"type":"string","format":"email"},"subject":{"type":"string"},"body":{"type":"string"},"sender":{"type":"string","nullable":true,"description":"The rep's name; replies go to their address"},"sentAt":{"type":"string","format":"date-time","description":"When it was sent from Sparhawk, or when the Gmail draft was created"},"repliedAt":{"type":"string","format":"date-time","nullable":true},"replySnippet":{"type":"string","nullable":true},"via":{"type":"string","nullable":true,"description":"`gmail-draft` for a Gmail draft; otherwise the Resend message id of a send from Sparhawk"},"draftId":{"type":"string","nullable":true,"description":"The Gmail draft id, for drafts"},"deliveredAt":{"type":"string","format":"date-time","nullable":true,"description":"For drafts: when the Gmail sync saw it sent; null until then"}}},"OpportunityDetail":{"allOf":[{"$ref":"#/components/schemas/Opportunity"},{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/OpportunityEvent"},"description":"Everything that happened to it"},"messages":{"type":"array","items":{"$ref":"#/components/schemas/OutreachMessage"},"description":"The emails sent from Sparhawk or saved to Gmail drafts, latest first"}}}]},"AddressSuggestion":{"type":"object","properties":{"email":{"type":"string","format":"email"},"tier":{"type":"string","enum":["found","web","pattern","guess"],"description":"found: in a research source or on file in Sparhawk. web: shown on a page about them. pattern: the company's format, from addresses on file or published on the web — unverified. guess: a common format, nothing known — unverified"},"basis":{"type":"string","description":"Where it comes from, in words"}}},"Suggestion":{"type":"object","description":"Discover: a company, person or theme the news keeps mentioning that the team doesn't track yet, found in the recent articles. The same name found again on a later run bumps `mentions` instead of adding a row.","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["company","person","theme"]},"status":{"type":"string","enum":["new","accepted","dismissed"]},"name":{"type":"string"},"hint":{"type":"string","nullable":true,"description":"company: its domain if a source gave it; person: employer and title; theme: the search query the theme would run"},"why":{"type":"string","description":"One or two sentences: why following it would widen the opportunity set"},"mentions":{"type":"integer","description":"How many runs have found it"},"score":{"type":"integer","minimum":0,"maximum":100},"evidence":{"type":"object","description":"Where it came from","properties":{"highlightIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The articles it came from"},"signalIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The posts it came from"},"quotes":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"host":{"type":"string","nullable":true}}},"description":"Short verbatim phrases from the articles"},"near":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["company","person"]},"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"description":"Tracked companies or people it was mentioned alongside"}}},"acceptedType":{"type":"string","enum":["company","person"],"nullable":true,"description":"The record it became when accepted (null for a theme)"},"acceptedId":{"type":"string","format":"uuid","nullable":true,"description":"The company, person or theme it became"},"acceptedById":{"type":"string","nullable":true},"acceptedBy":{"type":"string","nullable":true,"description":"Who accepted or dismissed it"},"lastSeenAt":{"type":"string","format":"date-time","description":"Bumped when a run finds it again"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"FoundEntity":{"type":"object","properties":{"type":{"type":"string","enum":["company","person"]},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"subtitle":{"type":"string","nullable":true,"description":"Company: industry · domain; person: title · company"},"reviewStatus":{"type":"string","enum":["pending","approved","rejected"]}}},"LookupSuggestion":{"type":"object","properties":{"name":{"type":"string"},"hint":{"type":"string","nullable":true,"description":"Company: its website's domain; person: their company. Pass it to POST /lookup as hint"},"why":{"type":"string","description":"Who they are and why they match"},"existing":{"type":"object","nullable":true,"description":"Already in Sparhawk. sameIdentity false: only the name matches, so it may be a different company or person","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"reviewStatus":{"type":"string","enum":["pending","approved","rejected"]},"sameIdentity":{"type":"boolean"}}}}},"LookupResult":{"type":"object","properties":{"kind":{"type":"string","enum":["companies","people"]},"candidate":{"type":"object","nullable":true,"description":"The fields found, to check before saving with POST /companies or /people. Company: name, website, domain, linkedinUrl, industry, hqCity, hqCountry, description; person: fullName, title, companyId (when their employer is in Sparhawk), companyName, location, linkedinUrl. null when it's already in Sparhawk (see existing) or the query could mean several (pick one of otherMatches)","additionalProperties":true},"existing":{"type":"object","nullable":true,"description":"Already in Sparhawk. sameIdentity false: only the name matches, so it may be a different company or person","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"reviewStatus":{"type":"string","enum":["pending","approved","rejected"]},"sameIdentity":{"type":"boolean"}}},"sources":{"type":"array","items":{"type":"string"},"description":"The web pages it was found from"},"otherMatches":{"type":"array","description":"Other companies or people the query could mean: look one up again with its query and hint","items":{"type":"object","properties":{"label":{"type":"string"},"query":{"type":"string"},"hint":{"type":"string","nullable":true}}}}}},"Deal":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"account":{"type":"string"},"companyId":{"type":"string","format":"uuid","nullable":true},"company":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"name":{"type":"string"},"domain":{"type":"string","nullable":true}}},"opportunityId":{"type":"string","format":"uuid","nullable":true,"description":"The opportunity it came from"},"ownerId":{"type":"string","nullable":true},"owner":{"type":"string","nullable":true},"stage":{"type":"string","description":"A stage key from GET /pipeline/settings"},"status":{"type":"string","enum":["open","won","lost"]},"reached":{"type":"array","items":{"type":"string"},"description":"Every stage it has been in"},"value":{"type":"integer","nullable":true,"description":"Annual contract value estimate, USD"},"probability":{"type":"integer","nullable":true,"description":"Override; null = the stage's"},"effectiveProbability":{"type":"integer"},"weighted":{"type":"integer","description":"value × effectiveProbability"},"product":{"type":"string","nullable":true,"description":"A product key from Settings → Pipeline (GET /api/v1/pipeline/settings → products; admins can add, rename, archive). Defaults: world_model, humanoid, av_sim, mapping, eval_sets, custom, other. A product's label is accepted too; archived products can't be newly assigned.","example":"world_model"},"productName":{"type":"string","nullable":true,"description":"The product's label"},"summaryLine":{"type":"string","nullable":true,"description":"First sentence of the AI executive summary"},"insights":{"type":"object","description":"AI reads (GET /deals/{id} only): contacts, referrals, nextStep, prep, autoContacts, summary { text, inputHash, at, byId }"},"forecastCategory":{"type":"string","enum":["commit","best_case","pipeline","omit"]},"expectedCloseAt":{"type":"string","format":"date-time","nullable":true},"nextStep":{"type":"string","nullable":true},"dueAt":{"type":"string","format":"date-time","nullable":true},"lostReason":{"type":"string","nullable":true},"closedAt":{"type":"string","format":"date-time","nullable":true},"lastActivityAt":{"type":"string","format":"date-time"},"lastInboundAt":{"type":"string","format":"date-time","nullable":true,"description":"Latest email from them"},"lastOutboundAt":{"type":"string","format":"date-time","nullable":true,"description":"Latest email from us"},"nextMeetingAt":{"type":"string","format":"date-time","nullable":true},"suggestion":{"type":"object","nullable":true,"description":"A stage move read off activity, waiting for the owner: { stage, reason, at, source }"},"brief":{"type":"object","nullable":true,"description":"The AI brief, when written: { summary, risks, buyers, nextBestAction, at }"},"engagement":{"type":"integer","description":"0-100: their emails and meetings in the last 30 days, how many of them, how recently"},"health":{"type":"integer","description":"0-100: engagement and progress minus risks"},"risks":{"type":"array","items":{"type":"string","enum":["no_reply","single_threaded","no_meeting","close_passed","stale","no_value","no_step"]}},"attention":{"type":"array","items":{"type":"string","enum":["reply","overdue","due","stale","suggestion","no_step","close_passed"]}},"canEdit":{"type":"boolean","description":"The caller may change it (owner, admin, or unowned)"}}}}},"paths":{"/api/v1/companies":{"get":{"tags":["Companies"],"summary":"List companies","description":"Target accounts and sales leads.","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Search name, domain, industry, description"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":500}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"updatedSince","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"sort","in":"query","schema":{"type":"string","default":"updatedAt"}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"]}},{"name":"reviewStatus","in":"query","schema":{"type":"string","enum":["all","approved","pending","rejected","not_rejected"],"default":"all"},"description":"not_rejected = approved and pending. Anything else is a 422"},{"name":"ownerId","in":"query","schema":{"type":"string"},"description":"Filter by ownerId (use 'null' for none)"},{"name":"stage","in":"query","schema":{"type":"string"},"description":"Filter by stage (use 'null' for none)"},{"name":"hqCountry","in":"query","schema":{"type":"string"},"description":"Filter by hqCountry (use 'null' for none)"}],"responses":{"200":{"description":"Page of records","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Company"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}}}},"post":{"tags":["Companies"],"summary":"Create company (single or bulk array ≤200)","description":"New records are created with reviewStatus=pending unless the key owner is an approver/admin and the key has the `review` scope. With upsert=true an existing record matched by domain, then case-insensitive name is updated instead (non-empty fields overwrite, tags/metadata merge). A same-named record with a different domain is a different one: it is not matched by name, and a new record is created.","parameters":[{"name":"upsert","in":"query","schema":{"type":"boolean"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CompanyInput"},{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/CompanyInput"}}]}}}},"responses":{"200":{"description":"Upserted existing / bulk results"},"201":{"description":"Created"}}}},"/api/v1/companies/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Companies"],"summary":"Get company with related records","responses":{"200":{"description":"Record plus `related`"}}},"patch":{"tags":["Companies"],"summary":"Update company (partial)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyInput"}}}},"responses":{"200":{"description":"Updated record"}}},"delete":{"tags":["Companies"],"summary":"Delete company (requires delete)","responses":{"200":{"description":"Deleted"}}}},"/api/v1/people":{"get":{"tags":["People"],"summary":"List people","description":"Contacts and decision makers.","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Search fullName, title, email"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":500}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"updatedSince","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"sort","in":"query","schema":{"type":"string","default":"updatedAt"}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"]}},{"name":"reviewStatus","in":"query","schema":{"type":"string","enum":["all","approved","pending","rejected","not_rejected"],"default":"all"},"description":"not_rejected = approved and pending. Anything else is a 422"},{"name":"companyId","in":"query","schema":{"type":"string"},"description":"Filter by companyId (use 'null' for none)"}],"responses":{"200":{"description":"Page of records","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Person"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}}}},"post":{"tags":["People"],"summary":"Create person (single or bulk array ≤200)","description":"New records are created with reviewStatus=pending unless the key owner is an approver/admin and the key has the `review` scope. With upsert=true an existing record matched by email, then linkedinUrl, then case-insensitive fullName is updated instead (non-empty fields overwrite, tags/metadata merge). A same-named record with a different email or linkedinUrl or companyId is a different one: it is not matched by name, and a new record is created.","parameters":[{"name":"upsert","in":"query","schema":{"type":"boolean"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/PersonInput"},{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/PersonInput"}}]}}}},"responses":{"200":{"description":"Upserted existing / bulk results"},"201":{"description":"Created"}}}},"/api/v1/people/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["People"],"summary":"Get person with related records","responses":{"200":{"description":"Record plus `related`"}}},"patch":{"tags":["People"],"summary":"Update person (partial)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonInput"}}}},"responses":{"200":{"description":"Updated record"}}},"delete":{"tags":["People"],"summary":"Delete person (requires delete)","responses":{"200":{"description":"Deleted"}}}},"/api/v1/sources":{"get":{"tags":["Sources"],"summary":"List sources","description":"Public channels we read for posts: company blogs, X accounts, LinkedIn pages, Substacks, RSS feeds. New sources need approval before they're read.","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Search name, url, description"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":500}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"updatedSince","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"sort","in":"query","schema":{"type":"string","default":"updatedAt"}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"]}},{"name":"reviewStatus","in":"query","schema":{"type":"string","enum":["all","approved","pending","rejected","not_rejected"],"default":"all"},"description":"not_rejected = approved and pending. Anything else is a 422"},{"name":"due","in":"query","schema":{"type":"boolean"},"description":"Only approved, active sources that are due to be read, per crawlFrequency since lastCrawledAt"},{"name":"companyId","in":"query","schema":{"type":"string"},"description":"Filter by companyId (use 'null' for none)"},{"name":"personId","in":"query","schema":{"type":"string"},"description":"Filter by personId (use 'null' for none)"},{"name":"kind","in":"query","schema":{"type":"string"},"description":"Filter by kind (use 'null' for none)"},{"name":"active","in":"query","schema":{"type":"string"},"description":"Filter by active (use 'null' for none)"}],"responses":{"200":{"description":"Page of records","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Source"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}}}},"post":{"tags":["Sources"],"summary":"Create source (single or bulk array ≤200)","description":"New records are created with reviewStatus=pending unless the key owner is an approver/admin and the key has the `review` scope. With upsert=true an existing record matched by url is updated instead (non-empty fields overwrite, tags/metadata merge).","parameters":[{"name":"upsert","in":"query","schema":{"type":"boolean"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/SourceInput"},{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/SourceInput"}}]}}}},"responses":{"200":{"description":"Upserted existing / bulk results"},"201":{"description":"Created"}}}},"/api/v1/sources/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Sources"],"summary":"Get source with related records","responses":{"200":{"description":"Record plus `related`"}}},"patch":{"tags":["Sources"],"summary":"Update source (partial)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceInput"}}}},"responses":{"200":{"description":"Updated record"}}},"delete":{"tags":["Sources"],"summary":"Delete source (requires delete)","responses":{"200":{"description":"Deleted"}}}},"/api/v1/posts":{"get":{"tags":["Posts"],"summary":"List posts","description":"News and updates about your accounts, found by sources and agents. The older /api/v1/signals path works the same.","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Search title, summary"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":500}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"updatedSince","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"sort","in":"query","schema":{"type":"string","default":"updatedAt"}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"]}},{"name":"companyId","in":"query","schema":{"type":"string"},"description":"Filter by companyId (use 'null' for none)"},{"name":"personId","in":"query","schema":{"type":"string"},"description":"Filter by personId (use 'null' for none)"},{"name":"sourceId","in":"query","schema":{"type":"string"},"description":"Filter by sourceId (use 'null' for none)"},{"name":"kind","in":"query","schema":{"type":"string"},"description":"Filter by kind (use 'null' for none)"}],"responses":{"200":{"description":"Page of records","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Post"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}}}},"post":{"tags":["Posts"],"summary":"Create post (single or bulk array ≤200)","description":"With upsert=true an existing record matched by url is updated instead (non-empty fields overwrite, tags/metadata merge). The older /api/v1/signals path works the same.","parameters":[{"name":"upsert","in":"query","schema":{"type":"boolean"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/PostInput"},{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/PostInput"}}]}}}},"responses":{"200":{"description":"Upserted existing / bulk results"},"201":{"description":"Created"}}}},"/api/v1/posts/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Posts"],"summary":"Get post with related records","description":"The older /api/v1/signals path works the same.","responses":{"200":{"description":"Record plus `related`"}}},"patch":{"tags":["Posts"],"summary":"Update post (partial)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostInput"}}}},"responses":{"200":{"description":"Updated record"}}},"delete":{"tags":["Posts"],"summary":"Delete post (requires delete)","responses":{"200":{"description":"Deleted"}}}},"/api/v1/monitor-runs":{"get":{"tags":["Sources"],"summary":"Recent source runs: Read sources now and the daily read","responses":{"200":{"description":"OK"}}},"post":{"tags":["Sources"],"summary":"Read sources now (admins only; the key needs write): read approved, active sources and store what they publish as new public posts","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sourceIds":{"type":"array","maxItems":200,"items":{"type":"string","format":"uuid"},"description":"Only these sources, at most 200 (default: all approved & active)"}}}}}},"responses":{"202":{"description":"Run started; poll GET /api/v1/monitor-runs/{id}"}}}},"/api/v1/monitor-runs/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Sources"],"summary":"Run progress: sources read (monitorsDone of monitorsTotal), posts created (signalsCreated) and errors","responses":{"200":{"description":"OK"}}}},"/api/v1/discovery":{"get":{"tags":["Sources"],"summary":"Source-discovery jobs for a company/person","parameters":[{"name":"entityType","in":"query","required":true,"schema":{"type":"string","enum":["company","person"]}},{"name":"entityId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}},"post":{"tags":["Sources"],"summary":"Find official sources for an approved company/person (runs automatically on approval; this re-runs it)","description":"Reads the entity's website and searches the web. Company-owned primary channels linked from the official site are auto-approved as sources; everything else is created as a pending source for review. LinkedIn company and person pages become sources too (their posts are read through Google); a company page linked from the official site is also saved to the profile's linkedinUrl.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["entityType","entityId"],"properties":{"entityType":{"type":"string","enum":["company","person"]},"entityId":{"type":"string","format":"uuid"}}}}}},"responses":{"202":{"description":"Queued; poll GET /api/v1/discovery/{id}"}}}},"/api/v1/discovery/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Sources"],"summary":"Discovery job: status, counts and a log of every decision","responses":{"200":{"description":"OK"}}}},"/api/v1/articles":{"get":{"tags":["Articles"],"summary":"The articles feed: stories Claude writes from posts about approved companies & people that relate to a saved theme, categorised, with their posts","description":"The older /api/v1/highlights path works the same.","parameters":[{"name":"filter","in":"query","schema":{"type":"string","enum":["all","unread","starred","shared","archived"]},"description":"For the caller. all/unread/shared leave out archived articles; starred includes them; archived lists only them"},{"name":"sort","in":"query","schema":{"type":"string","enum":["latest","top"],"default":"latest"},"description":"latest = when the story broke; top = most important first"},{"name":"category","in":"query","schema":{"type":"string","enum":["product","strategy","mna","funding","partnership","leadership","financials","legal","event","other"]}},{"name":"themeId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Only articles matching this theme"},{"name":"companyId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"personId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"q","in":"query","schema":{"type":"string","maxLength":100},"description":"Words to find in the title, the summary or a company or person name (case-insensitive); counts follow it"},{"name":"limit","in":"query","schema":{"type":"integer","default":30,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"{ data: Article[], counts: { filters, categories, themes, unthemed, unreadInView, archivedUnread } }: unreadInView = unread ones in the chosen filter, archivedUnread = unread ones that sit in Archived"}}},"post":{"tags":["Articles"],"summary":"Write an article by hand from existing posts, new posts or a url (requires write)","description":"The older /api/v1/highlights path works the same.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleInput"}}}},"responses":{"201":{"description":"The article (as in GET /articles/{id}) plus `visibleOnDashboard`. Until it matches a saved theme (matching starts straight away) it has `visibleOnDashboard: false`, `themes: []` and a `note`. It is also `false` (themes kept, with a note) when the article is archived for the caller — older than the archive cutoff, or archived by them — and then listed under filter=archived.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Article"},{"type":"object","properties":{"visibleOnDashboard":{"type":"boolean"},"note":{"type":"string"}}}]}}}},"409":{"description":"A post already belongs to another article"},"422":{"description":"Invalid input"}}}},"/api/v1/articles/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Articles"],"summary":"One article","description":"The older /api/v1/highlights path works the same.","responses":{"200":{"description":"Article","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Article"}}}}}},"patch":{"tags":["Articles"],"summary":"Edit an article (partial) and add or remove its posts (requires write)","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleUpdate"}}}},"responses":{"200":{"description":"The article (as in GET /articles/{id}) plus `visibleOnDashboard`. Until it matches a saved theme (matching starts straight away) it has `visibleOnDashboard: false`, `themes: []` and a `note`. It is also `false` (themes kept, with a note) when the article is archived for the caller — older than the archive cutoff, or archived by them — and then listed under filter=archived.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Article"},{"type":"object","properties":{"visibleOnDashboard":{"type":"boolean"},"note":{"type":"string"}}}]}}}},"404":{"description":"Not found"},"409":{"description":"A post already belongs to another article"},"422":{"description":"Invalid input, or it would be left without a post"}}},"delete":{"tags":["Articles"],"summary":"Delete an article with its read/star state, shares and theme matches; its posts stay (requires delete)","responses":{"200":{"description":"OK"}}}},"/api/v1/articles/{id}/state":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Articles"],"summary":"Mark read/unread, star/unstar and/or archive/unarchive, for the caller","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"read":{"type":"boolean"},"starred":{"type":"boolean"},"archived":{"type":"boolean","description":"false on an old article keeps it in the caller's feed"}}}}}},"responses":{"200":{"description":"OK"}}}},"/api/v1/articles/{id}/share":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Articles"],"summary":"Share with teammates (by email or user id), with an optional note; it lands unread under their 'Shared with me'","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"emails":{"type":"array","items":{"type":"string"}},"userIds":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}}}}}},"responses":{"200":{"description":"{ id, sharedWith }"}}}},"/api/v1/articles/read-all":{"post":{"tags":["Articles"],"summary":"Mark what's in the caller's view read: the main feed, or the tab, category, theme, account and search given","description":"Without `filter` this is the main feed, which leaves archived articles as they are; with `starred` or `shared` it marks only that set's unread articles. Returns the ids it marked, so a client can undo with POST /articles/{id}/state { read: false }.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"filter":{"type":"string","enum":["all","unread","starred","shared","archived"]},"category":{"type":"string","enum":["product","strategy","mna","funding","partnership","leadership","financials","legal","event","other"]},"themeId":{"type":"string","format":"uuid"},"companyId":{"type":"string","format":"uuid"},"personId":{"type":"string","format":"uuid"},"q":{"type":"string","maxLength":100}}}}}},"responses":{"200":{"description":"{ marked, ids }","content":{"application/json":{"schema":{"type":"object","properties":{"marked":{"type":"integer"},"ids":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}}}}},"/api/v1/opportunities":{"get":{"tags":["Opportunities"],"summary":"The leads found in the day's news: sales, partnership and product openings, strongest first, with the articles they came from","description":"Found in the daily chain, right after the day's articles are written: Claude reads the articles it hasn't read yet against the team's guidance (Resources, admins), its themes and the companies on file.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["open","all","new","working","won","dropped"],"default":"open"},"description":"open = new + working"},{"name":"kind","in":"query","schema":{"type":"string","enum":["sales","partnership","product"]}},{"name":"companyId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"{ opportunities: Opportunity[], counts: { new, working, won, dropped, archived, owners: { me, none, all } } } — counts are over everything, not just this page; archived = the ones the caller archived, which the list leaves out","content":{"application/json":{"schema":{"type":"object","properties":{"opportunities":{"type":"array","items":{"$ref":"#/components/schemas/Opportunity"}},"counts":{"type":"object","properties":{"new":{"type":"integer"},"working":{"type":"integer"},"won":{"type":"integer"},"dropped":{"type":"integer"},"archived":{"type":"integer","description":"Archived by the caller (left out of the list and the other counts)"},"owners":{"type":"object","properties":{"me":{"type":"integer"},"none":{"type":"integer"},"all":{"type":"integer"}},"description":"How many are the caller's, unowned, and in all"}}}}}}}},"422":{"description":"Unknown status or kind, or invalid companyId"}}},"post":{"tags":["Opportunities"],"summary":"Take an article on as an opportunity (requires write; no AI call)","description":"One row owned by the caller, status `working`, score 50, titled and explained from the article itself ('who to approach' and the angle say 'Not decided yet'), tied to the article and logged as a `found` event by the caller. The account is the article's company, else its person. Prepare outreach afterwards to research a contact.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["highlightId"],"properties":{"highlightId":{"type":"string","format":"uuid","description":"The article (API resource `highlights`)"},"kind":{"type":"string","enum":["sales","partnership","product"],"default":"sales"}}}}}},"responses":{"201":{"description":"The new opportunity, as in GET /opportunities/{id}","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Opportunity"}}}},"404":{"description":"No such article"},"409":{"description":"The article is already tracked by an open opportunity"},"422":{"description":"Invalid input, or the article isn't about a company or person on file"}}}},"/api/v1/opportunities/find":{"post":{"tags":["Opportunities"],"summary":"Find opportunities now (admins in the app only: an API key can't hold the admin role, so a key gets 403; one AI call, up to 90 seconds)","description":"Reads the articles created in the last `days` days (default 1, at most 40 articles) for openings and merges them into the open ones found in the last two weeks. Counts toward the daily AI budget.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"integer","minimum":1,"maximum":30,"default":1}}}}}},"responses":{"200":{"description":"{ highlights, found, created, updated }: articles read, openings found, new rows, existing rows bumped. An opening found again within two weeks is merged into the existing row.","content":{"application/json":{"schema":{"type":"object","properties":{"highlights":{"type":"integer"},"found":{"type":"integer"},"created":{"type":"integer"},"updated":{"type":"integer"},"error":{"type":"string","description":"Only when the run couldn't start (no admin user)"}}}}}},"403":{"description":"Not an admin"},"422":{"description":"Invalid input"},"429":{"description":"The daily AI budget is used up"}}}},"/api/v1/opportunities/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Opportunities"],"summary":"One opportunity with its articles, any prepared outreach, everything that happened to it (events) and the emails sent (messages)","responses":{"200":{"description":"OpportunityDetail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpportunityDetail"}}}},"404":{"description":"Not found"}}},"patch":{"tags":["Opportunities"],"summary":"Move it along (new → working → won / dropped), hand it to someone, or set the next step and when it's due (requires write)","description":"Setting status to `working` without an ownerId claims it for the caller unless someone already owns it. `ownerId: null` clears the owner. `nextStep` / `dueAt` record what happens next and when (null clears); each change is logged as an event and bumps `lastTouchAt`. A `dueAt` with no next step text becomes the step \"Follow up\". Changing a lead marks it read for the caller.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["new","working","won","dropped"]},"ownerId":{"type":"string","nullable":true,"description":"A user id"},"nextStep":{"type":"string","nullable":true,"maxLength":500},"dueAt":{"type":"string","nullable":true,"description":"ISO date or date-time"},"reason":{"type":"string","enum":["wrong fit","timing","already talking","no contact"],"description":"Why it was dropped: kept on the status event"},"done":{"type":"boolean","description":"With nextStep and dueAt both null: the step was finished (meta.done), not cleared"}}}}}},"responses":{"200":{"description":"Opportunity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Opportunity"}}}},"404":{"description":"Not found"},"422":{"description":"Invalid input, unknown ownerId or dueAt not a date"}}},"delete":{"tags":["Opportunities"],"summary":"Delete an opportunity; the articles it came from stay (requires delete)","responses":{"200":{"description":"OK"},"404":{"description":"Not found"}}}},"/api/v1/opportunities/{id}/outreach":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Opportunities"],"summary":"Research who to contact at the account and draft the pitch (requires write; a web search plus AI, 10-90 seconds)","description":"Names the best person to approach only from what the web sources show: a LinkedIn URL or email is returned only when it appears in the sources, otherwise null. The result is saved on the opportunity (`outreach`) and replaces any earlier one. Counts toward the daily AI budget.","responses":{"200":{"description":"{ outreach }","content":{"application/json":{"schema":{"type":"object","properties":{"outreach":{"$ref":"#/components/schemas/Outreach"}}}}}},"404":{"description":"Opportunity not found, or no one at the account found in a web search"},"429":{"description":"The daily AI budget is used up"},"502":{"description":"The draft couldn't be written just now; try again"},"503":{"description":"The web search didn't run; try again in a minute"}}}},"/api/v1/opportunities/{id}/notes":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Opportunities"],"summary":"Add a note to the opportunity (requires write)","description":"Free text up to 4000 characters. `@Harry` or `@Harry Dewhirst` names a teammate: they are told by email and the note shows on their Today view; their user ids are saved in `event.meta.mentions`. Bumps `lastTouchAt`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","maxLength":4000}}}}}},"responses":{"200":{"description":"{ event }","content":{"application/json":{"schema":{"type":"object","properties":{"event":{"$ref":"#/components/schemas/OpportunityEvent"}}}}}},"404":{"description":"Not found"},"422":{"description":"Empty body"}}}},"/api/v1/opportunities/{id}/addresses":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Opportunities"],"summary":"Email addresses the outreach contact could be reached at, by how sure we are","description":"`found` addresses appeared in the research sources or are on file in Sparhawk. `pattern` addresses are inferred from the pattern this company's other addresses on file follow and are unverified; `patternFrom` is how many addresses the pattern was taken from. Empty until outreach has been prepared and a contact named.","responses":{"200":{"description":"{ contact, suggestions, domain, patternFrom, prior }","content":{"application/json":{"schema":{"type":"object","properties":{"contact":{"type":"string","nullable":true,"description":"The researched contact's name"},"suggestions":{"type":"array","items":{"$ref":"#/components/schemas/AddressSuggestion"}},"domain":{"type":"string","nullable":true,"description":"The company's domain on file"},"patternFrom":{"type":"integer","description":"How many addresses on file the pattern came from (0 = none)"},"prior":{"type":"array","description":"What the team already wrote to these addresses, or to others at the same company","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"scope":{"type":"string","enum":["address","company"],"description":"address: the same address; company: another lead at the same company"},"to":{"type":"string"},"sender":{"type":"string","nullable":true,"description":"Null when the sender's account is gone"},"mine":{"type":"boolean","description":"The caller wrote it"},"at":{"type":"string","format":"date-time"},"subject":{"type":"string"},"draft":{"type":"boolean","description":"Only saved as a Gmail draft so far, not seen sent"},"account":{"type":"string","nullable":true,"description":"The other lead's account name, for company rows"}}}}}}}}},"404":{"description":"Not found"}}}},"/api/v1/opportunities/{id}/send":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Opportunities"],"summary":"Send the outreach email from Sparhawk (requires write; signed-in reps only)","description":"Only a rep signed in to the app can send: an API key gets 403, because nothing is emailed without a person's click (agents use `/draft`). Sent as `<rep's name> via Sparhawk` from the verified sending domain (DIGEST_FROM) with the rep's own address as reply-to, so answers land in their inbox. Logged on the opportunity as a `sent` event and a message; a `new` opportunity moves to `working` (owned by the rep unless someone has it), and a follow-up (\"Follow up with <first name> (<company>)\", four working days later at 12:00 UTC) replaces the next step when it is empty, one the tool wrote, or already due; a hand-written step that is still ahead stays (`followUpDue` is null then). The body is plain text; paragraphs are kept. An address anyone on the team already wrote to in the last 24 hours is refused (409) unless `confirmDuplicate` is true.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["to","subject","body"],"properties":{"to":{"type":"string","format":"email"},"subject":{"type":"string","maxLength":200},"body":{"type":"string","minLength":20,"maxLength":6000},"confirmDuplicate":{"type":"boolean","description":"Write to an address the team already wrote to in the last 24 hours"}}}}}},"responses":{"200":{"description":"{ id, nextStep, followUpDue }","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The message id"},"nextStep":{"type":"string","nullable":true,"description":"The next step this set, null when the existing one stayed"},"followUpDue":{"type":"string","format":"date-time","nullable":true}}}}}},"404":{"description":"Not found"},"409":{"description":"Someone on the team already wrote to this address in the last 24 hours; repeat with confirmDuplicate: true"},"422":{"description":"Invalid address, subject or body"},"503":{"description":"Email sending isn't set up (an admin has to configure it)"}}}},"/api/v1/opportunities/{id}/draft":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Opportunities"],"summary":"Save the outreach email to the key owner's Gmail drafts (requires write)","description":"Writes the email into the key owner's own Gmail Drafts through the Gmail API, so they send it from Gmail. Needs a Google sign-in that carries the gmail.compose scope (else 403: sign in with Google again and allow Gmail access). Logged on the opportunity as a `drafted` event and a message; a `new` opportunity moves to `working`, and when there is no next step it becomes \"Send the draft to <first name>\", due the next working day. The hourly Gmail sync (and `POST /api/v1/gmail/sync`) then reads the draft's thread and marks the message `deliveredAt` (logging a `sent` event and setting a follow-up), `repliedAt` (a `replied` event, next step \"Reply to <first name>\" due today), or a `bounced` event when the thread holds a delivery-failure notice (no `repliedAt`; an automatic follow-up becomes \"Find another address for <name>\"). Automatic replies such as out-of-office are ignored. The body is plain text. An address anyone on the team already wrote to in the last 24 hours is refused (409) unless `confirmDuplicate` is true.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["to","subject","body"],"properties":{"to":{"type":"string","format":"email"},"subject":{"type":"string","maxLength":200},"body":{"type":"string","minLength":20,"maxLength":6000},"confirmDuplicate":{"type":"boolean","description":"Write to an address the team already wrote to in the last 24 hours"}}}}}},"responses":{"200":{"description":"{ id, draftId, threadId, openUrl, nextStep, followUpDue }","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The message id"},"draftId":{"type":"string","description":"The Gmail draft id"},"threadId":{"type":"string","nullable":true,"description":"The Gmail thread the sync watches"},"openUrl":{"type":"string","format":"uri","description":"Opens the draft in Gmail"},"nextStep":{"type":"string","nullable":true,"description":"The next step this set, null when one already existed"},"followUpDue":{"type":"string","format":"date-time","nullable":true,"description":"When that next step is due"}}}}}},"403":{"description":"Gmail isn't connected, or Google no longer accepts the saved sign-in: the key owner must sign in with Google again and allow Gmail access"},"404":{"description":"Not found"},"409":{"description":"Someone on the team already wrote to this address in the last 24 hours; repeat with confirmDuplicate: true"},"422":{"description":"Invalid address, subject or body"},"502":{"description":"Gmail refused the draft"}}}},"/api/v1/gmail/sync":{"get":{"tags":["Gmail"],"summary":"Whether the key owner's Google sign-in carries the Gmail scopes and Google still accepts it","responses":{"200":{"description":"{ connected, reason }","content":{"application/json":{"schema":{"type":"object","properties":{"connected":{"type":"boolean"},"reason":{"type":"string","nullable":true,"description":"Why not connected, when a sign-in exists but isn't usable (a missing Gmail permission, a revoked token)"}}}}}}}},"post":{"tags":["Gmail"],"summary":"Sync the key owner's Gmail drafts now","description":"Reads the threads started by the key owner's drafts from the last 30 days (at most 50) and records what was sent (`deliveredAt`, a `sent` event, a follow-up four working days out replacing an empty, automatic or already-due next step) and what got a reply (`repliedAt`, `replySnippet`, a `replied` event; the next step becomes \"Reply to <first name>\" due today when it was empty or a follow-up the tool wrote). Delivery-failure notices are logged as `bounced` events instead (no `repliedAt`), and automatic replies (out of office) are ignored. The hourly cron does the same for everyone; this skips the ten-minute throttle. Returns zeros when Gmail isn't connected.","responses":{"200":{"description":"{ checked, delivered, replied, bounced, skipped }","content":{"application/json":{"schema":{"type":"object","properties":{"checked":{"type":"integer"},"delivered":{"type":"integer"},"replied":{"type":"integer"},"bounced":{"type":"integer"},"skipped":{"type":"boolean"}}}}}}}}},"/api/v1/opportunities/{id}/replied":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Opportunities"],"summary":"Record that the contact replied (requires write)","description":"Marks the latest email sent (or the one named by `messageId`) as replied, with an optional snippet, and logs a `replied` event; the next step becomes \"Reply to <first name>\" due today when it was empty or a follow-up the tool wrote. Works even when nothing was sent from Sparhawk.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"messageId":{"type":"string","format":"uuid"},"snippet":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"{ ok: true }"},"404":{"description":"Not found"}}}},"/api/v1/opportunities/{id}/person":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Opportunities"],"summary":"Add the researched outreach contact as a person record (requires write)","description":"Creates (or upserts) a person with the contact's name, title, LinkedIn and email, linked to the company, through the normal create flow (so it lands in review like any other add), and links the opportunity to them.","responses":{"200":{"description":"{ person }","content":{"application/json":{"schema":{"type":"object","properties":{"person":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"created":{"type":"boolean","description":"false when the person already existed"}}}}}}}},"404":{"description":"Not found"},"422":{"description":"No contact yet: prepare outreach first"}}}},"/api/v1/opportunities/account/{companyId}":{"parameters":[{"name":"companyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Opportunities"],"summary":"The account view: a company's open and closed opportunities, its people with last contact, and recent news","responses":{"200":{"description":"{ open, closed, contacts, news }","content":{"application/json":{"schema":{"type":"object","properties":{"open":{"type":"array","items":{"$ref":"#/components/schemas/Opportunity"},"description":"new + working, most recently seen first"},"closed":{"type":"array","items":{"$ref":"#/components/schemas/Opportunity"},"description":"won + dropped"},"contacts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"title":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"lastContact":{"type":"string","format":"date-time","nullable":true,"description":"When an outreach email was last sent to this address from Sparhawk"}}}},"news":{"type":"array","items":{"$ref":"#/components/schemas/Article"},"description":"The company's latest 15 articles"}}}}}},"404":{"description":"Company not found"}}}},"/api/v1/today":{"get":{"tags":["Opportunities"],"summary":"What the caller works on today: replies, unsent drafts, due or overdue, new leads, what teammates moved, stalled and unscheduled","description":"All of it is the caller's own (the key owner's) unless it says otherwise. `replies`: emails that got a reply, newest first. `draftsWaiting`: Gmail drafts written for leads and still not sent. `waitingOnReply`: how many leads have a send nobody has answered yet, follow-up not due. `due`: open opportunities whose `dueAt` is today or earlier, soonest first. `fresh`: new leads nobody has claimed (or that are the caller's) that the caller hasn't read, strongest first, at most 8 of `freshTotal`. `moved`: leads the caller owns or has worked that teammates changed since the caller last looked, with those events, at most 5 (`movedMore` is how many more). `stalled`: working ones untouched for 10 days and not already due. `noNextStep`: working ones with nothing scheduled (the most neglected few in `rows`, leaving out the stalled ones). `team.overdueByOwner`: how many leads each teammate has overdue.","responses":{"200":{"description":"{ replies, draftsWaiting, waitingOnReply, due, fresh, freshTotal, moved, movedMore, stalled, noNextStep, team }","content":{"application/json":{"schema":{"type":"object","properties":{"replies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The message id"},"to":{"type":"string"},"subject":{"type":"string"},"replySnippet":{"type":"string","nullable":true},"repliedAt":{"type":"string","format":"date-time"},"gmailThreadId":{"type":"string","nullable":true},"fromMe":{"type":"boolean","description":"The caller sent it, so the thread is in their own Gmail"},"opportunity":{"$ref":"#/components/schemas/Opportunity"}}}},"draftsWaiting":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The message id"},"to":{"type":"string"},"subject":{"type":"string"},"sentAt":{"type":"string","format":"date-time","description":"When the draft was saved"},"gmailThreadId":{"type":"string","nullable":true},"opportunity":{"$ref":"#/components/schemas/Opportunity"}}}},"waitingOnReply":{"type":"integer"},"due":{"type":"array","items":{"$ref":"#/components/schemas/Opportunity"}},"fresh":{"type":"array","items":{"$ref":"#/components/schemas/Opportunity"}},"freshTotal":{"type":"integer"},"moved":{"type":"array","items":{"type":"object","properties":{"opportunity":{"$ref":"#/components/schemas/Opportunity"},"events":{"type":"array","items":{"$ref":"#/components/schemas/OpportunityEvent"}}}}},"movedMore":{"type":"integer"},"stalled":{"type":"array","items":{"$ref":"#/components/schemas/Opportunity"}},"noNextStep":{"type":"object","properties":{"total":{"type":"integer"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/Opportunity"}}}},"team":{"type":"object","properties":{"overdueByOwner":{"type":"array","items":{"type":"object","properties":{"ownerId":{"type":"string"},"name":{"type":"string"},"count":{"type":"integer"}}}}}}}}}}}}}},"/api/v1/suggestions":{"get":{"tags":["Discover"],"summary":"Discover suggestions: companies, people and themes the news keeps mentioning that the team doesn't track, strongest first","description":"Found every day in the daily chain, after the opportunities, from the last 7 days of articles, read against the companies, people and themes on file. Accepting one creates the real record through the normal review flow.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["all","new","accepted","dismissed"],"default":"new"}},{"name":"kind","in":"query","schema":{"type":"string","enum":["company","person","theme"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"{ suggestions: Suggestion[], counts: { company, person, theme } } — counts are the new ones by kind over everything, not just this page","content":{"application/json":{"schema":{"type":"object","properties":{"suggestions":{"type":"array","items":{"$ref":"#/components/schemas/Suggestion"}},"counts":{"type":"object","properties":{"company":{"type":"integer"},"person":{"type":"integer"},"theme":{"type":"integer"}}}}}}}},"422":{"description":"Unknown status or kind"}}}},"/api/v1/suggestions/scan":{"post":{"tags":["Discover"],"summary":"Scan for suggestions now (admins in the app only: an API key can't hold the admin role, so a key gets 403; one AI call, up to 90 seconds)","description":"Reads the articles created in the last `days` days (default 7, at most 60 articles) for untracked companies, people and themes and merges them by name into what's already suggested. Counts toward the daily AI budget.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"integer","minimum":1,"maximum":30,"default":7}}}}}},"responses":{"200":{"description":"{ highlights, companies, people, themes, created, updated }: articles read, untracked names found by kind, new rows, existing rows bumped.","content":{"application/json":{"schema":{"type":"object","properties":{"highlights":{"type":"integer"},"companies":{"type":"integer"},"people":{"type":"integer"},"themes":{"type":"integer"},"created":{"type":"integer"},"updated":{"type":"integer"},"error":{"type":"string","description":"Only when the run couldn't start (no admin user)"}}}}}},"403":{"description":"Not an admin"},"422":{"description":"Invalid input"},"429":{"description":"The daily AI budget is used up"}}}},"/api/v1/suggestions/{id}/accept":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Discover"],"summary":"Add it (requires write): a company or person goes through review like any other add, a theme runs straight away","responses":{"200":{"description":"{ made: { type, id, name, existing? } } — the record it became; `existing` is true when it was already tracked (the suggestion is linked to it and nothing on the record changes)","content":{"application/json":{"schema":{"type":"object","properties":{"made":{"type":"object","properties":{"type":{"type":"string","enum":["company","person","theme"]},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"existing":{"type":"boolean"}}}}}}}},"404":{"description":"Not found"}}}},"/api/v1/suggestions/{id}/dismiss":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Discover"],"summary":"Not interested (requires write); it stays dismissed on later runs","responses":{"200":{"description":"OK"},"404":{"description":"Not found"}}}},"/api/v1/suggestions/{id}/reopen":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Discover"],"summary":"Back to the queue after a dismiss (requires write)","responses":{"200":{"description":"OK"},"404":{"description":"Not found"},"422":{"description":"Already added"}}}},"/api/v1/search":{"get":{"tags":["Themes"],"summary":"Natural-language search across approved companies & people and all posts (semantic + keyword, AI-reranked with reasons)","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"description":"e.g. 'humanoid robotics companies raising capital'"},{"name":"types","in":"query","schema":{"type":"string"},"description":"Comma-separated subset of company,person,signal (`signal` is a post)"}],"responses":{"200":{"description":"Scored matches, best first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"entityType":{"type":"string","enum":["company","person","signal"],"description":"`signal` is a post"},"entityId":{"type":"string","format":"uuid"},"score":{"type":"integer","minimum":0,"maximum":100},"reason":{"type":"string","nullable":true},"title":{"type":"string"},"subtitle":{"type":"string","nullable":true},"href":{"type":"string"},"url":{"type":"string","nullable":true}}}}}}}}}}}},"/api/v1/quick-search":{"get":{"tags":["Themes"],"summary":"The sidebar search: companies, people, opportunities, articles, sources, themes and documents that contain every keyword (no AI)","description":"Every word of `q` has to appear in the record's text, ignoring case: plain keyword matching, so it is fast enough to run as you type. Up to 5 hits of each kind. Companies, people and sources: approved only, plus pending ones when the key owner can review (rejected never). Articles: only ones anyone may see. A blank query returns an empty list. `href` is the page in the app.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","maxLength":200},"description":"e.g. 'figure robotics'"}],"responses":{"200":{"description":"{ data: QuickHit[] }","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["company","person","opportunity","article","source","theme","document"]},"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"subtitle":{"type":"string","nullable":true},"href":{"type":"string","description":"Where the app shows it"}}}}}}}}}}}},"/api/v1/themes":{"get":{"tags":["Themes"],"summary":"Saved themes with match counts","responses":{"200":{"description":"OK"}}},"post":{"tags":["Themes"],"summary":"Save a theme (natural-language description) and run it (requires write)","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string"},"name":{"type":"string"}}}}}},"responses":{"201":{"description":"{ theme, matches }"}}}},"/api/v1/themes/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Themes"],"summary":"Theme with its latest matches","responses":{"200":{"description":"{ theme, matches }"}}},"patch":{"tags":["Themes"],"summary":"Rename or change the description (changing it re-runs the theme)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"name":{"type":"string"}}}}}},"responses":{"200":{"description":"{ theme, matches }"}}},"delete":{"tags":["Themes"],"summary":"Delete a theme (requires write)","responses":{"200":{"description":"OK"}}}},"/api/v1/themes/{id}/run":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Themes"],"summary":"Re-run a theme against the latest data","responses":{"200":{"description":"{ theme, matches }"}}}},"/api/v1/find":{"get":{"tags":["Lookup"],"summary":"Already in Sparhawk? Companies & people matching every keyword (name, website, industry, what they do, title, company). No AI, so it's cheap","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"description":"Keywords, a name or a website (a URL matches on its domain)"}],"responses":{"200":{"description":"Up to 8 companies, then up to 8 people; approved ones and name matches first, rejected ones left out","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FoundEntity"}}}}}}},"422":{"description":"q is missing"}}}},"/api/v1/lookup":{"post":{"tags":["Lookup"],"summary":"Look a company or person up on the web and get the fields to save (AI, requires write). Nothing is saved","description":"A web search plus AI, 10-60 seconds; counts toward the daily AI budget. Says straight away (without AI) when a company with that name or website is already here (with a website hint, a same-name company only counts if its website is unknown or the same).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","query"],"properties":{"kind":{"type":"string","enum":["companies","people"]},"query":{"type":"string","description":"Company: name or website; person: full name or LinkedIn URL"},"hint":{"type":"string","nullable":true,"description":"Company: a website that's probably theirs (it steers the search); person: their company"},"companyId":{"type":"string","format":"uuid","nullable":true,"description":"People: the company they're being added from, linked unless the sources name another employer"}}}}}},"responses":{"200":{"description":"LookupResult","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LookupResult"}}}},"404":{"description":"Nothing matching found"},"422":{"description":"Invalid input"},"429":{"description":"The daily AI budget is used up"},"503":{"description":"Web search is unavailable; try again shortly"}}}},"/api/v1/suggest":{"post":{"tags":["Lookup"],"summary":"Up to 8 companies or people that match some keywords, from a web search and AI (requires write). Nothing is saved","description":"About 15 seconds; counts toward the daily AI budget. Look a suggestion up with POST /lookup (its name as query, its hint as hint).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","keywords"],"properties":{"kind":{"type":"string","enum":["companies","people"]},"keywords":{"type":"string","description":"e.g. 'humanoid robotics startups in Europe' (at least 3 characters)"}}}}}},"responses":{"200":{"description":"Suggestions, most relevant first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LookupSuggestion"}}}}}}},"422":{"description":"Invalid input"},"429":{"description":"The daily AI budget is used up"}}}},"/api/v1/me":{"get":{"tags":["Meta"],"summary":"Who am I — role and effective permissions of this key","responses":{"200":{"description":"OK"}}}},"/api/v1/review":{"get":{"tags":["Review"],"summary":"Pending suggestions awaiting approval, with document mentions and possible duplicates","parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["company","person","monitor"]},"description":"`monitor` is a source"}],"responses":{"200":{"description":"OK"}}}},"/api/v1/review/{type}/{id}":{"post":{"tags":["Review"],"summary":"Approve, reject, reopen or merge a suggestion (requires review)","parameters":[{"name":"type","in":"path","required":true,"schema":{"type":"string","enum":["company","person","monitor"]},"description":"`monitor` is a source"},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["approve","reject","reopen","merge"]},"targetId":{"type":"string","format":"uuid","description":"Required for merge: the existing record to merge into"}}}}}},"responses":{"200":{"description":"OK"}}}},"/api/v1/documents":{"get":{"tags":["Documents"],"summary":"List documents","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["queued","processing","extracted","failed"]}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}},"422":{"description":"Invalid status"}}},"post":{"tags":["Documents"],"summary":"Add a document (Google/web link as JSON, or multipart file upload ≤4.5MB). Extraction runs async.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string"},"title":{"type":"string"}}}},"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"title":{"type":"string"}}}}}},"responses":{"202":{"description":"Accepted; poll GET /api/v1/documents/{id}"},"415":{"description":"Unsupported file type"},"422":{"description":"Missing file or invalid URL"}}}},"/api/v1/documents/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Documents"],"summary":"Document with status, summary, text and mentions","responses":{"200":{"description":"OK"}}},"delete":{"tags":["Documents"],"summary":"Delete document (requires delete)","responses":{"200":{"description":"OK"}}}},"/api/v1/documents/{id}/extract":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Documents"],"summary":"Re-run AI extraction","responses":{"202":{"description":"Queued"}}}},"/api/v1/documents/{id}/file":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Documents"],"summary":"Download the original uploaded file","responses":{"200":{"description":"File bytes"}}}},"/api/v1/deals":{"get":{"tags":["Pipeline"],"summary":"The pipeline's deals, most recently active first","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["open","won","lost","all"],"default":"open"}},{"name":"owner","in":"query","schema":{"type":"string","default":"all"},"description":"`me`, `all` or a user id"},{"name":"stage","in":"query","schema":{"type":"string"}},{"name":"product","in":"query","schema":{"type":"string"},"description":"A product key (or label)"},{"name":"q","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":1000}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"deals":{"type":"array","items":{"$ref":"#/components/schemas/Deal"}}}}}}}}},"post":{"tags":["Pipeline"],"summary":"Move an opportunity into the pipeline, or start a deal (requires write)","description":"`{ opportunityId }` returns the existing deal when there already is one (`created: false`); with `title` it names the new deal (a company can have several open deals, e.g. `NVIDIA — Cosmos`); with `addToDealId` the opportunity is added to that open deal of the same company instead (its contacts and threads join it). Otherwise `{ title, account | companyId, value?, product?, contactEmail?, contactName? }`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"opportunityId":{"type":"string","format":"uuid"},"addToDealId":{"type":"string","format":"uuid"},"title":{"type":"string"},"account":{"type":"string"},"companyId":{"type":"string","format":"uuid"},"value":{"type":"integer"},"product":{"type":"string","nullable":true,"description":"A product key from Settings → Pipeline (GET /api/v1/pipeline/settings → products; admins can add, rename, archive). Defaults: world_model, humanoid, av_sim, mapping, eval_sets, custom, other. A product's label is accepted too; archived products can't be newly assigned.","example":"world_model"},"contactEmail":{"type":"string","format":"email"},"contactName":{"type":"string"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"created":{"type":"boolean"}}}}}},"404":{"description":"Opportunity not found"},"422":{"description":"Invalid input"}}}},"/api/v1/deals/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Pipeline"],"summary":"One deal with contacts, Gmail threads, the activity timeline (emails, meetings, notes, stage changes) and its source opportunity","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deal"}}}},"404":{"description":"Not found"}}},"patch":{"tags":["Pipeline"],"summary":"Change a deal (requires write; the owner or an admin)","description":"Moving to the Lost stage needs `lostReason`. Won/Lost close the deal and its opportunity. A later `expectedCloseAt` is recorded as a slip. `companyId` links the deal to a company (null unlinks) and records \"Linked to <company>\" on the timeline; the app then syncs the owner's Gmail and Calendar for the company's domain.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"companyId":{"type":"string","format":"uuid","nullable":true},"stage":{"type":"string"},"ownerId":{"type":"string","nullable":true},"value":{"type":"integer","nullable":true},"probability":{"type":"integer","nullable":true,"minimum":0,"maximum":100},"product":{"type":"string","nullable":true,"description":"A product key from Settings → Pipeline (GET /api/v1/pipeline/settings → products; admins can add, rename, archive). Defaults: world_model, humanoid, av_sim, mapping, eval_sets, custom, other. A product's label is accepted too; archived products can't be newly assigned.","example":"world_model"},"expectedCloseAt":{"type":"string","nullable":true},"nextStep":{"type":"string","nullable":true},"dueAt":{"type":"string","nullable":true},"lostReason":{"type":"string","enum":["no budget","no data need","went with a competitor","building in-house","timing","went dark","other"]},"forecastCategory":{"type":"string","enum":["commit","best_case","pipeline","omit"]}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deal"}}}},"403":{"description":"Not the owner or an admin"},"404":{"description":"Not found"},"422":{"description":"Invalid input"}}},"delete":{"tags":["Pipeline"],"summary":"Delete a deal (requires delete; owner or admin). The opportunity stays.","responses":{"200":{"description":"OK"}}}},"/api/v1/deals/{id}/summary":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Pipeline"],"summary":"The deal's AI executive summary","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"type":"object","nullable":true,"properties":{"text":{"type":"string"},"inputHash":{"type":"string"},"at":{"type":"string","format":"date-time"},"byId":{"type":"string","nullable":true}}}}}}}}}},"post":{"tags":["Pipeline"],"summary":"Write the executive summary now (requires write; one AI call)","description":"3-5 sentences on status, the opportunity and engagement, from the deal's emails (subjects, snippets, intents), meetings, notes, stage history, contacts, value, close date and recent company articles. The app also rewrites it on page view when the deal changed materially, at most hourly.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"inputHash":{"type":"string"},"at":{"type":"string"},"byId":{"type":"string","nullable":true}}}}}},"422":{"description":"Invalid input"},"429":{"description":"The daily AI budget is used up"}}}},"/api/v1/deals/{id}/notes":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Pipeline"],"summary":"Add a note to the deal's timeline (requires write)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","maxLength":4000}}}}}},"responses":{"200":{"description":"{ activity }"}}}},"/api/v1/deals/{id}/contacts":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Pipeline"],"summary":"Add a contact from their side (owner or admin)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"name":{"type":"string"},"role":{"type":"string"},"primary":{"type":"boolean"}}}}}},"responses":{"200":{"description":"OK"}}},"delete":{"tags":["Pipeline"],"summary":"Remove a contact","parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/v1/deals/{id}/suggestion":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Pipeline"],"summary":"Accept (moves the stage) or dismiss the stage suggestion","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"accept":{"type":"boolean"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deal"}}}}}}},"/api/v1/deals/{id}/follow-up":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Pipeline"],"summary":"Draft a follow-up email (one AI call). Drafting only: sending needs a person in the app","description":"Written from the deal, its latest Gmail thread in the key owner's mailbox, the 375ai knowledge base, the learned outreach lessons and the owner's outreach preferences.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"intent":{"type":"string","enum":["nudge","recap","availability","next_steps","custom"]},"note":{"type":"string","maxLength":500},"to":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string","nullable":true},"cc":{"type":"array","items":{"type":"string"}},"subject":{"type":"string"},"body":{"type":"string"},"threadId":{"type":"string","nullable":true},"inThread":{"type":"boolean"},"slots":{"type":"array","items":{"type":"object"}}}}}}},"422":{"description":"Invalid input"},"429":{"description":"The daily AI budget is used up"},"502":{"description":"Couldn't draft just now"}}}},"/api/v1/deals/{id}/availability":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Pipeline"],"summary":"Free slots on the key owner's calendar (weekdays 9-17 in its time zone, next ~10 working days)","parameters":[{"name":"durationMin","in":"query","schema":{"type":"integer","default":30}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"slots":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"},"label":{"type":"string"}}}}}}}}},"403":{"description":"Google needs reconnecting"}}}},"/api/v1/deals/{id}/brief":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Pipeline"],"summary":"Write the AI deal brief: summary, risks, who's involved, next best action (one AI call; saved on the deal)","responses":{"200":{"description":"The brief"},"422":{"description":"Invalid input"},"429":{"description":"The daily AI budget is used up"}}}},"/api/v1/deals/{id}/insights":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Pipeline"],"summary":"What AI read off the deal's email: suggested contacts, referrals, the next step, meeting prep, and contacts added from To/Cc","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"insights":{"type":"object","properties":{"contacts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"role":{"type":"string","nullable":true,"enum":["decision_maker","champion","technical_evaluator","procurement","legal","referral","other"]},"quote":{"type":"string"},"source":{"type":"object","properties":{"gmailId":{"type":"string"},"threadId":{"type":"string","nullable":true},"mailbox":{"type":"string","nullable":true},"at":{"type":"string","format":"date-time"},"from":{"type":"string","nullable":true}}},"status":{"type":"string","enum":["new","accepted","dismissed"]}}}},"referrals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"company":{"type":"string"},"domain":{"type":"string","nullable":true},"person":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"quote":{"type":"string"},"source":{"type":"object","properties":{"gmailId":{"type":"string"},"threadId":{"type":"string","nullable":true},"mailbox":{"type":"string","nullable":true},"at":{"type":"string","format":"date-time"},"from":{"type":"string","nullable":true}}},"status":{"type":"string","enum":["new","accepted","dismissed"]},"suggestionIds":{"type":"array","items":{"type":"string"}},"dealId":{"type":"string","nullable":true}}}},"nextStep":{"type":"object","nullable":true,"properties":{"text":{"type":"string"},"objections":{"type":"array","items":{"type":"string"}},"quote":{"type":"string"},"source":{"type":"object","properties":{"gmailId":{"type":"string"},"threadId":{"type":"string","nullable":true},"mailbox":{"type":"string","nullable":true},"at":{"type":"string","format":"date-time"},"from":{"type":"string","nullable":true}}},"status":{"type":"string","enum":["new","accepted","dismissed"]}}},"prep":{"type":"object","nullable":true,"description":"MeetingPrep: meetingKey, meetingAt, title, attendees, context, lastThread, accountNews, openQuestions, goals, at"},"autoContacts":{"type":"array","items":{"type":"string"},"description":"Emails the sync added as deal contacts from the To/Cc of their email"},"scannedAt":{"type":"string","nullable":true}}}}}}}}}},"post":{"tags":["Pipeline"],"summary":"Accept or dismiss a suggested contact, a referral or the next step read from email (owner or admin)","description":"Contact accept adds a deal contact (an email is needed when the thread didn't give one); `createPerson` also creates or links a person record in People (a new one is pending review). Referral `start_deal` starts a new deal for the referred company with the quote as a note. Next step accept sets the deal's next step.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["kind","id","action"],"properties":{"kind":{"type":"string","enum":["contact"]},"id":{"type":"string","format":"uuid"},"action":{"type":"string","enum":["accept","dismiss"]},"email":{"type":"string","format":"email"},"createPerson":{"type":"boolean","default":false}}},{"type":"object","required":["kind","id","action"],"properties":{"kind":{"type":"string","enum":["referral"]},"id":{"type":"string","format":"uuid"},"action":{"type":"string","enum":["start_deal","dismiss"]}}},{"type":"object","required":["kind","action"],"properties":{"kind":{"type":"string","enum":["next_step"]},"action":{"type":"string","enum":["accept","dismiss"]}}}]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"personId":{"type":"string","nullable":true},"personPending":{"type":"boolean"},"dealId":{"type":"string","nullable":true}}}}}},"404":{"description":"Suggestion not found"}}}},"/api/v1/deals/{id}/insights/scan":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Pipeline"],"summary":"Read the deal's latest inbound emails not read before (up to 8, in the key owner's mailbox) with one AI call; nothing new, no call","description":"Each message is read once: its reply intent is kept on the activity (`meta.intent`), and people, referrals and the next step it mentions become suggestions. The hourly sync does this for newly synced messages.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"integer"},"contacts":{"type":"integer"},"referrals":{"type":"integer"},"nextStep":{"type":"boolean"},"skipped":{"type":"string"}}}}}},"422":{"description":"Invalid input"},"429":{"description":"The daily AI budget is used up"}}}},"/api/v1/deals/{id}/prep":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Pipeline"],"summary":"Write prep for the deal's next meeting (today or later): attendees, account news, last thread, open questions, goals (one AI call; saved as insights.prep)","responses":{"200":{"description":"MeetingPrep"},"422":{"description":"Invalid input"},"429":{"description":"The daily AI budget is used up"}}}},"/api/v1/deals/{id}/articles":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Pipeline"],"summary":"Articles about the deal's company (posts about it, or its name) and on the themes recently relevant to it, newest first","parameters":[{"name":"filter","in":"query","schema":{"type":"string","enum":["all","company","themes"],"default":"all"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"summary":{"type":"string"},"category":{"type":"string"},"happenedAt":{"type":"string","format":"date-time"},"match":{"type":"string","enum":["company","theme","both"]},"themes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"related":{"type":"boolean"}}}},"read":{"type":"boolean"},"starred":{"type":"boolean"}}}},"themes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}}},"companyCount":{"type":"integer"},"themeCount":{"type":"integer"}}}}}}}}},"/api/v1/deals/sync":{"post":{"tags":["Pipeline"],"summary":"Bring the key owner's open deals up to date from their Gmail and Calendar now","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"deals":{"type":"integer"},"threadsLinked":{"type":"integer"},"emails":{"type":"integer"},"meetings":{"type":"integer"},"suggestions":{"type":"integer"},"skipped":{"type":"boolean"},"error":{"type":"string"}}}}}}}}},"/api/v1/pipeline":{"get":{"tags":["Pipeline"],"summary":"Pipeline metrics, forecast and the week's change","parameters":[{"name":"owner","in":"query","schema":{"type":"string","enum":["me","all"],"default":"all"}},{"name":"days","in":"query","schema":{"type":"integer","default":7,"maximum":90}}],"responses":{"200":{"description":"{ settings, metrics: { open, byStage, forecast, quarter, winRate, cycleDays, conversion, lostReasons, byOwner }, change: { added, advanced, movedBack, slipped, won, lost, totals }, needsYou }"}}}},"/api/v1/pipeline/settings":{"get":{"tags":["Pipeline"],"summary":"Stages (key, label, probability, kind, staleDays), the stale threshold, auto-create, and products ({ key, label, archived, color }; deals store the key)","responses":{"200":{"description":"PipelineSettings"}}},"put":{"tags":["Pipeline"],"summary":"Replace the stages, rules and products (admins only); a stage still holding deals, or a product deals still use, can't be removed (409; archive the product instead)","responses":{"200":{"description":"PipelineSettings"},"403":{"description":"Not an admin"},"409":{"description":"Stage or product in use"}}}}}}