Modeling

The modeling routes return derived analytics — margins, returns, growth rates, TTM aggregates — computed against a fixed point-in-time snapshot rather than against today's data. That anchoring is the point: a ratio is only reproducible if the statement versions behind it are pinned.

Every route here keys off company_id, the UUID, not the CIK. Get one from /v1/identity/resolve.

Snapshot first

Every modeling read requires as_of, and that date must name a snapshot that already exists. Without one the route returns 404:

{
  "error": {
    "code": "NOT_FOUND",
    "http_status": 404,
    "message": "As-of snapshot not found for modeling ratios query.",
    "details": {},
    "trace_id": "…"
  }
}

This is the single most common surprise on these routes. A 404 here does not mean the company is uncovered or the date is invalid — it means nobody has built a snapshot for that date yet. Build one, then read.

The modeling reads and snapshot builds are both available from Growth, so a Growth key can build the snapshot it then reads.

Build a snapshot

POST /v1/modeling/universe/pit-snapshot computes the snapshot and returns the metric rows it produced. It takes up to 50 CIKs at a time:

curl -X POST "https://api.arche.fi/v1/modeling/universe/pit-snapshot" \
  -H "X-Api-Key: $ARCHE_API_KEY" \
  -H "X-Request-ID: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "ciks": ["0000320193", "0000789019"],
    "as_of_date": "2025-06-30",
    "statement_types": ["INCOME_STATEMENT"],
    "metrics_bundle": "tier1"
  }'

All four fields are required. metrics_bundle is all or tier1.

{
  "page": 1,
  "page_size": 50,
  "total": 12,
  "items": [
    {
      "cik": "0000320193",
      "statement_type": "INCOME_STATEMENT",
      "metric_name": "BASIC_EPS",
      "metric_value": "1.650000",
      "statement_date": "2025-03-29",
      "fiscal_year": 2025,
      "fiscal_period": "Q2",
      "_meta": {
        "snapshot_id": "2128682cf89d4e3a364918c482ca249c2d888b066de903f559bb67cf0b0e48cb",
        "as_of_snapshot_id": "48e9af60-bab1-4fb5-9027-704bdfc9e0e6",
        "snapshot_hash": "26f836b84110370870b2a9239d058669f816c3fefc09431e329569508ea48156",
        "statement_version_id": "003c8265-6a73-4a41-bf80-3b78e7d8ddca",
        "strategy_id": "accepted_at_fallback_filed_at_eod_utc_source_preferred_own_period_acceptance_ordered_v4"
      }
    }
  ]
}

A large build can answer 202 instead of 200, meaning the snapshot is still being computed. Retry the read rather than the build.

Every row carries the statement_version_id it came from and the strategy_id that selected it, so a figure in a model traces back to a specific filed statement. Record as_of_snapshot_id alongside your model run — it is what makes the run reproducible.

Model periods

The normalized per-period rows the other routes are computed from. This is the route to read when you want the inputs rather than the derived figures:

curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/periods?as_of=2025-06-30&page_size=50" \
  -H "X-Api-Key: $ARCHE_API_KEY" \
  -H "X-Request-ID: $(uuidgen)" \
  -H "Accept: application/json"

Each row names its statement identity, its version_sequence, the statement_version_id behind it, and normalization_provenance_id — so you can carry a model input back to its provenance.

/v1/modeling/companies/{company_id}/multi-year returns the same rows arranged for multi-year comparison, with the valuation inputs computed from each one. A period row holds one statement, so an income-statement row has no balance sheet inputs; each null input names why in unavailable_metrics.

Ratios

Margins, returns and capital-structure ratios for each period in the snapshot:

curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/ratios?as_of=2025-06-30&page_size=50" \
  -H "X-Api-Key: $ARCHE_API_KEY" \
  -H "X-Request-ID: $(uuidgen)" \
  -H "Accept: application/json"

Representative response:

