Skip to content

Latest commit

 

History

History
356 lines (288 loc) · 16.3 KB

API.md

File metadata and controls

356 lines (288 loc) · 16.3 KB

API reference

Base path /api. Same-origin only — no CORS. Every failure returns { "error": string, "code": string } with an appropriate status; internals and stack traces never reach the client.

Authentication

Optional. Send x-anthropic-key: sk-ant-... to enable AI features. Endpoints that only compute deterministic results work without it. Endpoints that exist to call the model (/ask, /explain) return 401 missing_api_key without one.

A malformed key is rejected with 400 invalid_api_key_format before any provider call, rather than being silently ignored.

Session header

x-ridge-session: <8-64 url-safe chars> groups saved analyses. Only required when opting into persistence or deleting a saved analysis.


GET /health

{
  "status": "ok",
  "serverKey": false,
  "models": [{ "id": "claude-sonnet-5", "label": "Claude Sonnet 5", "note": "balanced capability, speed and cost" }],
  "defaultModel": "claude-sonnet-5"
}

serverKey reports whether the deployment has a fallback key configured. It never returns the key.


POST /analyze

multipart/form-data, one file. Runs the deterministic pipeline; adds AI interpretation only when a key is present.

Field Type Notes
file file Required. See supported extensions in the README.
question string Optional, ≤ 2000 chars.
model string Optional; one of the ids from /health.
sheet string Optional worksheet name, ≤ 128 chars.
target string Optional column to focus evidence on. Must exist in the file.
headerRow integer Optional, 1-indexed. Overrides the detected header row. A row past the end of the file is not an error — detection runs as it would have, and the request is reported in meta.structure.unapplied with correction: "headerRow", which makes the reading uncertain.
includeRows JSON array Optional, 1-indexed source rows to put back after they were excluded as aggregates. A row that matches no aggregate exclusion is not an error — it is reported in meta.structure.unapplied with the reason, and makes the reading uncertain.
save "true" | "false" Optional. Persistence happens only when explicitly true.

Response:

{
  "meta": {
    "filename": "team-sales.csv", "totalRows": 91, "columns": 7,
    "fileType": "spreadsheet", "isTabular": true, "sheetName": "Sheet1",
    "sheets": ["Sheet1"], "model": "claude-sonnet-5",
    "aiIncluded": false,        // whether an AI interpretation is present
    "target": "revenue",        // echoed back, or null
    "saved": false,             // whether this response was persisted
    "requestId": "92a...",      // also returned in the X-Request-ID header
    "generatedAt": "2026-08-01T18:30:00.000Z",
    "processingMs": 42,
    // How the file was read, before anything was computed. Absent on analyses
    // saved before schema 2.7 — which means the question was never asked, not
    // that nothing was excluded.
    "structure": {
      "headerRow": 3,             // 1-indexed, as the user sees it in Excel
      "headerSource": "detected", // or "specified", when headerRow was sent
      "confidence": "confident",  // "none" | "confident" | "uncertain"
      "observations": 4,          // rows the statistics were computed over
      "excluded": [               // never empty-by-omission; every set-aside row is here
        { "row": 1, "reason": "preamble", "confidence": "confident",
          "detail": "above the header row", "cells": ["Q3 Regional Revenue Report"] },
        { "row": 9, "reason": "aggregate", "confidence": "certain", "label": "TOTAL",
          "detail": "units and revenue equal the sum of rows 4–7", "cells": ["TOTAL", "443", "183750"] }
      ],
      "restored": [],             // rows put back via includeRows, still shown
      "unapplied": [],            // corrections that did not apply, with why:
                                  // includeRows that matched no exclusion, and a
                                  // headerRow past the end of the file
      "alternatives": [],         // other candidate header rows, when uncertain
      "version": "1.1.0"
    },
    "schemaVersion": "2.11", "evidenceEngine": "1.4.0"
  },
  // A numeric column carries `formats` only when it was written in a
  // spreadsheet's own notation — "currency", "thousands", "percent",
  // "negated parentheses". Absent means none were needed.
  //
  // `outliers.rows` (schema 2.10) are the flagged observations themselves:
  // `row` is the 1-based data-row number in the same row space provenance
  // source rows use, `side` says which fence, `beyond` how far past it. The
  // list is ordered by distance, capped at `rowsCap`; `count` is always the
  // true total. A payload without `rows` predates 2.10 — the rows were
  // identified but not shipped, which is not a claim that none exist.
  "stats": { "revenue": { "type": "numeric", "formats": ["currency", "thousands"],
                          "validCount": 90, "missing": 1, "invalid": 0,
                          "coverage": 98.9, "min": 100, "max": 4980, "mean": 0, "median": 0,
                          "std": 0, "meanConfidence95": { "lower": 0, "upper": 0 }, "quantiles": {},
                          "histogram": { "method": "iqr-tail-aware", "bins": [] },
                          "outliers": { "count": 2, "method": "iqr", "applied": true,
                                        "lowerFence": 0, "upperFence": 460,
                                        "rows": [ { "row": 11, "value": 900, "side": "above", "beyond": 440 } ],
                                        "rowsCap": 200 } } },
  "correlations": [ { "columnA": "revenue", "columnB": "spend", "method": "spearman",
                      "coefficient": 0.9068, "pearson": -0.0895, "spearman": 0.9068,
                      "n": 90, "coverage": 98.9, "strength": "very strong",
                      "smallSample": false, "caveat": "..." } ],
  "profile":  { "healthGrade": "B", "healthScore": 87, "issues": [] },
  "evidence": [ { "claim": "...", "metric": "spearman_rho", "value": 0.9068,
                  "columns": ["revenue","spend"], "method": "...", "sampleSize": 90,
                  "coverage": 98.9, "strength": "very strong", "caveat": null,
                  "statistics": null, "engineVersion": "1.4.0",
                  "provenance": { "formula": "ρ = ...", "inclusionRule": "...",
                    "inputRows": 91, "includedRows": 90, "excludedRows": 1,
                    "exclusionReasons": [], "sourceRowsPolicy": "...",
                    "sourceRows": [{ "rowNumber": 1, "values": { "revenue": 100 } }] } } ],
  "analysis": null,             // null unless a key was supplied
  "chartData": [],              // up to 100 rows for client-side charts
  "columns": ["date","region"],
  "rawText": null,              // document excerpt for non-tabular files
  "analysisId": null            // uuid when saved, else null
}

Errors: no_file, unsupported_file_type, upload_too_large (413), empty_file, invalid_sheet, invalid_target, unknown_target, question_too_long, unsupported_model, invalid_api_key_format. Every API response includes an X-Request-ID header. JSON error bodies echo it as requestId so an operator can correlate a user-visible failure without logging file contents, questions, or API keys.


POST /analyze-url

application/json. Same response shape. Fetches the file server-side, so the 4 MB request cap does not apply — remote files may be up to 25 MB.

{ "url": "https://example.com/data.xlsx", "question": "", "model": "",
  "sheet": "Q1", "target": "revenue", "save": false }

Only https:// is accepted. Private, loopback, link-local and CGNAT addresses are rejected before and after DNS resolution and at every redirect hop.


POST /analyze-multi

multipart/form-data, up to 10 files. The aggregate size of all files must stay under 4 MB, enforced independently of the per-file limit.

{
  "files": [ /* one analyze-shaped result per file, with `error` set on failures */ ],
  "crossSummary": null,   // AI cross-file synthesis; requires a key and 2+ AI-analyzed files
  "totalFiles": 3, "successCount": 3,
  "analysisId": null, "saved": false
}

A file that fails to parse does not fail the batch; its entry carries error.


POST /compare

multipart/form-data, exactly two tabular files. The first file is the baseline and the second is the current version. The endpoint is deterministic and never invokes an AI provider; an API key is neither required nor used.

{
  "kind": "comparison",
  "meta": { "comparisonVersion": "1.1.0", "schemaVersion": "2.11", "saved": false },
  "files": [ /* compact deterministic profile for each file */ ],
  "comparison": {
    "deterministic": true,
    "labels": { "baseline": "june.csv", "current": "july.csv" },
    "summary": { "rowDelta": 120, "healthScoreDelta": 4, "sharedColumns": 8 },
    "schema": { "added": [], "removed": [], "shared": [], "typeChanges": [] },
    "quality": { "baseline": {}, "current": {}, "deltas": {} },
    "columns": [ /* descriptive deltas plus Welch mean and two-sample KS inference */ ],
    "findings": [ /* thresholded, deterministic material changes */ ]
  },
  "analysisId": null
}

Set save=true to opt into persistence. Errors include comparison_requires_two_files and comparison_requires_tabular.


POST /explain — requires a key

Adds AI interpretation to results the deterministic pipeline already produced, so a keyless analysis can be explained without re-uploading.

{ "model": "", "question": "",
  "context": { "filename": "", "fileType": "spreadsheet", "columns": [],
               "stats": {}, "correlations": [], "evidence": [], "profile": {},
               "sampleRows": [], "rawText": "" } }

Returns { "analysis": { ... }, "model": "claude-sonnet-5" }. Context is size-capped per part; oversized parts degrade rather than inflating token spend.


POST /ask — requires a key

Follow-up question against an already-analyzed dataset.

{ "question": "Why did June rise?", "model": "",
  "priorQA": [{ "q": "...", "a": "..." }],
  "context": { "columns": ["revenue"], "stats": {}, "correlations": [] } }

Returns { "answer": string, "model": string }. Errors: missing_question, invalid_context, missing_api_key.


History — only when Supabase is configured

Endpoint Behaviour
GET /history?session=<id> { enabled, items[] }. Returns { enabled: false, items: [] } when unconfigured.
GET /analysis/:id One saved analysis by uuid. 404 not_found if absent, 400 invalid_id if malformed.
DELETE /history/:id Deletes a saved analysis. Requires x-ridge-session matching the session that saved it; a share link alone does not grant deletion.

Rate limits

Per IP, per warm instance, in memory.

Endpoint group Default Override
/analyze* 10/min RATE_LIMIT_POINTS
/ask, /explain 20/min RATE_LIMIT_ASK_POINTS

Exceeding a limit returns 429 rate_limited with a Retry-After header.


Error codes

Every failure returns { "error": string, "code": string, "requestId": string }. code is the stable identifier — match on it rather than on the message, which is written for a person and may be reworded. The reference below is complete: tests/api-docs.test.js fails if the server can emit a code this table does not list.

The request

Code Status Meaning
no_file 400 No file was attached to the upload.
empty_file 400 The file parsed but held no rows and no text.
unsupported_file_type 400 The extension is not one of the supported kinds.
file_too_large 413 One file exceeds the 4 MB per-file limit.
upload_too_large 413 The files together exceed the 4 MB request budget.
too_many_files 400 More than 10 files in one multi-file request.
unexpected_field 400 A file arrived under a field name the route does not accept.
upload_error 400 The upload failed for another reason reported by the parser of the multipart body.
invalid_json 400 A JSON request body did not parse.
parse_failed 422 The file matched a supported extension but could not be read.
pdf_timeout 422 A PDF took longer than 45 seconds to read and was abandoned.

The analysis parameters

Code Status Meaning
invalid_question 400 question was not a string.
question_too_long 400 question exceeds 2000 characters.
invalid_sheet 400 sheet was not a string, or exceeds 128 characters.
unknown_sheet 400 The workbook has no sheet of that name; the available names are listed in the message.
invalid_target 400 target was not a string, or exceeds 200 characters.
unknown_target 400 target names a column the file does not have.
invalid_columns 400 columns was not a JSON array of column names, or was too long.
unknown_column 400 columns names a column the file does not have.
no_columns_selected 400 The selection resolved to nothing to analyze.
invalid_header_row 400 headerRow was not a positive row number.
invalid_include_rows 400 includeRows was not a JSON array of positive row numbers, or was too long.
unsupported_model 400 model is not one of the ids /health returns.
invalid_context 400 /ask or /explain was called without usable dataset context.
missing_question 400 /ask was called without a question.
comparison_requires_two_files 400 /compare needs exactly two files, baseline first.
comparison_requires_tabular 400 A file given to /compare could not be read as a table.

A headerRow past the end of the file is not an error: detection runs as it would have, and the request is reported in meta.structure.unapplied.

The API key

Code Status Meaning
missing_api_key 401 An AI endpoint was called with no key configured or supplied.
invalid_api_key_format 400 The key does not begin with sk-ant-. Rejected before any provider call.
invalid_api_key 401 Anthropic rejected the key.
model_not_allowed 403 The key has no access to the selected model.

The upstream model

Code Status Meaning
upstream_rate_limited 429 The Anthropic account hit a rate limit.
upstream_bad_request 400 Anthropic rejected the request; its reason is included.
upstream_unreachable 502 The Anthropic API could not be reached.
upstream_error 502 Anthropic returned an error.
analysis_refused 422 The model declined to analyze the content.
answer_refused 422 The model declined to answer the question.
analysis_truncated 502 The analysis hit the output limit before finishing.
analysis_unparseable 502 The model's structured output did not parse.
empty_answer 502 The model returned no text.

URL ingestion

Code Status Meaning
invalid_url 400 Not a valid URL, or one carrying embedded credentials.
unsupported_protocol 400 Only https:// is fetched.
private_host 400 The URL resolves to a private, loopback, link-local or CGNAT address. Re-checked after DNS and after every redirect.
dns_failed 400 The host did not resolve.
too_many_redirects 400 More than 3 redirects, or a redirect with no location.
fetch_failed 502 The fetch failed, or the URL returned a non-2xx status.
fetch_timeout 502 The fetch exceeded 20 seconds.

History

Code Status Meaning
history_disabled 404 Supabase is not configured on this deployment.
history_unavailable 502 The store could not be reached.
invalid_id 400 The analysis id is not a uuid.
not_found 404 No such analysis — also returned for one that has passed its retention window, so a share link to an expired analysis discloses nothing about what was once there.
missing_session 400 Deleting requires x-ridge-session.

Everything else

Code Status Meaning
rate_limited 429 Per-IP limit exceeded. Carries Retry-After.
internal_error 500 An unexpected failure. The message is deliberately generic; correlate with requestId in the server log.