Data API v1

API Reference

Read and write named ranges and tables in a live Excel workbook over HTTPS. It's the same interface Claude, Zapier, Make, and FinAnalytics use to run scenarios against your financial models. New to AccessEXL? Start with the quick start instead. This page is the full reference.

Base URLhttps://api.accessanalytic.com.au

Overview

Every request operates on exactly one connected workbook, identified by its API key. There's no separate "select a workbook" step: the key is the workbook.

Which ranges are exposed, and whether each is readable, writable, or both, is defined inside the workbook itself by a config table your team maintains, not by anything you configure through this API. That means the same integration keeps working unchanged as the model evolves; add a row to the config table and the new range shows up the next time GET /schema is called.

Two data shapes are supported: single named-range cells (numbers, text, dropdown-validated values) and, optionally, whole rows of a real Excel Table for cases like a monthly forecast or a list of cost centres.

Authentication

Every request carries one workbook's API key in a header. There is no OAuth handshake, no session, and no separate account login for this API: the key is the only credential.

Header
X-API-Key: wk_live_9f2c...

A workbook's first API key is issued when it's connected from the dashboard (by an org admin, or by any member if the organisation allows it), and shown exactly once at that moment; any of a workbook's keys identifies it equally, so there's no separate workbook-selection step in a call. An org admin, or the member who connected that specific workbook, can add further keys (e.g. one per integration) and revoke any single one independently from the workbook's settings popup in the dashboard (click its settings icon) — revoking a key immediately invalidates just that key, without affecting any others issued for the same workbook.

Keep it secret. The key grants full read/write access to whatever ranges and tables the workbook's config exposes. Treat it like a database credential, not a public identifier: store it in your integration platform's secret storage, never in client-side code or a public repository.

Quickstart

Three calls: see what's available, change an input, read the recalculated result.

1. Discover what's exposed
# GET /schema
curl https://api.accessanalytic.com.au/schema \
  -H "X-API-Key: wk_live_9f2c..."
2. Run a scenario
# POST /run-scenario
curl https://api.accessanalytic.com.au/run-scenario \
  -X POST \
  -H "X-API-Key: wk_live_9f2c..." \
  -H "Content-Type: application/json" \
  -d '{"values": {"Revenue_Growth_Rate": 0.08}}'
Response
{
  "status": "updated",
  "values_written": { "Revenue_Growth_Rate": 0.08 },
  "outputs": { "EBITDA": 4200000, "Net_Margin": 0.183 },
  "display_names": {
    "Revenue_Growth_Rate": "Revenue Growth Rate",
    "EBITDA": "EBITDA ($)",
    "Net_Margin": "Net Margin"
  }
}

That's the whole loop: write an input, get the recalculated outputs back in the same response, atomically. Use this pattern (run-scenario) for the common "what if I change X" case rather than calling update-inputs and outputs as two separate requests.

Preparing your workbook

Before an API key can read or write anything, the workbook needs a table named exactly APIConfigTable which has one row per named range you want exposed. For example:

RangeNameDisplayNameDirectionTypeMinMaxValidationRangeNameDescription
Revenue_Growth_RateRevenue Growth Ratewritefloat-11Annual revenue growth assumption
Scenario_NameScenariowritetextrefScenarioNamesWhich named scenario to run
Internal_Seedwrite-onlyfloat
EBITDAEBITDA ($)readComputed EBITDA

Column reference

