API reference

EarningSpy Data API

Point-in-time earnings data for people who build their own models.

Every row is a snapshot frozen before the event and never rewritten, so you can backtest against it without look-ahead bias. Pre-earnings fundamentals, risk and event-window metrics; post-earnings outcomes with abnormal returns at 3, 30 and 60 days. Pull it into a DataFrame and go.

Base URL
https://api.earningspy.ai/v1
Endpoints
9
Features
384

Base URL

https://api.earningspy.ai/v1

Authentication

You need an API key. Request one and pass it on every call, either way:

x-api-key: esk_live_...
Authorization: Bearer esk_live_...

Keep it somewhere safe: we show it once and store only a hash, so a lost key gets replaced rather than recovered.

Tiers

The API is layered like the pipeline behind it. Each tier reads its own layer and everything below it.

TierWhat you get
bronzePre-market metrics. Fundamentals, valuation, risk and event-window features as they stood before the report. The raw material for your own models.
silverBronze plus a quality score, trained to separate the events worth modelling from the noise.
goldBronze and silver plus the model’s own read: the probability that an event moves abnormally, and which way. Plus the realized outcomes, precomputed.

What gold actually returns. Two classifiers in sequence. The first asks whether this event is likely to produce an abnormal move at all; the second, conditional on that, asks the direction. Both hand back calibrated probabilities and quintile buckets, not a buy/sell label. What you do with a 0.72 is your business.

The boundary is enforced twice: the authorizer resolves your key’s scope, and each layer can physically read only its own storage. A bronze key cannot reach gold data even if a routing bug lets the request through.

Need a different column set? If your research process expects specific features (a ratio you trust, a filter on your universe, a formula your team already validated) we build it as a named feature set on your key. Browse the data catalog and tell us what you need.

The response envelope

Every successful data response has the same shape:

{
  "data": [ { "TICKER": "ACME", "EARNINGS_DATE": "2026-07-14T00:00:00" } ],
  "next_cursor": "eyJkIjoiMjAyNi0wNy0xNFQwMDowMDowMCIsInQiOiJBQ01FIn0",
  "count": 1,
  "price_as_of": "2026-07-11T12:00:00+00:00",
  "layers_included": ["bronze"],
  "window": { "start": "2026-04-01", "end": "2026-09-30", "period": "biquarter" },
  "sort": "asc"
}
  • next_cursor: pass it back as ?cursor= for the next page. null means you have reached the end.
  • price_as_of: the freshest PRICE_AS_OF among the returned rows. null is normal, since only price-refreshed rows carry a timestamp, so historical rows report unknown freshness rather than a fabricated one.
  • count: rows in this page, never the total.
  • window: the date range actually applied, so an empty page is never ambiguous.
  • sort: the ordering actually applied.
  • layers_included: which layers contributed to these rows.

Pagination is keyset, not offset. Page 400 costs what page 1 costs, and rows are neither skipped nor repeated when data shifts underneath you. Follow next_cursor until it comes back null, and treat the value as opaque.

import requests
import pandas as pd

URL = "https://api.earningspy.ai/v1/pre/bronze/calendar"
HEADERS = {"x-api-key": "esk_live_..."}

rows, cursor = [], None
while True:
    r = requests.get(
        URL,
        headers=HEADERS,
        params={"view": "calendar", "period": "quarter", "cursor": cursor},
    ).json()
    rows += r["data"]
    cursor = r["next_cursor"]
    if cursor is None:
        break

df = pd.DataFrame(rows)

Feature sets

A feature set is a named column selection: the fields, their order, and any filter, decided server-side. Ask for one by name instead of assembling a column list.

GET /v1/pre/bronze/calendar?view=calendar

The same feature set widens as you move up a tier. calendar on bronze returns the pre-event features; on silver it carries the quality score too; on gold, the model’s direction and abnormality reads. Same name, more signal. layers_included in the response tells you which layers contributed.

How these change

Worth knowing if you are building against them.

