REST API
Search for companies with a single HTTP request. Base URL: https://trycanonical.ai
Authentication
All API requests require an API key passed in the x-api-key header. Create keys from your Settings.
curl https://trycanonical.ai/api/v1/search?q=fintech \ -H "x-api-key: YOUR_API_KEY"
Keys start with sk_. Keep them secret — they grant full API access to your account.
Search Companies
Supports two modes. Exactly one must be used per request — passing both, or neither, returns 422.
- NL mode (GET or POST): pass ?q= as a query parameter. The server runs the NL interpreter, then searches. Response includes interpreted_criteria + unmapped + warnings.
- Structured mode (POST, JSON body only): pass a JSON body with description and/or filters — the interpreter is skipped entirely. Response includes warnings but no interpreted_criteria. Note: structured mode is batch JSON only — sending Accept: text/event-stream with a structured body returns 406.
NL Mode — Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| q | string | required | Natural language search query |
| top_k | integer | 20 | Number of results (1–1000) |
| intent | string | inferred | Optional ranking profile applied after relevance filtering. Pass it when your client (e.g. an agent reading the user's goal from chat or memory) already knows the right slug; omit it to let the server infer intent from the query text. Allowed slugs: sales_prospecting, sales_timing, sales_expansion, competitive_tracking, emerging_competitor_scan, talent_source, recruiter_employer_vet, jobseeker_stability, jobseeker_growth. |
| include_partials | boolean | false | When true, also returns LLM-evaluated "partial" matches (close but not strong). Off by default because partials aren't billed. Behavior change: previously partials were included; opt back in here. |
| exclude_company_domain | string (repeatable) | — | Repeat to exclude multiple companies by domain (e.g. ?exclude_company_domain=stripe.com&exclude_company_domain=square.com). Stacks with any exclusions named in the NL query. Unresolvable domains appear in warnings. |
Breaking changes in the latest release: id has been removed from the result rows — domain is now the public company identifier. The include_partials parameter defaults to false (previously partials were always returned). If your integration used id as a key, switch to domain.
Structured Mode — Request Body
POST a JSON body instead of ?q= to skip NL interpretation. The interpreter is not called — filters are applied exactly as supplied.
| Field | Type | Description |
|---|---|---|
| description | string | null | Free-text semantic core. Drives ColBERT retrieval + LLM verification. Omit for filter-only searches. |
| filters | object | null |
Structured filter object. All keys are optional. Supported keys:
location (object) — nested location filter with keys cities, states, countries (each a list[string]). employee_size (string) — headcount bucket (e.g. "51-100"). founding_year_min / founding_year_max (integer) — inclusive bounds on the company's founding year (e.g. {"founding_year_min": 2015, "founding_year_max": 2020}). For an exact year, set both to the same value. founding_year_exclude_min / founding_year_exclude_max (integer) — inclusive bounds of a contiguous range to exclude. Both must be set together; for a single-year exclusion, set both to the same year. NULL-inclusive (companies with unknown founding year survive exclusion). funding_series (list[string]) — stage slugs (e.g. ["series_a", "series_b"]). funding_min_usd (integer) — minimum total funding raised. funding_post_money_min_usd (integer) — minimum post-money valuation. funded_after (string) — ISO date; only companies funded on or after this date. funding_investor (list[string]) — investor firm name variants. people (object) — nested people filter with keys: has_repeat_founder (boolean), has_technical_cofounder (boolean), requires_role_types (list[string]), founder_prior_categories (list[string]), founder_prior_companies (list[string]). exclude_company_domains (list[string]) — domains to exclude from results. exclude (object) — nested exclusion filter with keys: description (string, semantic exclusion), location (object with cities/states/countries), funding_series (list[string]), funding_investor (list[string]). |
| intent | string | null | Optional ranking profile slug (same values as NL mode). Omit to skip re-ranking. |
| top_k | integer | Number of results (1–1000, default 20). |
| include_partials | boolean | Include partial matches. Default false. |
Validation rules: exactly one of ?q= (NL mode) or a JSON body (structured mode) must be present per request — both → 422, neither → 422. Streaming (Accept: text/event-stream) is supported for NL mode only; sending it with a structured body returns 406.
Structured Mode — Example
curl -X POST "https://trycanonical.ai/api/v1/search" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "B2B fintech", "filters": { "location": {"cities": ["New York City"], "countries": ["United States"]}, "employee_size": "51-100", "founding_year_min": 2018, "founding_year_max": 2022, "funding_series": ["series_a", "series_b"], "funding_min_usd": 100000000, "people": {"has_technical_cofounder": true, "founder_prior_categories": ["faang"]}, "exclude_company_domains": ["stripe.com"], "exclude": {"description": "consulting", "location": {"countries": ["India"]}} }, "intent": "sales_prospecting", "top_k": 20 }' # Response — same shape as NL mode, but no interpreted_criteria field { "results": [ /* ... */ ], "count": 5, "credits_used": 5, "credits_remaining": 995, "warnings": [] }
Intent slugs
Each slug controls how the verified result set is reordered. The ranker uses precomputed signals (hiring direction, momentum, retention, talent magnetism, brand reach) so picking an intent doesn't run a second search — it just changes the sort. The same slugs also reorder similar-to results: pass intent to /api/v1/similar, or use it on a /api/v1/search "similar to X" query.
| Slug | When to pick it | What the ranker optimizes for |
|---|---|---|
| sales_prospecting | Top-of-funnel outbound list with no specific buying-window constraint. Generic ICP fit. | Companies actively growing, hiring, with brand reach. |
| sales_timing | "Who is buying now?" — near-term buying window. | Recent hiring acceleration paired with healthy growth. |
| sales_expansion | Existing customers ripe for upsell, cross-sell, or deepening. | Stable, steadily-growing companies with strong retention. |
| competitive_tracking | Monitoring named competitors' direction and posture. | Hiring-direction shifts, momentum, brand reach, geo footprint. |
| emerging_competitor_scan | Discovering new entrants / rising challengers. | Fast-growing, aggressively hiring companies with rising visibility. |
| talent_source | Recruiter / hiring manager looking for companies to poach from. | Hiring in target function, alumni footprint, team pedigree, retention. |
| recruiter_employer_vet | Evaluating whether to send a candidate to a particular company. | Employer retention, stability, talent demand, brand reach. |
| jobseeker_stability | Job seeker prioritizing job security ("is this company safe?"). | Low churn, positive net flow, retention, moderate momentum. |
| jobseeker_growth | Job seeker prioritizing career upside ("where can my career accelerate?"). | High momentum, growth-function hiring, talent magnetism, hiring velocity. |
Example Request
curl "https://trycanonical.ai/api/v1/search?q=AI+healthcare+startups&top_k=5" \ -H "x-api-key: YOUR_API_KEY" # Intent-ranked (sales prospecting): curl "https://trycanonical.ai/api/v1/search?q=AI+healthcare+startups&top_k=20&intent=sales_prospecting" \ -H "x-api-key: YOUR_API_KEY"
Response
{
"results": [
{
"name": "Acme Health AI",
"domain": "acmehealthai.com",
"description": "AI-powered diagnostic platform...",
"headquarters": "San Francisco, CA, United States",
"employee_count": 150,
"founding_year": 2019,
"funding": { "latest_series": "series_b", "latest_amount_usd": 45000000 },
"dimensions": {
"industry": ["Healthcare", "Artificial Intelligence"],
"offering_type": ["Software"]
},
"verdict": "relevant",
"verdict_reason": "AI diagnostic platform matches the healthcare/AI criteria..."
}
],
"count": 5,
"query": "AI healthcare startups",
"credits_used": 5,
"credits_remaining": 995,
"warnings": [],
"interpreted_criteria": {
"description": "AI healthcare startups",
"intent_inferred": "sales_prospecting"
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
| results | Company[] | Array of matching companies (strong verdicts only by default; partials when include_partials=true). |
| results[].name | string | Company name |
| results[].domain | string | Primary domain. This is the public company identifier — use it for follow-up calls (exclusions, details, similar). |
| results[].description | string | Company description |
| results[].headquarters | string | null | Company headquarters location |
| results[].employee_count | integer | null | Number of employees |
| results[].founding_year | integer | null | Year the company was founded. null when unknown. |
| results[].funding | object | null | Latest funding round: {latest_series, latest_amount_usd, latest_lead_investor, latest_date}. Null when no funding data. |
| results[].dimensions | object | null | Classification tags (industry, offering_type, target_customer, etc.) |
| results[].verdict | string | null | "relevant" (strong match) or "partial" (only with include_partials=true). |
| results[].verdict_reason | string | null | LLM verifier's reasoning (truncated for compactness). Useful for explainability. |
| count | integer | Number of results returned |
| query | string | Echo of the original query |
| credits_used | integer | Credits consumed (1 credit per strong result row) |
| credits_remaining | integer | null | Remaining credits for your account |
| warnings | object[] | Filter-degradation signals when an exclusion couldn't be fully resolved. Each entry: {code, message, field, attempted}. Empty when the search ran cleanly. |
| interpreted_criteria | object | null | How the natural-language query was parsed (description, location, employee_size, funding axes, exclusions, inferred intent). Use this for transparency — if interpretation looks wrong, retry with a clearer query. See POST /api/v1/interpret for preview-before-search. |
Preview Interpretation
Preview how a natural-language query parses into structured criteria — call this BEFORE /api/v1/search when you want to confirm the system's understanding without spending credits. Free, rate-limited.
POST /api/v1/interpret
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| q | string | Natural language query to parse (e.g. "AI fintech in SF, not Stripe"). |
Example
curl -X POST "https://trycanonical.ai/api/v1/interpret?q=AI+fintech+in+SF+not+Stripe" \ -H "x-api-key: YOUR_API_KEY" # Response { "query": "AI fintech in SF not Stripe", "interpreted_criteria": { "description": "AI fintech companies", "location": { "city": "San Francisco" }, "exclude_company_names": ["Stripe"] }, "exclusion_resolution": [ { "input": "Stripe", "resolved": true, "name": "Stripe", "domain": "stripe.com" } ] }
Pass the resolved domain to /api/v1/search via ?exclude_company_domain=stripe.com to lock the exclusion in deterministically. If the query states a constraint the engine can't map to a filter (e.g. profitable, household name), it's returned in an unmapped array inside interpreted_criteria instead of being dropped silently — surfacing what was understood but not applied.
Lookup Companies by Name
Resolve free-text company names to canonical candidates. Each input name returns ranked candidates plus a disambiguation recommendation: when auto_resolve_recommended is true the match is unambiguous (use primary_candidate_domain); when false the name is a genuine toss-up — surface the candidates for the user to choose. Costs 1 credit when at least one candidate is returned (a no-match lookup is free), rate-limited. Response includes credits_used and credits_remaining.
Confidence & auto-resolve policy. confidence is high (one candidate, or all candidates are alias domains of a single entity), medium (multiple distinct entities but the top one clearly dominates — at least 5× the next entity's headcount and ≥100 employees, or the only candidate with a description), or low (comparable distinct entities). Under the default auto_when_confident mode, high and medium auto-resolve; low asks. Invariant: primary_candidate_domain is non-null and exactly one candidate has is_primary: true only when auto_resolve_recommended is true; when asking, primary_candidate_domain is null and every candidate has is_primary: false (the list is still ranked best-guess-first, but no candidate is authoritative).
POST /api/v1/lookup
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| name | string (repeatable) | required | One or more free-text company names. Repeat to look up several at once. Max 20 per call. |
| k | integer | 5 | Candidates to return per name (1–25). |
| disambiguation_mode | string | auto_when_confident | Override the recommendation: auto_when_confident (default), always_auto, or always_ask. |
Example
curl -X POST "https://trycanonical.ai/api/v1/lookup?name=Stripe&name=Square&k=3" \ -H "x-api-key: YOUR_API_KEY" # Response { "results": { "Stripe": { "confidence": "medium", "auto_resolve_recommended": true, "primary_candidate_domain": "stripe.com", "candidates": [ { "name": "Stripe", "domain": "stripe.com", "headquarters": "San Francisco, CA, US", "description": "Online payment processing...", "is_primary": true, "payload_richness": "full" } ] }, "Square": { "confidence": "low", "auto_resolve_recommended": false, "primary_candidate_domain": null, "candidates": [ { "name": "Block", "domain": "block.xyz", "is_primary": false, "payload_richness": "full" }, { "name": "Square Yards", "domain": "squareyards.com", "is_primary": false, "payload_richness": "full" } ] } }, "credits_used": 1, "credits_remaining": 999 }
Errors
The API returns standard HTTP status codes. Error responses include a JSON body with a detail field.
| Status | Meaning | Body |
|---|---|---|
| 401 | Missing or invalid API key | {"detail": "API key required. Include x-api-key header."} |
| 406 | SSE requested with structured body | Structured mode is batch JSON only — remove Accept: text/event-stream or switch to NL mode. |
| 422 | Both or neither search mode supplied | Pass exactly one of ?q= or a structured JSON body, not both and not neither. |
| 402 | Insufficient credits | {"detail": "Insufficient credits. Have X, need Y. Purchase more at /billing."} |
| 429 | Rate limit exceeded | {"error": "Rate limit exceeded"} |
| 500 | Internal server error | {"detail": "Internal server error"} |
Credits
Search / similar: each strong-match result row consumes 1 credit — a search returning 20 results uses 20 credits. Lookup (/api/v1/lookup) costs 1 credit per call when it returns at least one candidate (a no-match lookup is free), regardless of how many names or candidates. /api/v1/interpret is free. Credits used and remaining are included in every billable response.