ColumnRequiredNotes
RangeNamerequiredMust exactly match a defined name in the workbook (Formulas → Name Manager).
DisplayNameoptionalA human-readable label, returned alongside every value as display_names. Blank falls back to RangeName.
Directionrequiredread: readable only. write: writable and readable; a value written and read back (including via run-scenario's echoed outputs) just works. write-only: writable, but excluded from every read, including the config table's own display.
Typeoptionalfloat (or number, accepted as an identical synonym), int, or text. Only applies to write/write-only rows. Blank defaults to float.
Min / MaxoptionalNumeric bounds enforced on every write.
ValidationRangeNameoptionalName of a named range holding an allowed-value list (mirrors an Excel dropdown). When set, Type/Min/Max are ignored: the submitted value must be one of that range's current values.
DescriptionoptionalFree text: write anything that helps describe what this range is for. Unlike DisplayName, blank means the range simply doesn't appear in descriptions at all (no fallback). Returned once by GET /schema, not repeated in every value-bearing response; meant as background context for an AI assistant (or a person) to read before reasoning about a value, not a per-value label.

Column headers are matched leniently: Display Name, display_name, and DISPLAYNAME are all recognised as DisplayName. Edit the table any time and call POST /schema/refresh to pick up the change immediately, without a redeploy or reconnect.

Nothing works without this table. If APIConfigTable is missing, or contains a duplicate RangeName, every endpoint returns 409 until it's fixed, by design, so a broken config can never silently expose the wrong thing.

Exposing tables (optional)

APIConfigTable covers single cells. To write (and optionally read back) a whole block of rows at once (a forecast, a list of cost centres), expose a real Excel Table via two further optional config tables.

APIBlocksConfigTable

One row per exposed table.

TableNameDirectionWriteModeDescription
SalesForecastTablewriteappendMonthly sales forecast rows
CostCentersTablewrite-onlyoverwriteCost centre list

WriteMode is overwrite (clear existing rows, then write the new ones, the default if blank) or append (add after what's already there).

APIBlockColumnsConfigTable

One row per column of each table above: same Type / Min / Max / ValidationRangeName / DisplayName / Description semantics as the single-cell config, including number as a synonym for float. A column of the real table that isn't listed here is never exposed, even though it physically exists.

TableNameColumnNameDisplayNameType
SalesForecastTableMonthMonthtext
SalesForecastTableAmountForecast Amount ($)float

Both tables are optional and independent of APIConfigTable. A workbook with only the single-cell config behaves exactly as before. Either one missing simply means no tables are exposed ("blocks": {} in GET /schema).

Endpoints

Seven endpoints, all under the base URL above. Six are billable against your organisation's monthly quota on a successful response (marked below); schema/refresh never is. Of those six, five count as one call per request; the exception is writing to a block table (POST /blocks/{table_name}), which counts one call per row written instead — see Usage and quota.

GET/schema60 / min

Lists every range and table this workbook exposes right now; call this first, always, rather than assuming a range name. Results are cached server-side for five minutes.

Counts against quota

Response 200

{
  "label": "FY26 Business Case",
  "writable": {
    "Revenue_Growth_Rate": { "type": "float", "min": -1, "max": 1 },
    "Scenario_Name": { "type": "text", "allowed_values": ["Base", "Upside"] }
  },
  "readable": ["EBITDA", "Net_Margin", "Revenue_Growth_Rate"],
  "blocks": {
    "SalesForecastTable": {
      "direction": "write",
      "write_mode": "append",
      "description": "Monthly sales forecast rows",
      "columns": {
        "Month": { "type": "text", "display_name": "Month" },
        "Amount": {
          "type": "float", "min": 0,
          "display_name": "Forecast Amount ($)",
          "description": "In AUD, GST exclusive"
        }
      }
    }
  },
  "display_names": {
    "Revenue_Growth_Rate": "Revenue Growth Rate",
    "EBITDA": "EBITDA ($)"
  },
  "descriptions": {
    "Revenue_Growth_Rate": "Annual revenue growth assumption used across every scenario"
  }
}

A range that's write-only appears in writable but never in readable. display_names covers every writable and readable range; pair it with a value to show a person a friendly label instead of the raw range name. descriptions only includes ranges that actually have one set (EBITDA and Scenario_Name don't, here). Read it once to understand what a range represents; it's not repeated in any other response.

POST/schema/refresh10 / min

Forces an immediate re-read of the config tables, bypassing the five-minute cache. Call this right after editing APIConfigTable (or either block config table) if you don't want to wait for the cache to expire on its own.

Same response shape as GET /schema. Never counts against quota: it's a config sync, not a data operation.

POST/run-scenario30 / min

Writes one or more inputs, recalculates, and reads every readable range back, as one atomic operation under a single lock. This is the recommended way to answer "what happens if I change X": update-inputs and outputs as two separate calls leave a gap where another request could land in between.

Counts against quota

Request body

{ "values": { "Revenue_Growth_Rate": 0.08, "Scenario_Name": "Upside" } }

Response 200

{
  "status": "updated",
  "values_written": { "Revenue_Growth_Rate": 0.08, "Scenario_Name": "Upside" },
  "outputs": { "EBITDA": 4200000, "Net_Margin": 0.183 },
  "display_names": { /* for every name above */ }
}

Pass every value you want changed in a single call rather than one call per value, so the recalculation reflects them all together. Values are validated against each range's type / min / max / allowed_values before anything is written; the whole request is rejected (400) if any one value fails.

If the workbook has Reset values after each call turned on (a per-workbook setting in the AccessEXL dashboard), the written ranges' prior values are restored and the workbook is recalculated again immediately after the response above is computed — so outputs still reflects this call's own scenario, but the workbook itself is left exactly as it was found, ready for the next caller. If that restore step itself fails (the calculation above still succeeded), the response includes an extra reset_warning string explaining so; everything else in the response is unaffected.

POST/update-inputs30 / min

Writes one or more inputs and recalculates, without reading anything back. Use this only when you genuinely don't need the resulting outputs in the same response; otherwise prefer run-scenario.

Counts against quota

Request body

{ "values": { "Revenue_Growth_Rate": 0.08 } }

Response 200

{
  "status": "updated",
  "values_written": { "Revenue_Growth_Rate": 0.08 },
  "display_names": { "Revenue_Growth_Rate": "Revenue Growth Rate" }
}

If the workbook has Reset values after each call turned on (a per-workbook setting in the AccessEXL dashboard), the written ranges' prior values are restored and the workbook is recalculated again immediately after this response is computed. If that restore step itself fails (the write above still succeeded), the response includes an extra reset_warning string explaining so.

GET/outputs60 / min

Reads every currently readable range, without changing any inputs. Use this for a plain read of whatever state the workbook is already in.

Counts against quota

Response 200

{
  "EBITDA": 4200000,
  "Net_Margin": 0.183,
  "Revenue_Growth_Rate": 0.08,
  "display_names": { /* one entry per range above */ }
}

Every readable range is returned as a top-level key, alongside the reserved display_names key.

GET/blocks/{table_name}60 / min

Reads a configured table's current rows, filtered down to only the columns listed in APIBlockColumnsConfigTable. A real Excel Table can have columns beyond what's meant to be exposed, and those never leak through.

Counts against quota

Response 200

# GET /blocks/SalesForecastTable
{
  "rows": [
    { "Month": "Jan", "Amount": 120000 },
    { "Month": "Feb", "Amount": 135000 }
  ],
  "column_display_names": { "Month": "Month", "Amount": "Forecast Amount ($)" }
}

Returns 404 if the table name isn't configured, or 403 if it's configured write-only, or if the table currently has more rows than your plan's per-call row limit (see Usage and quota).

POST/blocks/{table_name}30 / min

Writes a block of rows to a configured table in one call. Each row only needs to include the columns it's setting. Whether existing rows are cleared first depends on that table's configured WriteMode.

Counts against quota once PER ROW written, not once per call

Request body

# POST /blocks/SalesForecastTable
{
  "rows": [
    { "Month": "Mar", "Amount": 142000 },
    { "Month": "Apr", "Amount": 150000 }
  ]
}

Response 200

{ "status": "updated", "rows_written": 2, "write_mode": "append" }

Doesn't echo the resulting rows back: call GET /blocks/{table_name} separately to confirm. A single request is capped at 1,000 rows. This doesn't accept text formatted as a spreadsheet formula: any string value is written as literal text, never evaluated.

Billing: this is the one endpoint where the number of calls charged isn't the number of requests — each row in rows counts as its own call against your quota, since a large import genuinely costs more than a single-cell write. Writing 20 rows in one call uses 20 calls; writing zero rows (an empty rows array, e.g. just to clear a table in overwrite mode) uses none. See Usage and quota.

Errors

Every error response is JSON with a detail field describing what went wrong and, where relevant, how to fix it.

StatusMeaningTypical cause
400Bad requestAn unknown range/column name, a value out of range or the wrong type, or malformed JSON.
401UnauthorizedX-API-Key header missing, the key doesn't match any workbook ("Invalid API key"), or the key has an expiry date that's passed ("This API key has expired" — an org admin, or the member who connected that workbook, can edit or clear that key's expiry from the dashboard to re-enable it, or add a new key).
402Quota exceededSee Usage and quota: includes calls_used/quota in the response body.
403ForbiddenThe organisation is suspended, a write-only block table was requested with GET, or the table has more rows than your plan allows reading/writing in one call (see Usage and quota).
404Not foundThe requested block table name isn't configured in APIBlocksConfigTable.
409ConflictThe workbook's Microsoft connection needs reconnecting, or APIConfigTable is missing/invalid; nothing runs until it's fixed.
429Too many requestsThe per-endpoint rate limit was hit, see Rate limits.
503Workbook busyAnother request is already running against this workbook. Retry shortly. Excel has no concurrency of its own, so every request is serialised.

Quota-exceeded body

{
  "detail": {
    "error": "quota_exceeded",
    "message": "Monthly API call allowance used (1000 of 1000). It resets at the start of the next billing period.",
    "calls_used": 1000,
    "quota": 1000
  }
}

Rate limits

Limits are per-endpoint and keyed to your workbook's own API key. Another org's traffic never affects your limit, and vice versa.

EndpointLimit
GET /schema60 / minute
POST /schema/refresh10 / minute
POST /run-scenario30 / minute
POST /update-inputs30 / minute
GET /outputs60 / minute
GET /blocks/{table_name}60 / minute
POST /blocks/{table_name}30 / minute

A request over the limit gets 429 with {"error": "Rate limit exceeded: …"}. Back off and retry. This is never a sign anything is misconfigured.

Usage and quota

Every successful call to a billable endpoint counts once against your organisation's monthly quota — except writing to a block table, where each row written counts as its own call, since a large import genuinely costs more than a single-cell write. Reading a single output still counts as just one call, the same as reading an entire block table back in one GET /blocks/{table_name} call.

  • Quota is shared across every workbook your organisation has connected, not tracked per workbook.
  • POST /schema/refresh never counts: it's a config sync, not a data operation.
  • Writing N rows in one POST /blocks/{table_name} call uses N calls from your quota, not one — writing zero rows uses none.
  • Any member can view current usage from the dashboard; choosing or changing the plan is admin-only. On a paid plan, calls beyond the included allowance are billed automatically at the plan's rate and are never blocked.
  • Your plan also caps how many rows a single GET/POST /blocks/{table_name} call can read or write, from 100 rows on the free tier up to 100,000 on the top business tier. This is a per-call cap, not a cumulative total: a table that's grown past the limit can still be written to in smaller batches, it just can't be read back in one call until the org upgrades (or the table shrinks).