hub-equity: Python SDK
Typed Python client for the Hub-Equity financial-data API: machine-readable XBRL / iXBRL from primary filings, SEC EDGAR (US) and ESEF (Europe), normalized into canonical financial concepts, with the provenance preserved.
What sets the data apart, and what this SDK exposes:
- Source-linked. Every value carries the filing it came from (
form_type,filing_date, a viewer URL). You can trace any number back to the filer's document, not a black box that silently rewrites as-filed history. - XBRL-native. Calculation linkbase (with filer weights and verifier-corrected weights), dimensional structure, and the raw filer-declared facts, not just flattened labels.
- Restatement-aware. Per-concept deltas across 10-K/A amendments and across silent restatements, the figures a later filing reprinted differently with no amendment filed.
- Cross-issuer, calendar-aligned.
comparematches periods by calendar year (Bloomberg / FactSet convention), so Apple (Sep FYE) and a Dec-FYE peer line up.
The SDK is a thin, typed wrapper over the public REST API: it ships no data and no secrets; you bring your own API key. Full API reference: https://docs.hub-equity.com. It covers a subset of the API: fact decomposition, segments, the amendment summary, cross-period comparison, roll-up, currency conversion, the entity quality grade, validation results, extension concepts and the hub catalog are read over REST.
Maturity. The data engine and public REST API behind this SDK run in production and power Hub-Equity's own chat. This PyPI package is a newly published client for that API; the surface is stable (Semantic Versioning, 2.x), but as a distributed package it is fresh, hence the Beta classifier.
Install
pip install hub-equity
# Optional pandas helpers:
pip install 'hub-equity[pandas]'
Python 3.10+. Runtime deps: httpx, pydantic.
Authentication
from hub_equity import HubEquity
# Explicit key
client = HubEquity(api_key="INSERT_YOUR_API_KEY")
# Or set HUBEQUITY_API_KEY env var (recommended for notebooks)
client = HubEquity()
A key is required: the client refuses to start without one (ValueError), since the API answers 401 AUTH_REQUIRED to a call without a key. A Free key takes a minute at https://hub-equity.com/settings/api-keys (shown once, store it securely).
Set HUBEQUITY_API_URL to point the client at a non-default host (defaults to
https://api.hub-equity.com).
Entity arguments accept a Hub-Equity UUID, a ticker (e.g. "AAPL") or
TICKER.MIC (e.g. "MC.XPAR") on every entity method, compare included. An
ambiguous ticker answers 409 with the suffixes that disambiguate it.
Quickstart
from hub_equity import HubEquity
with HubEquity() as client:
fin = client.get_financials("MC.XPAR", years=5) # LVMH, every statement
revenue = fin.metric("REVENUE")
print([(v.fiscal_year, v.display_value) for v in revenue.values])
rev = client.get_metric_history("AAPL", "REVENUE", years=5)
print(f"{rev.entity_name}: {rev.hub_label} (CAGR {rev.cagr_pct:.1f}%)")
for p in rev.points:
print(f" FY{p.fiscal_year}: {p.value:,.0f} {p.currency}"
f" ({p.yoy_growth_pct:+.1f}% YoY) ← {p.source.form_type} {p.source.filing_date}")
# p.source.viewer_url / p.source.external_url -> trace back to the filing
Methods
| Area | Method | Returns |
|---|---|---|
| Entities | find_entity(query) |
FindEntityResult: up to 5 ranked matches (name, ticker, TICKER.MIC, CIK) |
search_entities(q, ...), iter_entities(q, ...) |
SearchResponse / generator of SearchResult |
|
get_entity_profile(entity) |
EntityProfile: identifiers, DEI, auditor, latest filings |
|
| Statements | get_financials(entity, *, years=3, category='all', currency=None, end_year=None) |
Financials: periods + metrics; cite MetricValue.display_value |
| Facts | get_fact(entity, code, fiscal_year, fiscal_period_type='FY') |
Fact: one value + its source filing (HubEquityError 404 when none is served) |
| Screener | screen_companies(*, country, sector, min_revenue, ..., sort_by, limit=20) |
ScreenCompaniesResult (Free: 20 rows, no quality filter) |
| Time-series | get_metric_history(entity, code, *, years=5, period_type='FY') |
MetricHistory: values + YoY + CAGR |
| As-of | get_metric_as_of(entity, code, as_of, *, years=5, period_type='FY') |
AsOfResponse: series read from the filings filed on or before a date (beta: can differ from the served values) |
| Compare | compare(entity_ids, hub_concept_codes, fiscal_year, fiscal_period_type='FY') |
CompareEntitiesResult: N×M matrix |
| Calc linkbase | get_calculation_sections(filing_id) |
CalculationSectionsResponse: calc roles |
get_calculation_tree(filing_id, link_role) |
CalculationTree: edges + weights |
|
| Dimensions | get_entity_dimensions(entity) |
EntityDimensionsResponse: axes + members |
| Raw facts | get_filing_facts(filing_id, *, section, concept, is_extension, limit, offset), iter_filing_facts(filing_id, *, ..., per_page=200) |
RawFactsResponse / generator of RawFact (Free: structure without the values) |
| Restatements | get_restatement_diff(entity, *, fiscal_year, hub_concept_code, min_diff_pct=0.01, kind, source='amendments') |
AmendmentDiffResult (source='comparatives': silent restatements) |
| Quality | get_filing_quality_grade(filing_id) |
QualityGradeResponse: A-F + 4 drivers |
| Concepts | search_concepts(q, *, taxonomy, category, limit, offset, cursor) |
ConceptSearchResponse |
iter_concepts(q, *, taxonomy, category, page_size=100) |
generator of ConceptSummary |
|
get_concept(hub_concept_code) |
ConceptDetail: forward catalog entry (label, description, category, taxonomy scope) |
|
| Exports (Team, Enterprise) | create_export(entity_ids, *, years=3, category='all', xbrl='none'), list_exports, get_export(job_id) |
ExportJob |
download_export(job_id, path=None) |
zip bytes, or the Path written |
|
| Webhooks (Enterprise) | create_webhook(name, url, *, events, entity_ids), rotate_webhook_secret(id) |
WebhookCreated (secret shown once) |
list_webhooks, get_webhook(id), update_webhook(id, ...), delete_webhook(id) |
Webhook |
|
list_webhook_deliveries(id, *, limit=50) |
list[WebhookDelivery] |
|
send_webhook_test(id), redeliver_webhook_delivery(id, delivery_id) |
WebhookAttempt |
Every method returns a typed pydantic model (autocomplete + validation), with
extra="allow" so a newer API never breaks an older SDK.
Compare issuers (calendar-year aligned, premium)
matrix = client.compare(
entity_ids=["AAPL", "MSFT"],
hub_concept_codes=["REVENUE", "NET_INCOME"],
fiscal_year=2024,
)
for row in matrix.rows:
cell = row.cells.get("REVENUE")
if cell:
print(f"{row.name} ({row.ticker}): {cell.value:,.0f} {cell.currency}"
f" [{cell.source.form_type if cell.source else ''}]")
Audit a number to its filing
# Drill to the raw filer-declared facts (qname, unit, decimals, dimensions).
# The values come on a paid plan; a Free key reads the structure, values null.
facts = client.get_filing_facts("<filing-uuid>", concept="Revenue", limit=50)
for f in facts.facts:
print(f.concept_qname, f.value_numeric, f.unit, f.dimensions)
pandas
import pandas as pd
hist = client.get_metric_history("MSFT", "REVENUE", years=10)
df = pd.DataFrame([p.model_dump() for p in hist.points])
df["source_form"] = [p.source.form_type for p in hist.points]
Background exports (Team, Enterprise)
import time
job = client.create_export(["AAPL", "MC.XPAR"], years=5)
while job.status in ("pending", "running"):
time.sleep(10)
job = client.get_export(job.id)
if job.status == "ready":
client.download_export(job.id, path="export.zip") # follows the 307 to the signed URL
else:
print(job.error)
Webhooks (Enterprise, SDK 2.0.0)
hook = client.create_webhook("prod", "https://example.com/hub-equity")
SECRET = hook.secret # shown once: store it now
client.send_webhook_test(hook.id) # a signed ping, answered with your endpoint's status
client.list_webhook_deliveries(hook.id) # what was sent, skipped or retried
A subscription created in /settings/webhooks (or create_webhook) delivers signed
POSTs to your endpoint: filing.ingested when a filing's values are served,
restatement.material when a later filing restates a value you may have used.
Verify the raw body, not a re-serialised parse:
from hub_equity import verify_webhook_signature
@app.post("/hub-equity")
async def receive(request):
raw = await request.body()
if not verify_webhook_signature(SECRET, request.headers, raw):
return Response(status_code=401)
event = json.loads(raw) # {"id", "event", "created_at", "data"}
...
return Response(status_code=204) # answer 2xx within 10 s
The check is constant-time, rejects a timestamp more than five minutes from your
clock, and accepts either signature during the 24 hours that follow a secret
rotation (v1=<new>,v1=<old>). Deduplicate on the X-Hub-Equity-Delivery header:
delivery is at-least-once.
Reliability
- Retries
429(rate limit) +5xx(server) with exponential backoff (3 retries, base 1 s, cap 10 s); honorsRetry-Afterwhen present. - After exhausting retries on
429, raisesRateLimitError(subclass ofHubEquityError) with the lastretry_afterattached. A429whoseRetry-Afterexceeds the 10 s cap (a daily or monthly allowance used up) is raised at once, not retried. - Other 4xx raise
HubEquityErrorimmediately (no retries), with.status_code. - The exception message carries the API's own
error.codeanderror.message(e.g. which suffix disambiguates a ticker, when a quota resets).
from hub_equity import HubEquity, HubEquityError, RateLimitError
with HubEquity() as client:
try:
rev = client.get_metric_history("AAPL", "REVENUE")
except RateLimitError as exc:
print(f"Rate-limited, retry after {exc.retry_after}s")
except HubEquityError as exc:
print(f"{exc.status_code}: {exc}")
Rate limits
| Caller | Per minute |
|---|---|
| Free key | 120 |
| Builder key | 300 |
| Team key | 600 |
| Enterprise key | 1 000 |
Bucketed per API key.
Volume allowances are counted per workspace on API keys and refuse with a
429 (DAILY_QUOTA_EXCEEDED / MONTHLY_QUOTA_EXCEEDED, headers X-Quota-*,
resets_on in the body): Free 200 calls a day and 5 000 a month, Builder
50 000 a month, Team 500 000 a month, Enterprise unlimited.
Premium endpoints (compare, compare-filings, as-of, dimensions,
restatement diff, calculation tree, validation results,
extension concepts) require a paid key (403 SCOPE_MISSING on a Free key,
401 without a key). A Free key is also capped on /decomposition (depth=1)
and on /companies/screen (20 results, no min_quality_grade), gets the axes of
/segments/{code} without their members, and reads the raw facts of
/filings/{id}/facts without their values.
Versioning
The SDK follows Semantic Versioning: a breaking change ships only in a major
version. Pin a major version in production (hub-equity>=2,<3). Response models
use extra="allow", so additive API changes ship in minor releases without
breaking older SDK installs.
2.0.0 breaks two things, because the API itself changed: HubEquity()
without a key raises ValueError (the API refuses keyless calls, so the
api_key="" examples of 1.0 and 1.1 no longer run), and
CompareCell.extraction_method is removed. See the CHANGELOG.
License
Apache-2.0. See LICENSE.
Metadata
Release files for hub-equity 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hub_equity-2.0.0.tar.gz | 43.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hub_equity-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 71.4 kB
Release files / hub_equity-2.0.0.tar.gz
| Download URL | hub_equity-2.0.0.tar.gz |
|---|---|
| Size | 43.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d3eee6c9e210d4bffa678c35f9a84f3a25773f86e6db881cdb8da65f88aeadf3
|
|
BLAKE2b-256 checksum How to use checksums |
501304d2bcbfce2920b63d30e9d2f9d5ff3a0141080a0f1ef68bf1f34a4ed454
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / hub_equity-2.0.0-py3-none-any.whl
| Download URL | hub_equity-2.0.0-py3-none-any.whl |
|---|---|
| Size | 28.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d257aa3de36a05ff047b398df71ce8d40526f16b422188f6b724a15dbc6b5cc5
|
|
BLAKE2b-256 checksum How to use checksums |
bb8793ceca220920eadf5b15c64baaded8885665f1d8a3f58e81222adc1dbe29
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency log