# Factor API documentation | SGG Research

Source: https://sggresearch.com/api-docs

[Skip to documentation](https://sggresearch.com/api-docs#api-content)

Developer documentation / API v1

# Factor data. Ready to integrate.

Retrieve ECPND and ECQDI through one consistent interface. Each request delivers a complete score universe with its calculation version, publication time and data status.

[Download OpenAPI](https://sggresearch.com/api-docs/openapi.json)[Manage API keys](https://sggresearch.com/account)OpenAPI 3.1 · contract 1.2.0

Base URL https://sggresearch.com

01

## Your first request

Create a key in [your account](https://sggresearch.com/account) after activating a factor subscription. Set the base URL and keep the key in an environment variable or secret manager. Use the same environment for the website, key and API.

Environment

```
export SGG_BASE_URL="https://sggresearch.com"
# Set SGG_API_KEY to the key created in your account.
```

GET /v1/scores?factor=ecpnd

```
curl --fail-with-body --silent --show-error --max-time 20 \
  "$SGG_BASE_URL/v1/scores?factor=ecpnd" \
  --header "Authorization: Bearer $SGG_API_KEY"
```

These examples make one request and do not retry automatically. The Python and Node.js examples use a strict current-only policy; choose a data-status policy appropriate to your research.

1. Retrieve the full snapshot for each licensed factor you need.

2. Check its status, cutoff and version before using the data.

3. Save the original response, join by ticker and apply your own model.

Customer data endpoints
Method & path: [GET /v1/factors](https://sggresearch.com/api-docs#authentication) | Purpose: List the factors your key can access.
Method & path: [GET /v1/scores](https://sggresearch.com/api-docs#scores) | Purpose: Retrieve a complete published score set.
Method & path: [GET /v1/scores/history](https://sggresearch.com/api-docs#history) | Purpose: Read historical coverage and the file checksum.
Method & path: [GET /v1/scores/history.csv.gz](https://sggresearch.com/api-docs#history-file) | Purpose: Download historical scores; HEAD checks file headers.

02

## Authentication and factor access

Every data endpoint requires Authorization: Bearer <key>. Production keys begin with sgg_live_; sandbox keys begin with sgg_test_. Keep keys in a server or research environment. Do not put them in URLs, notebooks you share publicly or browser-side application code.

A key inherits the active factor subscriptions on its account. ECPND and ECQDI are licensed separately. If you subscribe to both, use the same key for two requests and set factor explicitly. Omitting it always selects ECPND, including on an ECQDI-only account.

Factor: ecpnd | Delivered field: ecpnd_score | Research definition: Earnings call participation network and dynamics. [Read the paper](https://sggresearch.com/whitepaper/ecpnd).
Factor: ecqdi | Delivered field: ecqdi_score | Research definition: Earnings call quantitative dynamics and intensity. [Read the paper](https://sggresearch.com/whitepaper/ecqdi).

### GET /v1/factors

No query parameters. Returns only the currently licensed factors, together with their score and history URLs. No active subscription returns 403 subscription_required; an unlicensed factor request returns 403 factor_subscription_required.

Discover access

```
curl --fail-with-body --silent --show-error --max-time 20 \
  "$SGG_BASE_URL/v1/factors" \
  --header "Authorization: Bearer $SGG_API_KEY"
```

Example response - account with both factors

```
{
  "factors": [
    {
      "factor": "ecpnd",
      "score_name": "ecpnd_score",
      "factor_version": "ecpnd-v1",
      "scores": "/v1/scores?factor=ecpnd",
      "history": "/v1/scores/history?factor=ecpnd"
    },
    {
      "factor": "ecqdi",
      "score_name": "ecqdi_score",
      "factor_version": "ecqdi-v1",
      "scores": "/v1/scores?factor=ecqdi",
      "history": "/v1/scores/history?factor=ecqdi"
    }
  ]
}
```

Use a separate key for each integration. A key is shown once, stored as a hash and can be revoked in your account. Up to 10 active keys are supported. All keys share account limits. Website sessions and Google sign-in tokens do not authenticate the data API.

03

## Current scores

### GET /v1/scores

Without filters, the response includes every ticker in that factor’s published research universe, sorted alphabetically. There is no pagination or separate call feed to merge. Coverage can differ by factor and over time; join the returned tickers to your own investment universe.

All query parameters are optional
Parameter: factor | Format / default: ecpnd or ecqdi Default: ecpnd | Behaviour: Selects the licensed factor and score field.
Parameter: ticker | Format / default: Uppercase, up to 16 characters | Behaviour: Returns one ticker. Letters, digits, dots and hyphens are accepted. Unknown tickers return 404.
Parameter: format | Format / default: json or csv Default: json | Behaviour: Selects the representation. The Accept header does not override it.
Parameter: at | Format / default: RFC 3339 timestamp | Behaviour: Selects the latest check recorded by that time. Cannot be future-dated or combined with snapshot_id.
Parameter: snapshot_id | Format / default: UUID from an earlier response | Behaviour: Retrieves that factor’s saved score set and original publication metadata.

Parameter names and values are case-sensitive. Unknown query fields and repeated scalar parameters are rejected. Do not send a request body. For reconstructed daily history, use the historical download rather than a date parameter.

200 / application/json / illustrative three-row universe

```
{
  "factor": "ecpnd",
  "score_name": "ecpnd_score",
  "universe": "US research universe",
  "factor_version": "ecpnd-v1",
  "snapshot_id": "d5c7355c-2b9f-4421-8a13-538e70c2f6cd",
  "calculated_at": "2026-09-28T11:00:08.000Z",
  "last_checked_at": "2026-09-28T12:00:06.000Z",
  "data_through": "2026-09-28T12:00:00.000Z",
  "status": "current",
  "prices_through": "2026-09-25",
  "coverage": {
    "tickers": 3,
    "price_tickers_current": 3,
    "price_tickers_tracked": 3,
    "pending_calls": 0,
    "failed_calls": 0
  },
  "scores": [
    {
      "ticker": "AAPL",
      "ecpnd_score": 0.64
    },
    {
      "ticker": "MSFT",
      "ecpnd_score": null
    },
    {
      "ticker": "NVDA",
      "ecpnd_score": 0.51
    }
  ]
}
```

### Reading the score

Each row has exactly two fields: ticker and the chosen score name. Values range from 0 to 1; higher values are more favourable under the factor definition. They are research measurements, not probabilities or return forecasts. null means no eligible value is available. Preserve it: a numerical zero is valid.

ECPND can change as eligible participation history evolves, even without another call from the company. Its current participation record expires at 126 US market sessions. ECQDI carries the latest qualified call value for at most 90 calendar days; a later unusable call does not extend that expiry. A day without a call is therefore not automatically a missing-score day.

### CSV and coverage

format=csv returns ticker,ecpnd_score or ticker,ecqdi_score, with missing scores left blank. Publication, check, cutoff and status remain available in the X-Factor-* headers. Current CSV files have no daily date column; preserve the headers alongside the file.

One ECQDI ticker as CSV

```
curl --fail-with-body --silent --show-error --max-time 20 --get \
  "$SGG_BASE_URL/v1/scores" \
  --header "Authorization: Bearer $SGG_API_KEY" \
  --data-urlencode "factor=ecqdi" \
  --data-urlencode "ticker=AAPL" \
  --data-urlencode "format=csv"
```

Coverage fields and their scope

Field: tickers | Meaning: Rows in the complete snapshot. A ticker filter does not reduce this count.
Field: price_tickers_current price_tickers_tracked | Meaning: Market-data coverage at the check. This tracked panel can differ from the score universe. Null means unavailable.
Field: pending_calls failed_calls | Meaning: Source-update queue counts. These are not counts of missing scores.
Field: scored_tickers | Meaning: ECQDI only: non-null values in the full set. For ECPND, count non-null values in the unfiltered response.
Field: pending_factor_calls | Meaning: ECQDI only: call records awaiting factor processing.

04

## Timing and data status

The service checks for changes hourly. It publishes a new score set when relevant inputs change; otherwise it reuses the existing set and records another check. New or corrected calls, newly eligible historical evidence and expiry can affect the result. Processing begins after transcript arrival; publication at the end of a call is not guaranteed.

Field: calculated_at | Meaning: UTC publication time of the numerical score set. It stays unchanged when a later check reuses that set.
Field: last_checked_at | Meaning: Recorded completion time of the selected check. With snapshot_id, this is the set’s original publication time.
Field: data_through | Meaning: Observation cutoff examined by that check. It does not certify that all expected inputs arrived.
Field: prices_through | Meaning: Latest stored benchmark price session. Check status and coverage for gaps elsewhere in the tracked panel.

In the example above, a set published at 11:00 is reused by the 12:00 check. A newer last_checked_at does not mean the numerical scores changed. All API timestamps are UTC; score dates in historical files are US market sessions.

### HTTP success and data freshness are separate

HTTP 200 means a published set was retrieved. Before using it, inspect status and data_through. The status reports one condition in the priority order below; inspect coverage for additional gaps.

Status: stale | What it means: For a latest request, the saved information cutoff is more than 75 minutes old.
Status: call_source_delayed | What it means: The source reconciliation was not current at the selected check.
Status: call_updates_pending | What it means: Call updates were pending or failed, including factor processing where reported.
Status: partial_market_data | What it means: The benchmark or tracked price panel was not fully current.
Status: current | What it means: The check and tracked inputs were current. Individual scores may still be missing.

During a delay, the last complete published set remains available with its recorded metadata. If no first set exists, the API returns 503 score_snapshot_preparing. Explicit historical requests retain their recorded input status; age alone does not mark them stale.

### A daily research workflow

Retrieve after the US reporting day, save the response and use only data published before your decision. Retrieve again if you need later arrivals before the next opening. Hourly checks do not require hourly portfolio changes. The published portfolio studies use their stated next-session eligibility and scheduled decisions; they are not tests of hourly execution. See the [ECPND](https://sggresearch.com/whitepaper/ecpnd) and [ECQDI](https://sggresearch.com/whitepaper/ecqdi) papers for each study’s rules.

05

## Retrieve the observation you used

Save the full JSON response for each research decision, including the factor, version, snapshot ID and timestamps. Numerical snapshots are immutable; new data and corrections produce later publications. Your active subscription is still required to retrieve a saved set.

- Latest: omit at and snapshot_id to retrieve the most recent completed check.

- As of a time: use at to select the latest completed check recorded no later than that timestamp.

- Exact score set: use snapshot_id to retrieve those numerical values and the set’s original publication metadata.

A snapshot ID belongs to one factor. It cannot retrieve another factor’s data. A set can be reused by many checks, so looking it up by ID will not reproduce the later check time you originally received. Keep the original response if that full decision record matters.

Recorded publication history

```
# Replace this example timestamp with your actual decision cutoff.
curl --fail-with-body --silent --show-error --max-time 20 --get \
  "$SGG_BASE_URL/v1/scores" \
  --header "Authorization: Bearer $SGG_API_KEY" \
  --data-urlencode "factor=ecpnd" \
  --data-urlencode "at=2026-09-28T12:15:00Z"
```

Use UTC timestamps ending in Z, or URL-encode timezone offsets. Do not combine at and snapshot_id. A future time returns 400; no stored publication by the requested time returns 404. Historical CSV dates are not live publication dates.

06

## Historical data, with a verifiable release

### GET /v1/scores/history

Read the metadata before downloading. It specifies the factor version, exact date range, row counts, missing values and SHA-256 checksum. Each factor has its own historical release; its end date does not advance with hourly live updates.

Historical release metadata

```
curl --fail-with-body --silent --show-error --max-time 20 \
  "$SGG_BASE_URL/v1/scores/history?factor=ecpnd" \
  --header "Authorization: Bearer $SGG_API_KEY"
```

Metadata: first_session / last_session | Use: Actual included dates. The nominal window is described by window_years, window_start and window_end.
Metadata: sessions / tickers / rows | Use: Distinct dates, distinct identifiers and total records. Coverage need not form a rectangular panel.
Metadata: scored_rows / missing_rows | Use: Available values and explicit missing observations.
Metadata: availability / universe | Use: Reconstruction convention and coverage definition for this release.
Metadata: download | Use: Authenticated relative URL. Retain its query parameters and use the same API origin.
Metadata: download_sha256 | Use: Checksum of the compressed file. source_sha256 is separate provenance, not the download checksum.

### GET / HEAD /v1/scores/history.csv.gz

The decompressed CSV contains date,ticker,ecpnd_score or date,ticker,ecqdi_score. Dates identify historical decision sessions under the release’s reconstruction convention. Scores are bounded from 0 to 1; missing fields are blank. These are reconstructed historical data, separate from prospectively published live snapshots.

1. Retrieve metadata for the required factor.

2. Download to a temporary file using the returned URL and your Bearer key.

3. Verify the compressed bytes against download_sha256.

4. Only then keep or decompress the file. Save the metadata with it.

Python example - stream, verify and save

Python 3.9+ / standard library

```
import hashlib
import json
import os
import time
from pathlib import Path
from urllib.parse import urlsplit
from urllib.request import Request, urlopen

base = os.environ["SGG_BASE_URL"].rstrip("/")
headers = {"Authorization": f"Bearer {os.environ['SGG_API_KEY']}"}
factor = "ecpnd"  # Or "ecqdi", with that subscription.
request = Request(f"{base}/v1/scores/history?factor={factor}", headers=headers)
with urlopen(request, timeout=20) as response:
    release = json.load(response)

download = release["download"]
parts = urlsplit(download)
if parts.scheme or parts.netloc or not download.startswith("/v1/scores/"):
    raise ValueError("Unexpected download location")

target = Path(f"sgg-research_{factor}_history.csv.gz")
temporary = target.with_suffix(".gz.part")
digest = hashlib.sha256()
deadline = time.monotonic() + 150
try:
    request = Request(base + download, headers=headers)
    with urlopen(request, timeout=35) as response, temporary.open("wb") as output:
        if response.headers.get_content_type() != "application/gzip":
            raise ValueError("Expected a gzip download")
        while chunk := response.read(1024 * 1024):
            if time.monotonic() > deadline:
                raise TimeoutError("Download deadline exceeded")
            digest.update(chunk)
            output.write(chunk)
    if digest.hexdigest() != release["download_sha256"]:
        raise ValueError("Checksum mismatch; retrieve metadata and retry")
    temporary.replace(target)
    target.with_suffix(".json").write_text(json.dumps(release), encoding="utf-8")
finally:
    temporary.unlink(missing_ok=True)
```

Transfers are limited to 120 seconds in total and 30 seconds without socket activity. Discard interrupted files; byte-range resume is not supported. If a release changes between metadata retrieval and download, the checksum will differ. Retrieve fresh metadata and repeat the download.

Check size and ETag without spending a download allowance

HEAD / file headers only

```
curl --head --silent --show-error --max-time 20 \
  "$SGG_BASE_URL/v1/scores/history.csv.gz?factor=ecpnd" \
  --header "Authorization: Bearer $SGG_API_KEY"
```

HEAD uses the ordinary request budget but no download allowance. It never returns a response body, including for errors. Inspect status, Retry-After and X-Request-Id.

07

## Predictable limits and conditional requests

Allowances are shared across your account’s keys, factors and historical website downloads. One full-universe request is preferable to one request per stock. The limits below are rendered from this environment’s service configuration.

Allowance: Data API requests | Configured limit: 120 per minute · 10,000 per UTC day
Allowance: Concurrent requests | Configured limit: 5 per account
Allowance: Historical-file downloads | Configured limit: 10 per UTC day · 1 at a time
Allowance: Ordinary request deadline | Configured limit: 15 seconds of server processing
Allowance: Historical transfer deadline | Configured limit: 120 seconds total · 30 seconds idle
Allowance: Request size | Configured limit: 2,048-byte URL · 16 KiB headers · 64 KiB body limit; no GET/HEAD body

Minute windows follow UTC clock minutes; daily windows reset at 00:00 UTC. Limits persist across restarts. Authenticated requests admitted to the data handler consume the request allowance, including conditional requests and subsequent errors. Rejected quota checks do not consume an additional allowance. Started file GETs also consume a download allowance, including 304 responses and interrupted transfers.

Response headers: X-RateLimit-* | Interpretation: Minute request budget.
Response headers: X-DailyLimit-* | Interpretation: UTC daily request budget.
Response headers: X-DownloadLimit-* | Interpretation: Daily file-download budget, where a download quota is checked.
Response headers: Retry-After | Interpretation: Minimum delay in seconds before retrying a limited or temporarily unavailable request.

Each budget family has Limit, Remaining and Reset suffixes. Reset is a Unix timestamp in seconds. Headers may be absent if the request was rejected before the relevant quota check. Account management and authentication also have separate protective limits.

### ETag and HTTP 304

Save the ETag from a successful score or file response. Send it in If-None-Match on the next GET to the same URL with the same account. A 304 response has no body: keep your cached data and read the returned metadata. Authentication, subscription access and request limits are still checked.

Score ETags include check metadata and freshness status, so a new ETag does not necessarily mean different score values. Use snapshot_id to identify the numerical set. An ETag is not a historical-file checksum.

Score-response headers

- X-Factor-Name: selected factor.

- X-Factor-Snapshot: numerical score-set ID.

- X-Factor-Calculated-At: score-set publication time.

- X-Factor-Last-Checked-At: selected check time.

- X-Factor-Data-Through: examined input cutoff.

- X-Factor-Status: data status.

The selected factor’s X-ECPND-* or X-ECQDI-* aliases are also present. Prefer the generic names for integrations supporting both products. These headers describe score responses, not historical-file downloads.

08

## Errors you can handle explicitly

Application errors return a machine-readable error, a readable message, a service-generated request_id and a retryable flag. The request ID also appears in X-Request-Id. Branch on codes or HTTP status, not message wording.

429 / illustrative application error

```
{
  "error": "rate_limit_exceeded",
  "message": "The request limit has been reached. Wait before retrying.",
  "request_id": "21c8a620-6710-48c0-a814-743a8eac8f7b",
  "retryable": true,
  "retry_after_seconds": 23
}
```

Status: 400 | Typical codes: invalid_request invalid_factor use_at_or_snapshot_id future_observation_requested | Next action: Correct the parameters. Validation errors can include details with the field and failed rule.
Status: 401 | Typical codes: unauthorized | Next action: Check the key, environment and Bearer header. Do not retry unchanged.
Status: 403 | Typical codes: factor_subscription_required subscription_required insufficient_scope | Next action: Check the selected factor and account access.
Status: 404 | Typical codes: ticker_not_found score_snapshot_not_available | Next action: Check the ticker, factor, saved ID or requested time.
Status: 405 | Typical codes: method_not_allowed | Next action: Use a method listed in the Allow header.
Status: 429 | Typical codes: rate_limit_exceeded daily_limit_exceeded download_limit_exceeded concurrent_request_limit | Next action: Wait at least Retry-After. Reduce concurrency or wait for the budget reset.
Status: 500 | Typical codes: internal_error | Next action: Preserve the request ID. Investigate persistent failures with support.
Status: 503 | Typical codes: service_unavailable request_timeout score_snapshot_preparing historical_export_preparing | Next action: Retry safe reads after the indicated delay, within your workflow deadline.

Transport limits may also produce 408, 413, 414, 415 or 431. Proxies and network failures can return non-JSON responses. Check status and Content-Type before parsing; HEAD responses have no body.

### A bounded retry policy

1. For normal GET requests, use a client timeout slightly longer than the 15-second server deadline; 20 seconds is suitable for the default configuration.

2. Retry transient connection failures and safe 429/503 reads with exponential backoff and random jitter. Never retry sooner than Retry-After.

3. Bound attempts, for example to five, and stop at your workflow deadline. A daily budget reset may be hours away.

4. Preserve the last accepted dataset separately. A failed request or partial download must never become an empty or zero-filled score set.

09

## Contract, reproducibility and support

The [OpenAPI 3.1 document](https://sggresearch.com/api-docs/openapi.json) defines request parameters, factor-specific response schemas, examples, headers and errors. It is public and can be imported into compatible API tools or client generators. Its server entry targets this environment; use development credentials only with the development API.

- API path version: /v1 identifies the interface.

- Contract version: 1.2.0 identifies this documentation edition.

- Factor version: factor_version identifies the calculation definition.

- Snapshot ID: snapshot_id identifies a saved numerical result.

Parse the fields your integration needs and tolerate additional metadata. Treat an unknown status conservatively until reviewed. Preserve missing values, source responses and historical-release checksums with your own model inputs.

For [integration support](https://sggresearch.com/terms#contact), include the environment, endpoint, approximate UTC time, HTTP status and request ID. Never send an API key or password. Your [dashboard](https://sggresearch.com/account) provides key management, recent request activity, subscriptions and authorized historical downloads.

This API provides data analytics and research inputs. This product and service are not financial or investment advice. Independently verify data consistency and suitability before using it in your models or decisions. [Service terms](https://sggresearch.com/terms#research-use).