{
  "page": 1,
  "page_size": 50,
  "total": 67,
  "items": [
    {
      "model_ratio_id": "8baeabfd-6ef4-4e07-952a-73e0f8b64dbe",
      "company_id": "a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a",
      "cik": "0000320193",
      "statement_type": "INCOME_STATEMENT",
      "accounting_standard": "US_GAAP",
      "statement_date": "2025-03-29",
      "fiscal_year": 2025,
      "fiscal_period": "Q2",
      "currency": "USD",
      "version_sequence": 6,
      "as_of_snapshot_id": "48e9af60-bab1-4fb5-9027-704bdfc9e0e6",
      "as_of_timestamp": "2025-06-30T23:59:59Z",
      "snapshot_hash": "26f836b84110370870b2a9239d058669f816c3fefc09431e329569508ea48156",
      "metrics": {
        "GROSS_MARGIN": "0.470506",
        "OPERATING_MARGIN": "0.310291",
        "NET_MARGIN": "0.259860",
        "ROE": "0.370980",
        "ROA": "0.074811",
        "ROIC": "0.191197",
        "DEBT_TO_EQUITY": "1.380382",
        "CURRENT_RATIO": "0.820870",
        "QUICK_RATIO": "0.516245",
        "BOOK_VALUE_PER_SHARE": "4.436465",
        "DSO": "24.667205",
        "DPO": "96.477462",
        "INVENTORY_TURNOVER": "8.054235",
        "CASH_CONVERSION_CYCLE": "-60.636012"
      },
      "unavailable_metrics": [
        {
          "metric": "INTEREST_COVERAGE",
          "reason": "MISSING_INPUT",
          "missing_inputs": ["INTEREST_EXPENSE"]
        }
      ]
    }
  ]
}

Ratios live in metrics as decimal strings, not floats — "0.470506" is a 47.05% gross margin. Keep them as strings or decimals; parsing to binary floating point is how a reported figure stops reconciling.

Growth

Year-over-year, quarter-over-quarter and CAGR figures, with cagr_years naming the window:

curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/growth?as_of=2025-06-30&page_size=50" \
  -H "X-Api-Key: $ARCHE_API_KEY" \
  -H "X-Request-ID: $(uuidgen)" \
  -H "Accept: application/json"

Rows carry REVENUE_GROWTH_YOY, REVENUE_GROWTH_QOQ, NET_INCOME_GROWTH_YOY and similar in metrics, in the same decimal-string form.

Trailing twelve months

TTM aggregates, with the window stated on every row so a figure is never ambiguous about what it sums:

curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/ttm?as_of=2025-06-30&page_size=50" \
  -H "X-Api-Key: $ARCHE_API_KEY" \
  -H "X-Request-ID: $(uuidgen)" \
  -H "Accept: application/json"

Each row carries ttm_window_start and ttm_window_end beside the summed metrics. Check the window before comparing two companies: a TTM figure that ends on a different date is a different measurement.

A line is totalled only when every quarter in the window reports it. A line some quarter omits is listed in unavailable_metrics as INCOMPLETE_TRAILING_YEAR rather than summed over fewer than four quarters.

When a metric can't be computed

Ratio, growth and multi-year rows never leave a metric silently absent. Each metric the route computes that a row does not carry is listed in unavailable_metrics, with the reason and, when an unreported input is the cause, the canonical lines that were missing. TTM rows list the lines their window reports but cannot total. In the ratios example above, Apple's income statement for the quarter has no interest expense line, so interest coverage has no divisor:

"unavailable_metrics": [
  {
    "metric": "INTEREST_COVERAGE",
    "reason": "MISSING_INPUT",
    "missing_inputs": ["INTEREST_EXPENSE"]
  }
]

reason is one of a fixed set, so code can branch on it:

reasonMeaning
NOT_REPORTEDThe filing does not report the line for the period.
NOT_ON_STATEMENTThe value comes from a different statement than the row. Read that statement's row for the period.
MISSING_INPUTAn input the formula needs is not reported for the period. missing_inputs names it.
DENOMINATOR_ZEROEvery input is present, but the divisor is zero.
NON_POSITIVE_VALUEThe formula is defined only for positive values, such as a CAGR base or a share count, and one is zero or negative.
NO_COMPARABLE_PERIODA growth rate has no earlier period of the same length to compare against.
NOT_APPLICABLE_TO_PERIODThe metric is not defined for this kind of period, such as quarter-over-quarter growth on an annual row.
INCOMPLETE_TRAILING_YEARNot every quarter of the trailing twelve months reports the line.
PENDING_REBUILDThe inputs now support a value that the stored row predates. The next model build fills it.

On ratios and growth, every metric in the route's set is in metrics or in unavailable_metrics. On multi-year, every null in valuation_inputs has an entry. Model periods carry the statement as reported, so a line absent from their metrics was not reported.

Coverage varies by company

A snapshot covering a company does not guarantee every derived table has rows for it. In the same snapshot above, Apple returns 67 ratio rows and Microsoft returns none, while both return model periods.

An empty items with total: 0 here means the derived rows were not computed for that company, not that the company is uncovered. Read /v1/modeling/companies/{company_id}/periods to confirm the inputs exist. Within a row that does exist, a missing ratio is never zero: its reason is in unavailable_metrics.

Was this page helpful?