A feature set’s columns are additive. New columns get appended; existing ones are not renamed or removed under you. If a column has to go, it ships as a new feature set and the old one keeps working. Position is stable, so ordinal access is safe, though selecting by name is still the better habit.

A different universe is a different feature set, never a parameter. sp500 is calendar with a filter, and it gets its own name rather than a ?universe= flag. That way what you asked for is in the request, and a cached or logged response is never ambiguous about which population it covered.

What you request is what you get. Ask for calendar in a year and you get the same columns in the same order, plus anything added since.

Filtering by period

?period=day        today
?period=week       this week
?period=next_week  next week
?period=month      this month
?period=quarter    this quarter
?period=biquarter  this quarter and the previous one (default)

Windows are relative to today. biquarter is the default because the store spans years. Without a window, a bare request would start paging from the oldest data we hold.

Need an arbitrary historical range for a backtest? That is a feature set we cut for you. Ask.

Common issues

PRICE_AS_OF is empty on historical rows. That is the honest answer, not a gap. Past events are served exactly as they were recorded, so there is no live price to timestamp.

DATADATE and DIVIDEND_EX_DATE are strings that look like dates. Cast them before you sort, or you will get lexicographic ordering and a very confusing chart.

NA is a real ticker. Nano Labs Ltd. Your CSV parser will quietly turn it into a null and you will lose a company. We know because we did it first.

Post-event outcomes are empty for upcoming events. A 60-day return needs 60 days. Nothing is missing; the future simply has not happened yet.

Available feature sets

Ask for one by name with ?view=. The same name returns more columns on a higher tier, and layers_included in the response tells you which layers contributed.

Feature set Frame Columns Requires What it returns
calendar pre 18 bronze key Upcoming earnings with size, valuation, headline risk and event score.
sp500 pre 18 bronze key The same columns as `calendar`, restricted to S&P 500 constituents.

Endpoints

Every route requires a key. The tier badge is the minimum a key must carry; a higher tier reads everything below it.

Pre-earnings

GET /pre/bronze/calendar
bronze

Upcoming earnings, with the features as they stand today

https://api.earningspy.ai/v1/pre/bronze/calendar

The pre-earnings snapshot for every company with a report coming up: what the fundamentals, valuation, risk and event-window metrics looked like before the market found out. Refreshed weekly, and each row is frozen at capture. We do not go back and rewrite history, which is what makes it safe to backtest.

Ordered by earnings date, then ticker.

Included in the calendar feature set: identity (TICKER, COMPANY, SECTOR, EARNINGS_DATE, DAYS_LEFT), timing flags (IS_BMO, IS_AMC), valuation (MARKET_CAP, P/E, FWD_P/E), surprise history (EPS_SURPRISE, REVENUE_SURPRISE), risk (SHARPE_ANNZD, FHS_GARCH_ANNZD_VOL) and the event score (EVENT_SCORE, EVENT_SCORE_QUINT).

On the values. EPS here is the consensus going into the report, not a result: this is the pre-earnings snapshot, so it carries the expectation.

Parameters

Name In Description
view query

A named view: the fields to return, their order, any row filter and the sort, all decided server-side.

Prefer this over fields. A view is a surface we define and can change in one place; a field list is one a caller assembled and now depends on. Filtered variants are separate view names (sp500) rather than filter parameters.

