Company Atlas API — contract (v1)
Base: /api/v1 · JSON (orjson) · UTC ISO-8601 timestamps (Z) · immutable ids (co_…, sen_…, evt_…) · slugs for public URLs.
Public GET endpoints need no auth. Owner endpoints (watchlists, alerts) take X-CA-Owner-Token (client-generated random string ≥ 24 chars, stored hashed).
Admin endpoints take X-CA-Admin-Token. Errors: { "detail": string } with 4xx/5xx. Pagination: ?page=1&per_page=25 (max 200) →
{ items, page, per_page, total, pages }. Lists accept sort where documented; unknown params are ignored.
Interpretive language is careful by design (spec §167–168): events say "no longer listed", never "fired"; anything inferred carries a
confidence (0–1) and a confidence_label (VERIFIED | HIGH_CONFIDENCE | LIKELY | INFERRED | LOW_CONFIDENCE).
Shapes
type CompanyCard = {
id: string; slug: string; display_name: string; legal_name: string | null; canonical_domain: string; website: string;
description: string | null; industries: string[]; industry_primary: string | null; country: string | null; hq_city: string | null;
hq_region: string | null; public_company: boolean; ticker: string | null; exchange: string | null; founded_year: number | null;
employees_band: string | null; logo_url: string | null; status: string; onboarding_status: string; importance: number; tier: 1|2|3|4;
metrics: Partial<Record<Metric, number>>; // activity_score, hiring_momentum_30d, product_velocity, ai_adoption, corporate_change_index, open_jobs …
counts: { sensors: number; observations: number; changes: number; events: number; jobs_open: number };
last_event_at: string | null; last_observed_at: string | null;
sparkline?: number[]; // 30 daily activity values when `?sparkline=1` (or on detail)
};
type Metric = 'activity_score'|'hiring_momentum_7d'|'hiring_momentum_30d'|'hiring_momentum_90d'|'open_jobs'|'ai_adoption'|'product_velocity'|
'geo_expansion'|'developer_momentum'|'communication_activity'|'pricing_activity'|'leadership_activity'|'corporate_change_index'|'anomaly_score'|'historical_coverage';
type Event = {
id: string; company: { id: string; slug: string; display_name: string; canonical_domain: string; country: string|null; logo_url: string|null };
event_type: string; event_subtype: string; importance: number; confidence: number; confidence_label: string;
title: string; summary: string | null; old_value: string | null; new_value: string | null;
payload: Record<string, unknown>; entities: Record<string, unknown>; tags: string[];
detected_at: string; effective_at: string | null; published_at: string | null;
source_url: string | null; surface: string | null; sensor_id: string | null; change_id: string | null; cluster_id: string | null;
origin: 'deterministic'|'llm'|'hybrid'|'backfill'; model_name: string | null; prompt_version: string | null; status: 'active'|'retracted'|'duplicate'|'review';
sources?: { source_url: string; surface: string | null; detected_at: string; kind: string; sensor_id: string | null }[]; // detail only
};
type Sensor = {
id: string; company_id: string; surface: string; connector_id: string; url: string; canonical_url: string; domain: string;
status: string; tier: 'A'|'B'|'C'|'D'|'E'; quality_score: number; discovery_confidence: number; discovery_method: string | null;
current_interval_s: number; next_run_at: string; last_run_at: string | null; last_success_at: string | null; last_change_at: string | null;
last_status: number | null; last_failure_class: string | null; consecutive_failures: number;
observation_count: number; snapshot_count: number; change_count: number; meaningful_change_count: number; event_count: number; created_at: string;
};
type Snapshot = { id: string; sensor_id: string; version_no: number; fetched_at: string; title: string | null; language: string | null;
text_length: number | null; block_count: number | null; extracted_summary: Record<string, number>; content_hash: string; previous_snapshot_id: string | null };
type Change = { id: string; sensor_id: string; surface: string; company_id: string; detected_at: string; significance: number; kind: string;
blocks_added: number; blocks_removed: number; blocks_modified: number; text_delta_ratio: number; similarity: number | null;
snapshot_before: string | null; snapshot_after: string; diff?: DiffPayload; structured_delta?: Record<string, unknown> };
type DiffPayload = { added: BlockDelta[]; removed: BlockDelta[]; modified: BlockDelta[]; moved: string[]; counts: Record<string, number>;
text_delta_ratio: number; similarity: number; reasons: string[] };
type BlockDelta = { key: string; kind: string; path: string; before: string | null; after: string | null; weight: number; similarity: number | null };
type Job = { id: string; title: string; department: string | null; location_text: string | null; city: string | null; country: string | null;
remote: boolean | null; employment_type: string | null; seniority: string | null; url: string | null; posted_at: string | null;
first_seen_at: string; last_seen_at: string; removed_at: string | null; status: 'open'|'no_longer_listed'; is_ai: boolean };
type Person = { id: string; name: string; title: string | null; role_category: string | null; is_executive: boolean; first_seen_at: string; last_seen_at: string; removed_at: string | null; status: string; source_url: string | null };
type Product = { id: string; name: string; category: string | null; description: string | null; url: string | null; first_seen_at: string; last_seen_at: string; removed_at: string | null; status: string };
type Plan = { id: string; plan_name: string; price: number | null; price_text: string | null; currency: string | null; billing_period: string | null; unit: string | null; features: string[]; contact_sales: boolean; version_no: number; valid_from: string; valid_to: string | null; status: string; source_url: string | null };
type Location = { id: string; kind: string; name: string | null; city: string | null; region: string | null; country: string | null; lat: number | null; lon: number | null; first_seen_at: string; last_seen_at: string; removed_at: string | null; status: string; source_url: string | null };
type NewsItem = { id: string; title: string; url: string; summary: string | null; category: string | null; published_at: string | null; first_seen_at: string; language: string | null };
type MetricPoint = { day: string; value: number; confidence: number };
type Signal = { id: string; company_id: string | null; scope: string; scope_key: string | null; kind: string; strength: number; confidence: number; title: string; explanation: string | null; evidence: Record<string, unknown>; window_days: number; detected_at: string; status: string };Endpoints
Platform
| Method | Path | Notes |
|---|---|---|
| GET | /health, /ready |
service health (also at /api/v1/health) |
| GET | /stats |
{ companies, companies_active, sensors, sensors_active, observations, snapshots, changes, meaningful_changes, events, jobs_open, countries, industries, observations_today, changes_today, events_today, dataset_started_at, dataset_age_days, oldest_history_days, last_observation_at, archive: {objects, bytes} } (cached 60 s) |
| GET | /stats/history?days=90 |
{ items: GlobalDaily[] } — { day, companies_active, sensors_active, observations, changes, meaningful_changes, events, events_by_type, jobs_open, jobs_new, jobs_removed, activity_index } |
| GET | /system |
public aggregate health only: { sensors_online, sensors_failing, observations_today, events_today, countries_covered, queue_lag_s, scheduler_last_tick_at, fetch_per_min, success_rate_24h } |
| GET | /pulse |
homepage aggregate: { stats, live: Event[12], movers: CompanyCard[10], hiring: CompanyCard[8], launches: Event[8], pricing: Event[8], ai: CompanyCard[8], industries: IndustryRow[12], countries: CountryRow[12], trending: TrendRow[10], activity_index: {value, delta_7d, series: MetricPoint[30]}, map: MapBucket[] } (cached 60 s) |
| GET | /live?limit=50&since=<iso>&event_type=&min_importance= |
latest active events (no cache); since returns only newer than that time |
| GET | /live/stream |
SSE: event: event with an Event JSON per message (poll-based, ~5 s), event: heartbeat every 20 s, ?since= supported |
Companies
| Method | Path | Notes |
|---|---|---|
| GET | /companies |
filters: q, country, industry, tier, public (bool), status, has_events (bool), sort=activity|events|hiring|name|importance|recent|relevance (relevance is automatic with q), sparkline=1 → page of CompanyCard |
| GET | /companies/{slug_or_id} |
CompanyCard & { aliases: string[], domains: {domain, kind}[], relationships: {kind, company: {slug, display_name} | null, to_name, valid_from, valid_to, confidence}[], metrics_detail: {metric, value, confidence, computed_at, inputs}[], sensors_by_surface: Record<string, number>, coverage: {historical_coverage, first_observed_at, days_observed, sensor_uptime}, signals: Signal[], sparklines: {activity_30d: number[], hiring_90d: number[]} } |
| GET | /companies/{slug}/events |
filters event_type, event_subtype, since, until, min_importance, surface; sort=recent|importance → page of Event |
| GET | /companies/{slug}/timeline?filter=all|products|jobs|pricing|leadership|locations|legal|news|developer&limit=200 |
{ items: (Event & {day: string})[], days: {day, count}[] } grouped for the timeline UI |
| GET | /companies/{slug}/metrics?metric=activity_score&days=90 |
{ current: {metric, value, confidence, computed_at, formula_version, inputs}[], series: Record<Metric, MetricPoint[]> } |
| GET | /companies/{slug}/jobs?status=open|removed|all&q=&country=&ai=1 |
page of Job + { summary: {open, new_7d, removed_7d, ai_open, by_country: {country,n}[], by_department: {department,n}[], remote_ratio} } as meta |
| GET | /companies/{slug}/people |
{ listed: Person[], no_longer_listed: Person[] } |
| GET | /companies/{slug}/products |
{ listed: Product[], removed: Product[] } |
| GET | /companies/{slug}/pricing |
{ current: Plan[], history: Plan[] } |
| GET | /companies/{slug}/locations |
{ items: Location[], countries: string[] } |
| GET | /companies/{slug}/news?limit=50 |
{ items: NewsItem[] } |
| GET | /companies/{slug}/sensors |
{ items: Sensor[] } |
| GET | /companies/{slug}/history |
{ sensors: (Sensor & { versions: Snapshot[] })[] } — historical page viewer index (max 20 versions per sensor) |
| GET | /companies/{slug}/similar?limit=8 |
{ items: CompanyCard[] } (same industry/country, closest importance) |
| GET | /companies/compare?companies=stripe,adyen,block |
{ companies: CompanyCard[], metrics: Record<Metric, Record<slug, number>>, series: Record<slug, MetricPoint[]>, events_30d: Record<slug, Record<event_type, number>>, jobs: Record<slug, {open, ai_open, new_30d}>, locations: Record<slug, number> } (2–6 companies) |
Sensors, snapshots, changes (provenance)
| Method | Path | Notes |
|---|---|---|
| GET | /sensors/{id} |
Sensor & { company: CompanyRef, latest_snapshot: Snapshot | null } |
| GET | /sensors/{id}/snapshots?limit=50 |
{ items: Snapshot[] } |
| GET | /sensors/{id}/changes?limit=50 |
{ items: Change[] } |
| GET | /snapshots/{id} |
Snapshot & { text: string (≤ 200 kB), blocks: Block[], extracted: object } |
| GET | /snapshots/{id}/diff/{other_id} |
{ before: Snapshot, after: Snapshot, diff: DiffPayload } (computed on demand) |
| GET | /changes/{id} |
Change with diff and structured_delta, plus events: Event[] |
| GET | /events/{id} |
Event detail with sources, change (Change summary), company |
Events
| Method | Path | Notes |
|---|---|---|
| GET | /events |
filters event_type, event_subtype, country, industry, since, until, min_importance, min_confidence, q, surface, origin, company; sort=recent|importance → page of Event |
| GET | /events/types |
{ types: {event_type, subtypes: {event_subtype, count_30d}[], count_30d}[] } |
| GET | /events/summary?days=7&group=type|industry|country |
{ items: {key, count, delta_pct}[] } |
Rankings, industries, countries, signals, trends
| Method | Path | Notes |
|---|---|---|
| GET | /rankings?kind=most_active|hiring_growth|hiring_decline|product_velocity|ai_active|geo_expansion|developer_momentum|pricing_changes|unusual_activity&window=24h|7d|30d|90d|1y&country=&industry=&limit=50 |
{ kind, window, items: (CompanyCard & { rank: number; value: number; delta: number | null })[] } |
| GET | /industries |
{ items: IndustryRow[] } — { slug, name, parent_slug, companies, events_7d, events_30d, hiring_momentum_30d, activity_score, ai_adoption, top_event_types: string[] } |
| GET | /industries/{slug} |
IndustryRow & { description, companies: CompanyCard[24] (most active), events: Event[20], hiring: {open, new_30d, removed_30d, momentum_30d}, series: MetricPoint[90] (activity), countries: {country, companies}[], trending: TrendRow[] } |
| GET | /countries |
{ items: CountryRow[] } — { code, name, region, companies, events_7d, events_30d, hiring_momentum_30d, activity_score, industry_mix: {industry, companies}[], lat, lon } |
| GET | /countries/{code} |
CountryRow & { companies: CompanyCard[24], events: Event[20], movers: CompanyCard[10], new_entrants: CompanyCard[10], series: MetricPoint[90], industries: IndustryRow[] } |
| GET | /signals?kind=&scope=company|industry|country|global&limit=50 |
{ items: Signal[] } |
| GET | /trends?window=7d|30d|90d&limit=30 |
{ items: TrendRow[] } — { term, mentions, companies, momentum, series: number[] } |
| GET | /map?metric=events_30d|companies|hiring |
{ buckets: MapBucket[] } — { lat, lon, country, city: string | null, companies: number, events_30d: number, jobs_open: number, top: {slug, display_name}[] } (clustered by city/country, ≤ 600 buckets) |
| GET | /index |
Global Corporate Activity Index: { value, baseline: 100, delta_7d, delta_30d, series: MetricPoint[365], by_type: Record<string, number>, by_country: {key, value}[], by_industry: {key, value}[], formula_version } |
Search
| Method | Path | Notes |
|---|---|---|
| GET | /search?q=&types=companies,events,industries,countries,people,products&limit=10 |
{ query, companies: CompanyCard[], events: Event[], industries: IndustryRow[], countries: CountryRow[], people: (Person & {company: CompanyRef})[], products: (Product & {company: CompanyRef})[], took_ms } |
| GET | /search/suggest?q= |
{ items: { kind: 'company'|'industry'|'country'|'event_type', label, sublabel, href }[] } (≤ 10, < 50 ms) |
| GET | /ask?q= |
natural-language routing (deterministic parser → structured query; LLM optional): { interpretation: {filters…}, answer: string, companies: CompanyCard[], events: Event[], sources: string[] } |
Watchlists & alerts (owner token)
| Method | Path | Notes |
|---|---|---|
| GET/POST | /watchlist |
GET → { items: CompanyCard[], events: Event[30] (for the watched companies) }; POST { company: slug } adds; DELETE /watchlist/{slug} removes |
| GET/POST | /alerts |
POST { name, company?: slug, condition: {event_types?: string[], min_importance?: number, metrics?: {activity_score?: {gt: number}}}, channel: 'web'|'webhook', target?: url }; DELETE /alerts/{id} |
| GET | /alerts/deliveries?limit=50 |
recent deliveries for this owner |
Exports & docs
| Method | Path | Notes |
|---|---|---|
| GET | /export/events.{json,ndjson,csv}?since=&event_type=&country=&limit=10000 |
streamed export |
| GET | /export/companies.{json,ndjson,csv}?country=&industry= |
streamed export |
| GET | /export/jobs.ndjson?company=&since= |
|
| GET | /sitemap?kind=companies|industries|countries&page= |
{ items: {slug, updated_at}[], pages } — only companies with indexed = true |
| GET | /methodology |
{ metrics: {metric, formula_version, description, inputs: string[]}[], significance_bands, event_types: string[], confidence_labels } |
Admin (X-CA-Admin-Token)
| Method | Path | Notes |
|---|---|---|
| GET | /admin/overview |
{ companies_by_status, sensors_by_status, sensors_by_tier, queue: {pending, running, dead, oldest_pending_s}, llm: {pending, done_today, failed_today, budget_left}, failures_24h_by_class, fetch_rate_1h, change_rate_1h, meaningful_rate_1h, storage: {objects, bytes}, workers: {name, last_seen_at, inflight}[], cost_today: {fetch, browser, llm} } |
| GET | /admin/connectors |
{ items: {id, name, version, category, enabled, sensors_active, sensors_failing, success_rate_24h, avg_latency_ms, change_rate_24h, errors_24h, last_run_at}[] } |
| GET | /admin/sensors?status=&domain=&connector=&company=&filter=healthy|failing|stale|blocked|redirected|low_quality|high_activity&page= |
page of Sensor & {company: CompanyRef} |
| POST | /admin/sensors/{id}/{action} |
action ∈ pause, resume, retry, rediscover, retire, run_now; body { interval_s?, connector_id? } for set_interval, set_connector |
| GET | /admin/companies?onboarding_status=&page= · POST /admin/companies { website, display_name?, country?, industries? } (create + queue discovery) · POST /admin/companies/{slug}/rediscover |
|
| GET | /admin/failures?class=&since=&page= · GET /admin/queue?kind=&status= · POST /admin/queue/requeue-dead |
|
| GET | /admin/llm?status=&page= · GET /admin/reviews?kind=&status=open · POST /admin/reviews/{id} { resolution: 'accepted'|'rejected', note? } |
|
| POST | /admin/events/{id}/retract { reason } · POST /admin/events/{id}/restore |
|
| GET | /admin/quality |
{ coverage: {companies_active_pct, sensors_active_pct}, freshness: {sensors_checked_24h_pct, stale}, duplicate_rate, event_confidence_avg, unknown_surfaces, failed_sensors, calibration: {correct, duplicate, noise, misclassified} } |
| GET | /admin/costs?days=30 |
{ items: {day, dimension, key, units, cost_estimate}[], per_1000_companies, per_million_observations, per_meaningful_event } |
| POST | /admin/cache/clear |
Rate limits & auth
| Tier | How | Limit (per client, per minute) |
|---|---|---|
| anonymous | no header — keyed by client IP (first hop of X-Forwarded-For) |
120 |
| authenticated | X-CA-API-Key: <key> (create with catlas api-key create <name>) |
600 |
| paid | X-CA-API-Key with tier = paid (catlas api-key create <name> --tier paid) |
3 000 |
| internal | X-CA-API-Key with tier = internal |
unlimited |
| admin | valid X-CA-Admin-Token |
bypass (also required for /admin/*) |
Token bucket per process (refills continuously at limit / 60 tokens per second). Every limited response carries X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Tier; when the bucket is empty the API answers 429 { "detail": "rate limit exceeded" } with a
Retry-After (seconds). /health, /ready, /api/v1/docs and /api/v1/openapi.json are never limited. Keys are stored as sha256 hashes
(api_keys.key_hash); the raw key is printed once by the CLI. last_used_at / request_count are updated lazily (≈ every 30 s). Owner endpoints
(/watchlist, /alerts*) need X-CA-Owner-Token (client-generated, 24–200 chars); the owner row is created on first use and the token is only
ever stored hashed. Admin endpoints answer 401 { "detail": "admin token required" } without a valid X-CA-Admin-Token.
Implementation notes (API v1.0 — additive details)
- Caching / ETags. Cached public aggregates (
/pulse60 s,/stats60 s,/system30 s,/rankings120 s,/industries*,/countries*,/index,/map,/trends,/events/types,/events/summary,/stats/history300 s,/sitemap600 s,/methodology3600 s) returnCache-Control: public, max-age=N, stale-while-revalidate=2Nplus a weakETag; a matchingIf-None-Matchyields304. Other public GETs carrypublic, max-age=30…600(per resource volatility);/live*, owner and admin routes areno-store.POST /admin/cache/clear?prefix=clears one key prefix (e.g.pulse,industries:) or everything. - Pagination totals.
totalis exact up to 10 000 (bounded count) so deep feeds stay cheap;pagesderives from it. /livealso acceptscountryandindustry; the payload is{ items, count, cursor, server_time }— passcursorback assince./live/streamaccepts?since=&event_type=&min_importance=&max_s=(max_s≤ 3600 bounds the connection for proxies with idle timeouts; the stream ends withevent: endcarrying the last cursor). The first message is always anevent: heartbeatwith{at, cursor};event: eventmessages carryid: <event id>. Response headers includeX-Accel-Buffering: no./companiesgainssort=relevance(automatic whenqis present);quses FTS (companies.search) for ≥ 3 characters with a trigram fallback and a prefix match for shorter strings.sparkline=1adds 30 dailyactivity_scorepoints./companies/{slug}additionally returnscompany_type, employees, wikidata_id, indexed, discovered_at, first_observed_at, last_change_at, recent_events: Event[10]andsparkline./companies/{slug}/timelineacceptsfilter=corporate(FINANCING, M&A, PARTNERSHIP, STRATEGY) andbefore=<iso>; items carryday./companies/{slug}/jobsacceptsdepartment,remote,sort=recent|title|posted;qis a trigramILIKEontitle./companies/{slug}/historyacceptsversions=1…20;/companies/{slug}/sensors?include_retired=1;/companies/{slug}/news?category=;/companies/{slug}/locations?status=all;/companies/{slug}/pricing?history_limit=./companies/compareacceptsdays=(7–365) for the activity series; unknown slugs →404 company not found: <slug>; fewer than 2 → 422./eventsacceptsstatus=active|retracted|duplicate|review|all(defaultactive). Retracted events exposeretracted_reason./events/{id}embeds the fullCompanyCardundercompany(a superset ofCompanyRef),sources(falls back to the event's ownsource_urlwhenevent_sourcesis empty),changeandcluster./events/typeslists every taxonomy type/subtype (count0when unseen) so filter UIs can render on an empty database./events/summaryitems also carryprevious(count of the preceding window)./rankingsitems areCompanyCard & { rank, value, delta }; the payload also listskinds.hiring_growth/hiring_declinereadhiring_momentum_7d|30d|90daccording to the window;pricing_changescounts PRICING events in the window;deltais the difference with themetric_seriesvalue at the start of the window (nullwhen no history yet)./countries/{code}accepts an ISO-2 code or a name slug (/countries/canada);CountryRowincludesslugandsubregion./industries/{slug}addschildren,signals,companies_total;trendingis[]until per-industry trend data exists (never faked)./signalsacceptsscope_key,company,min_strength; items include acompanyref when scoped to a company./searchpeople/products use trigram similarity (migrations/pending/api.sqladds the two supporting GIN indexes)./search/suggestitems addslug/code/event_typenext tohref./askreturnsengine(deterministic|llm) andevents_total; the parser isservices/llm/ask.pywhen present (deterministic parse + optional LLM refinement), elseapi/ask_fallback.py. Answers only phrase measured counts./snapshots/{id}acceptsinclude=text,blocks,extractedand returnstext_truncated(text capped at 200 kB),sensor,object_keys./snapshots/{a}/diff/{b}orders the pair byfetched_at, returnssource: "change"(stored diff, withchange_id) or"computed"(sdk.diff.compare);501when the diff engine is unavailable,404when block objects are missing.- Exports.
/export/events.{fmt}also acceptsuntil,company,min_importance,limit(≤ 10 000);/export/companies.{fmt}acceptsstatus,tier,limit(≤ 20 000);/export/jobs.{json|ndjson|csv}acceptsstatus=open|removed|all,country,ai,limit(≤ 10 000). CSV flattens the company ref (company_slug,company_name,company_domain,country). /sitemappages hold 5 000 entries;kind=companiesreturnstotal; industries/countries only list entries with ≥ 1 active company.- Admin additions.
GET /admin/sensorsacceptssurface,sort=recent|next_run|failures|quality|changes|created; sensor actions return{ ok, action, sensor, queued? , interval_s? }(set_intervalis clamped toCA_MIN_INTERVAL_S…CA_MAX_INTERVAL_Sand recomputes the tier).POST /admin/companies→201 { ok, company, queued }(409 when the registrable domain already exists; 422 for non-http(s) URLs, unknown country codes or industry slugs).GET /admin/failuresreturnsby_classandclasses;GET /admin/queuereturnscounts+items;POST /admin/queue/requeue-dead { kind? };GET /admin/llmincludesstats;POST /admin/reviews/{id}acceptslabel(correct | duplicate | noise | misclassified) feeding/admin/quality.calibration; retract/restore append an audit trail underpayload._auditand never delete;GET /admin/storagereports the object store./admin/overviewalso returnscompanies_by_onboarding,scheduler_heartbeat.
Conventions for implementers
- Every list endpoint is bounded (
per_page ≤ 200,limit ≤ 500), uses indexed predicates, and returnsCache-Control: public, max-age=60for public aggregates (/pulse,/stats,/rankings,/industries,/countries) andno-storefor/live*, owner and admin routes. - Company lookups accept slug or id. Unknown → 404
{detail: "company not found"}. - Text search: Postgres FTS (
companies.search,events.search) + trigram fallback for short/partial queries; never LIKE on unindexed columns. - Numbers: metrics are 0–100 floats rounded to 1 decimal except
hiring_momentum_*(percentage, may be negative) andopen_jobs(int). - Never fabricate: when a metric has no inputs, omit it (or
null) rather than returning 0 as if measured.