See the views reference (#views) for the available views and their columns.

calendar · sp500

period query

A named window over EARNINGS_DATE, always relative to the request date. There is no anchor parameter - use start/end for an arbitrary window.

PeriodWindow
daytoday
weekMonday to Sunday of the current week
next_weekMonday to Sunday of the FOLLOWING week
month_to_datethe 1st of the current month through today
monthfirst to last day of the current month
quarterthe current calendar quarter
biquarterthe current quarter and the previous one

biquarter is the default because the store spans years: without a window, a bare request would page from the oldest data it holds.

In Q1 the biquarter window crosses a year boundary - in March it runs 1 Oct of the prior year to 31 Mar.

day · week · next_week · month_to_date · month · quarter · biquarter

Default biquarter

sort query

Direction of the EARNINGS_DATE ordering. TICKER is always ascending within a date, in both directions.

Ascending is the default because these are upcoming events - soonest first is the calendar reading. Use desc for looking backwards.

asc · desc

Default asc

limit query
Rows per page.

Default 100

cursor query
Opaque pagination token from a previous response’s next_cursor. Treat as opaque - the encoding is not part of the contract. A malformed cursor returns 400.

Example responses

First page of a week

{
  "data": [
    {
      "TICKER": "ACME",
      "COMPANY": "Acme Financial Corp.",
      "SECTOR": "Financial",
      "EARNINGS_DATE": "2026-07-14T00:00:00",
      "DAYS_LEFT": 3,
      "IS_BMO": 1,
      "MARKET_CAP": 895300000000,
      "P/E": 14.2,
      "FWD_P/E": 13.1,
      "EPS": 5.8,
      "EPS_SURPRISE": 0.041,
      "SHARPE_ANNZD": 0.94,
      "FHS_GARCH_ANNZD_VOL": 0.213,
      "VAR_H": -0.024,
      "C_VAR": -0.036,
      "EVENT_SCORE": 0.61,
      "EVENT_SCORE_QUINT": 4,
      "PRICE_AS_OF": "2026-07-11T12:00:00+00:00"
    }
  ],
  "next_cursor": "eyJkIjoiMjAyNi0wNy0xNFQwMDowMDowMCIsInQiOiJBQ01FIn0",
  "count": 1,
  "price_as_of": "2026-07-11T12:00:00+00:00",
  "layers_included": [
    "bronze"
  ],
  "window": {
    "start": "2026-04-01",
    "end": "2026-09-30",
    "period": "biquarter"
  },
  "sort": "asc"
}

Final page - next_cursor is null

{
  "data": [],
  "next_cursor": null,
  "count": 0,
  "price_as_of": null,
  "layers_included": [
    "bronze"
  ],
  "window": {
    "start": "2026-04-01",
    "end": "2026-09-30",
    "period": "biquarter"
  },
  "sort": "asc"
}

Status codes

  • 200 A page of earnings events
  • 400 A parameter was rejected: an unknown column in fields, a malformed cursor, an out-of-range limit, or an impossible date.
  • 401 No credential presented, or the key is unknown.
  • 403 The key is revoked or expired, or its tier does not include the layer being requested.
  • 429 The key’s usage-plan throttle or monthly quota was exceeded. Back off and retry; quotas reset monthly.
  • 500 Unexpected server error. Details are logged, never returned.
GET /pre/bronze/ticker/{ticker}
bronze

One ticker's full event history, features and outcomes together

https://api.earningspy.ai/v1/pre/bronze/ticker/{ticker}

Every earnings event for a ticker, newest first: the upcoming one, then its history. Each historical row pairs the pre-event snapshot with what actually happened afterwards, with abnormal returns measured at 3, 30 and 60 days and the market’s own move stripped out.

That pairing is the point. Features on the left, realized outcome on the right, one row per event: a training set that needs no assembly.

The outcome columns are gold tier, so a gold key returns the features and the targets together on this route.

Not paginated. A single ticker has few rows, and you want all of them.

Ask for a feature set by name:

GET /v1/pre/bronze/ticker/ACME?view=calendar
GET /v1/pre/bronze/ticker/NVDA?view=sp500

Parameters

Name In Description
ticker required path

Uppercase symbol. Dots and hyphens are supported (BRK.B, RDS-A). Lowercase input is upper-cased.

NA is a valid ticker (Nano Labs Ltd), not a null.

view query

A named view, exactly as on the calendar route - the same view gives you an upcoming event and this ticker’s history in the same shape, so a client can render both from one definition.

A view’s row filters are not applied here: the path parameter already fixes which rows come back, and an index filter would make the same ticker appear or vanish depending on the view.

See the views reference (#views) for the available views.

calendar · sp500

limit query

Cap the number of events returned. Omit for the ticker’s full history.

Useful when only the recent past matters - limit=6 gives the upcoming event plus the previous five, which is what an earnings history panel typically renders.

Status codes

  • 200 The ticker’s upcoming events
  • 400 The ticker is malformed - wrong characters or too long. Returned before any query runs.
  • 401 No credential presented, or the key is unknown.
  • 403 The key is revoked or expired, or its tier does not include the layer being requested.
  • 404 No bronze rows for this ticker
  • 429 The key’s usage-plan throttle or monthly quota was exceeded. Back off and retry; quotas reset monthly.
  • 500 Unexpected server error. Details are logged, never returned.

Post-earnings

GET /post/bronze/calendar
bronze

Companies that have already reported

https://api.earningspy.ai/v1/post/bronze/calendar

The snapshot after the announcement: what the report changed, and what the stock did next. Built for post-earnings drift research, the anomaly where price keeps sliding in the direction of the surprise for weeks after the news. A 30 to 60 day horizon is where the effect has historically been most measurable.

The period-over-period change columns are what most of this is for. EPS_CHANGE sits beside EPS, P/E_CHANGE beside P/E, and so on for every fundamental. Level tells you where a company is; change tells you what just happened to it, and the second one is what the drift responds to.

On the targets. Bronze gives you the features. What it does not give you is what happened next: the abnormal returns at 3, 30 and 60 days, and the realized labels the drift is measured against. Those are gold tier.

You can absolutely compute them yourself. It means sourcing price history, picking a market model, defining the event window, and deciding how to handle the awkward cases: a ticker that halted, a report that moved, a window that straddles a split. It is a week of work and a source of quiet errors that do not surface until your backtest looks suspiciously good.

Or you take them precomputed, on the same row as the features, aligned to the same event. That is what a gold key buys you: CAR_3, CAR_30, CAR_60, the expected-return baselines they are measured against, and the realized labels REALIZED_DIRECTION, REALIZED_IS_ABNORMAL and REALIZED_PERSISTENCE, served from /v1/pre/gold/ticker/{ticker}.

They are also, candidly, the labels our own models train on. We are not keeping the targets back; we are selling you the version we already had to get right.

Parameters

Name In Description
view query

A named view. Post-earnings views are a separate namespace - asking for a pre-earnings view here returns a 400 naming the endpoint where it does work.

See the views reference (#views).

period query

A named window over EARNINGS_DATE, relative to the request date. The default is month_to_date rather than biquarter: a bare request on this frame should answer “what just happened”.

PeriodWindow
daytoday
weekMonday to Sunday of the current week
month_to_datethe 1st of the current month through today
monthfirst to last day of the current month
quarterthe current calendar quarter
biquarterthe current quarter and the previous one

next_week is accepted but returns nothing here - it names a future window, and this frame holds only past events.

day · week · next_week · month_to_date · month · quarter · biquarter

Default month_to_date

sort query
Date direction. Ticker is always ascending within a date. Defaults to desc on this frame - a recap reads backwards from the most recent event.

asc · desc

Default desc

limit query

Default 100

cursor query
Opaque keyset cursor from a previous response.

Example response

The post-earnings brief, newest first

{
  "data": [
    {
      "TICKER": "ACME",
      "COMPANY": "Acme Financial Corp.",
      "SECTOR": "Financial",
      "EARNINGS_DATE": "2026-07-14T00:00:00",
      "EPS": 7.7,
      "EPS_CHANGE": 0.328,
      "SALES_CHANGE": 0.118,
      "MARKET_CAP_CHANGE": 0.062,
      "P/E_CHANGE": -0.044,
      "OPER_M_CHANGE": 0.019,
      "FCF_CHANGE": 0.087,
      "ROE_CHANGE": 0.023
    }
  ],
  "next_cursor": null,
  "count": 1,
  "price_as_of": null,
  "layers_included": [
    "bronze"
  ],
  "window": {
    "start": "2026-04-01",
    "end": "2026-09-30",
    "period": "biquarter"
  },
  "sort": "asc"
}

Status codes

  • 200 A page of completed earnings events
  • 400 A parameter was rejected: an unknown column in fields, a malformed cursor, an out-of-range limit, or an impossible date.
  • 401 No credential presented, or the key is unknown.
  • 403 The key is revoked or expired, or its tier does not include the layer being requested.
  • 429 The key’s usage-plan throttle or monthly quota was exceeded. Back off and retry; quotas reset monthly.
  • 500 Unexpected server error. Details are logged, never returned.
  • 503 This deployment is not configured to serve the requested frame - its parquet and schema keys are absent from the function’s environment.
GET /post/bronze/ticker/{ticker}
bronze

One ticker's completed events, newest first

https://api.earningspy.ai/v1/post/bronze/ticker/{ticker}

The same post-report snapshot for a single ticker across its history. Useful for a single-name study, or for checking how a company has behaved around its own reports rather than across the market.

Parameters

Name In Description
ticker required path
view query

limit query

Status codes

  • 200 The ticker’s completed events, newest first
  • 400 A parameter was rejected: an unknown column in fields, a malformed cursor, an out-of-range limit, or an impossible date.
  • 401 No credential presented, or the key is unknown.
  • 403 The key is revoked or expired, or its tier does not include the layer being requested.
  • 404 No post-earnings rows for this ticker. On this frame that also covers a ticker whose only events are still upcoming.
  • 429 The key’s usage-plan throttle or monthly quota was exceeded. Back off and retry; quotas reset monthly.
  • 500 Unexpected server error. Details are logged, never returned.
  • 503 This deployment is not configured to serve the requested frame - its parquet and schema keys are absent from the function’s environment.

Silver

GET /pre/silver/calendar
silver

The calendar, plus the quality read

https://api.earningspy.ai/v1/pre/silver/calendar

Everything the bronze calendar returns, with the silver model’s inference joined on. One inference per event, from the latest trained model, so rows never multiply.

Silver answers one question: is this event worth modelling at all? It is trained to separate events with a clean, tradeable structure from the ones that are mostly noise. Filtering on it before you run anything expensive is the intended use.

Added on this tier:

Column
QUALITY_SCOREThe score itself
QUALITY_QUINTQuintile bucket, 1 to 5
QUALITY_SIGNALCategorical read
QUALITY_PROBCalibrated probability
QS_VS_SECTORScore relative to the event’s sector

Only events silver has scored come back. Asking for quality scores and receiving rows without one would be worse than a shorter list.

Parameters

Name In Description
view query

A named view: the fields to return, their order, any row filter and the sort, all decided server-side.

Prefer this over fields. A view is a surface we define and can change in one place; a field list is one a caller assembled and now depends on. Filtered variants are separate view names (sp500) rather than filter parameters.

See the views reference (#views) for the available views and their columns.

calendar · sp500

period query

A named window over EARNINGS_DATE, always relative to the request date. There is no anchor parameter - use start/end for an arbitrary window.

PeriodWindow
daytoday
weekMonday to Sunday of the current week
next_weekMonday to Sunday of the FOLLOWING week
month_to_datethe 1st of the current month through today
monthfirst to last day of the current month
quarterthe current calendar quarter
biquarterthe current quarter and the previous one

biquarter is the default because the store spans years: without a window, a bare request would page from the oldest data it holds.

In Q1 the biquarter window crosses a year boundary - in March it runs 1 Oct of the prior year to 31 Mar.

day · week · next_week · month_to_date · month · quarter · biquarter

Default biquarter

sort query

Direction of the EARNINGS_DATE ordering. TICKER is always ascending within a date, in both directions.

Ascending is the default because these are upcoming events - soonest first is the calendar reading. Use desc for looking backwards.

asc · desc

Default asc

limit query
Rows per page.

Default 100

cursor query
Opaque pagination token from a previous response’s next_cursor. Treat as opaque - the encoding is not part of the contract. A malformed cursor returns 400.

Example responses

First page of a week

{
  "data": [
    {
      "TICKER": "ACME",
      "COMPANY": "Acme Financial Corp.",
      "EARNINGS_DATE": "2026-07-14T00:00:00",
      "DAYS_LEFT": 3,
      "MARKET_CAP": 895300000000,
      "EVENT_SCORE": 0.61,
      "QUALITY_SCORE": 0.68,
      "QUALITY_QUINT": 4,
      "QUALITY_SIGNAL": "clean",
      "QUALITY_PROB": 0.68,
      "QS_VS_SECTOR": 0.12,
      "PRICE_AS_OF": "2026-07-11T12:00:00+00:00"
    }
  ],
  "next_cursor": null,
  "count": 1,
  "price_as_of": "2026-07-11T12:00:00+00:00",
  "layers_included": [
    "bronze",
    "silver"
  ],
  "window": {
    "start": "2026-04-01",
    "end": "2026-09-30",
    "period": "biquarter"
  },
  "sort": "asc"
}

Final page - next_cursor is null

{
  "data": [],
  "next_cursor": null,
  "count": 0,
  "price_as_of": null,
  "layers_included": [
    "bronze"
  ],
  "window": {
    "start": "2026-04-01",
    "end": "2026-09-30",
    "period": "biquarter"
  },
  "sort": "asc"
}

Status codes

  • 200 A page of earnings events
  • 400 A parameter was rejected: an unknown column in fields, a malformed cursor, an out-of-range limit, or an impossible date.
  • 401 No credential presented, or the key is unknown.
  • 403 The key is revoked or expired, or its tier does not include the layer being requested.
  • 429 The key’s usage-plan throttle or monthly quota was exceeded. Back off and retry; quotas reset monthly.
  • 500 Unexpected server error. Details are logged, never returned.
GET /pre/silver/ticker/{ticker}
silver

One ticker's history, plus the quality read

https://api.earningspy.ai/v1/pre/silver/ticker/{ticker}

One ticker’s full event history with the silver quality read joined on, newest first. The single-name counterpart to the silver calendar.

Only events silver has scored come back.

Parameters

Name In Description
ticker required path

Uppercase symbol. Dots and hyphens are supported (BRK.B, RDS-A). Lowercase input is upper-cased.

NA is a valid ticker (Nano Labs Ltd), not a null.

view query

A named view, exactly as on the calendar route - the same view gives you an upcoming event and this ticker’s history in the same shape, so a client can render both from one definition.

A view’s row filters are not applied here: the path parameter already fixes which rows come back, and an index filter would make the same ticker appear or vanish depending on the view.

See the views reference (#views) for the available views.

calendar · sp500

limit query

Cap the number of events returned. Omit for the ticker’s full history.

Useful when only the recent past matters - limit=6 gives the upcoming event plus the previous five, which is what an earnings history panel typically renders.

Status codes

  • 200 The ticker’s upcoming events
  • 400 The ticker is malformed - wrong characters or too long. Returned before any query runs.
  • 401 No credential presented, or the key is unknown.
  • 403 The key is revoked or expired, or its tier does not include the layer being requested.
  • 404 No bronze rows for this ticker
  • 429 The key’s usage-plan throttle or monthly quota was exceeded. Back off and retry; quotas reset monthly.
  • 500 Unexpected server error. Details are logged, never returned.

Gold

GET /pre/gold/calendar
gold

The calendar, plus direction and abnormality

https://api.earningspy.ai/v1/pre/gold/calendar

Bronze features, the silver quality read, and the gold model’s output on top.

Added on this tier:

Column
GOLD_IS_ABNORMAL_PROBProbability the event produces an abnormal move
GOLD_IS_ABNORMAL_QUINTQuintile bucket of that probability
GOLD_DIRECTION_PROBProbability of direction, conditional on abnormality
GOLD_DIRECTION_QUINTQuintile bucket of direction
GOLD_CONVICTIONA blend of direction probability and the event score
GOLD_SECTOR_ABN_RANKSector abnormal rate from the training window

GOLD_CONVICTION is empty for most events by design. The model declines to take a view on setups it does not recognise, and a model with an opinion about everything would be less useful, not more.

The targets come with it. Gold also returns the outcome columns: abnormal returns at each horizon, the expected-return baselines behind them, and the realized labels. For a completed event those sit on the same row as the features, aligned to one event. Point y at a column and start training.

Parameters

Name In Description
view query

A named view: the fields to return, their order, any row filter and the sort, all decided server-side.

Prefer this over fields. A view is a surface we define and can change in one place; a field list is one a caller assembled and now depends on. Filtered variants are separate view names (sp500) rather than filter parameters.

See the views reference (#views) for the available views and their columns.

calendar · sp500

period query

A named window over EARNINGS_DATE, always relative to the request date. There is no anchor parameter - use start/end for an arbitrary window.

PeriodWindow
daytoday
weekMonday to Sunday of the current week
next_weekMonday to Sunday of the FOLLOWING week
month_to_datethe 1st of the current month through today
monthfirst to last day of the current month
quarterthe current calendar quarter
biquarterthe current quarter and the previous one

biquarter is the default because the store spans years: without a window, a bare request would page from the oldest data it holds.

In Q1 the biquarter window crosses a year boundary - in March it runs 1 Oct of the prior year to 31 Mar.

day · week · next_week · month_to_date · month · quarter · biquarter

Default biquarter

sort query

Direction of the EARNINGS_DATE ordering. TICKER is always ascending within a date, in both directions.

Ascending is the default because these are upcoming events - soonest first is the calendar reading. Use desc for looking backwards.

asc · desc

Default asc

limit query
Rows per page.

Default 100

cursor query
Opaque pagination token from a previous response’s next_cursor. Treat as opaque - the encoding is not part of the contract. A malformed cursor returns 400.

Example responses

First page of a week

{
  "data": [
    {
      "TICKER": "ACME",
      "EARNINGS_DATE": "2026-07-14T00:00:00",
      "EPS_CHANGE": 0.328,
      "SALES_CHANGE": 0.118,
      "QUALITY_QUINT": 4,
      "GOLD_IS_ABNORMAL_PROB": 0.71,
      "GOLD_DIRECTION_PROB": 0.66,
      "GOLD_DIRECTION_QUINT": 4,
      "GOLD_CONVICTION": 0.58,
      "CAR_3": 0.021,
      "CAR_30": 0.058,
      "CAR_60": 0.071,
      "EXP.RET_30": 0.012,
      "REALIZED_DIRECTION": 1,
      "REALIZED_IS_ABNORMAL": 1,
      "REALIZED_PERSISTENCE": 0.83
    }
  ],
  "next_cursor": null,
  "count": 1,
  "layers_included": [
    "bronze",
    "silver",
    "gold"
  ],
  "sort": "asc"
}

Final page - next_cursor is null

{
  "data": [],
  "next_cursor": null,
  "count": 0,
  "price_as_of": null,
  "layers_included": [
    "bronze"
  ],
  "window": {
    "start": "2026-04-01",
    "end": "2026-09-30",
    "period": "biquarter"
  },
  "sort": "asc"
}

Status codes

  • 200 A page of earnings events
  • 400 A parameter was rejected: an unknown column in fields, a malformed cursor, an out-of-range limit, or an impossible date.
  • 401 No credential presented, or the key is unknown.
  • 403 The key is revoked or expired, or its tier does not include the layer being requested.
  • 429 The key’s usage-plan throttle or monthly quota was exceeded. Back off and retry; quotas reset monthly.
  • 500 Unexpected server error. Details are logged, never returned.
GET /pre/gold/ticker/{ticker}
gold

One ticker's history, plus the model's read

https://api.earningspy.ai/v1/pre/gold/ticker/{ticker}

One ticker’s full event history with the gold model’s output joined on, newest first.

This is the route where features and targets land together: the pre-event snapshot, the model’s read, and for events that have already resolved, the abnormal returns at 3, 30 and 60 days and the realized labels. One row per event, no assembly required.

Parameters

Name In Description
ticker required path

Uppercase symbol. Dots and hyphens are supported (BRK.B, RDS-A). Lowercase input is upper-cased.

NA is a valid ticker (Nano Labs Ltd), not a null.

view query

A named view, exactly as on the calendar route - the same view gives you an upcoming event and this ticker’s history in the same shape, so a client can render both from one definition.

A view’s row filters are not applied here: the path parameter already fixes which rows come back, and an index filter would make the same ticker appear or vanish depending on the view.

See the views reference (#views) for the available views.

calendar · sp500

limit query

Cap the number of events returned. Omit for the ticker’s full history.

Useful when only the recent past matters - limit=6 gives the upcoming event plus the previous five, which is what an earnings history panel typically renders.

Status codes

  • 200 The ticker’s upcoming events
  • 400 The ticker is malformed - wrong characters or too long. Returned before any query runs.
  • 401 No credential presented, or the key is unknown.
  • 403 The key is revoked or expired, or its tier does not include the layer being requested.
  • 404 No bronze rows for this ticker
  • 429 The key’s usage-plan throttle or monthly quota was exceeded. Back off and retry; quotas reset monthly.
  • 500 Unexpected server error. Details are logged, never returned.

Meta

GET /meta/schema
bronze key

The column catalog

https://api.earningspy.ai/v1/meta/schema

Every servable bronze column with its type and freshness class. This is the authoritative list ?fields= is validated against - read it rather than hard-coding column names, because the pipeline regenerates it on every run.

Authenticated like every other route: the column catalog is a product surface, not public documentation.

Example response

The column contract

{
  "primary_key": [
    "EARNINGS_DATE",
    "TICKER"
  ],
  "column_count": 168,
  "columns": [
    {
      "name": "EARNINGS_DATE",
      "dtype": "datetime64[ns]",
      "freshness_class": "filing"
    },
    {
      "name": "TICKER",
      "dtype": "object",
      "freshness_class": "filing"
    },
    {
      "name": "PRICE_AS_OF",
      "dtype": "datetime64[ns, UTC]",
      "freshness_class": "price"
    }
  ]
}

Status codes

  • 200 The column contract
  • 401 No credential presented, or the key is unknown.
  • 403 The key is revoked or expired, or its tier does not include the layer being requested.
  • 429 The key’s usage-plan throttle or monthly quota was exceeded. Back off and retry; quotas reset monthly.
  • 500 Unexpected server error. Details are logged, never returned.

Errors

Every error returns a JSON body with a single error key. The message is human-readable and never contains internal detail.

400 BadRequest

A parameter was rejected: an unknown column in fields, a malformed cursor, an out-of-range limit, or an impossible date.

403 Forbidden

The key is revoked or expired, or its tier does not include the layer being requested.

{ "error": "tier does not include 'gold' data" }
503 FrameUnavailable

This deployment is not configured to serve the requested frame - its parquet and schema keys are absent from the function’s environment.

A 503 rather than a 404 or a 500: the route exists and the request was valid, but the data behind it cannot be reached right now. It is a configuration state, not a caller error, and it is transient in the sense that a deploy fixes it.

This is why the post-earnings keys are optional at runtime - a deployment missing them serves the pre-earnings routes normally rather than failing at import and taking every endpoint down.

500 InternalError

Unexpected server error. Details are logged, never returned.

429 TooManyRequests

The key’s usage-plan throttle or monthly quota was exceeded. Back off and retry; quotas reset monthly.

401 Unauthorized

No credential presented, or the key is unknown.

Ready to build on it?

The API is in private beta. Leave your email and we will be in touch when keys open up.