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.
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.
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.
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.
Three calls: see what's available, change an input, read the recalculated result.
# GET /schema
curl https://api.accessanalytic.com.au/schema \
-H "X-API-Key: wk_live_9f2c..."# 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}}'{
"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.
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:
| RangeName | DisplayName | Direction | Type | Min | Max | ValidationRangeName | Description |
|---|---|---|---|---|---|---|---|
Revenue_Growth_Rate | Revenue Growth Rate | write | float | -1 | 1 | Annual revenue growth assumption | |
Scenario_Name | Scenario | write | text | refScenarioNames | Which named scenario to run | ||
Internal_Seed | write-only | float | |||||
EBITDA | EBITDA ($) | read | Computed EBITDA |
| Column | Required | Notes |
|---|---|---|
RangeName | required | Must exactly match a defined name in the workbook (Formulas → Name Manager). |
DisplayName | optional | A human-readable label, returned alongside every value as display_names. Blank falls back to RangeName. |
Direction | required | read: 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. |
Type | optional | float (or number, accepted as an identical synonym), int, or text. Only applies to write/write-only rows. Blank defaults to float. |
Min / Max | optional | Numeric bounds enforced on every write. |
ValidationRangeName | optional | Name 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. |
Description | optional | Free 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.
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.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.
APIBlocksConfigTableOne row per exposed table.
| TableName | Direction | WriteMode | Description |
|---|---|---|---|
SalesForecastTable | write | append | Monthly sales forecast rows |
CostCentersTable | write-only | overwrite | Cost 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).
APIBlockColumnsConfigTableOne 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.
| TableName | ColumnName | DisplayName | Type |
|---|---|---|---|
SalesForecastTable | Month | Month | text |
SalesForecastTable | Amount | Forecast 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).
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.
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{
"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.
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.
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.
{ "values": { "Revenue_Growth_Rate": 0.08, "Scenario_Name": "Upside" } }{
"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.
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.
{ "values": { "Revenue_Growth_Rate": 0.08 } }{
"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.
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{
"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.
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.
# 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).
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.
# POST /blocks/SalesForecastTable
{
"rows": [
{ "Month": "Mar", "Amount": 142000 },
{ "Month": "Apr", "Amount": 150000 }
]
}{ "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.
Every error response is JSON with a detail field describing what went wrong and, where relevant, how to fix it.
| Status | Meaning | Typical cause |
|---|---|---|
| 400 | Bad request | An unknown range/column name, a value out of range or the wrong type, or malformed JSON. |
| 401 | Unauthorized | X-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). |
| 402 | Quota exceeded | See Usage and quota: includes calls_used/quota in the response body. |
| 403 | Forbidden | The 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). |
| 404 | Not found | The requested block table name isn't configured in APIBlocksConfigTable. |
| 409 | Conflict | The workbook's Microsoft connection needs reconnecting, or APIConfigTable is missing/invalid; nothing runs until it's fixed. |
| 429 | Too many requests | The per-endpoint rate limit was hit, see Rate limits. |
| 503 | Workbook busy | Another request is already running against this workbook. Retry shortly. Excel has no concurrency of its own, so every request is serialised. |
{
"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
}
}Limits are per-endpoint and keyed to your workbook's own API key. Another org's traffic never affects your limit, and vice versa.
| Endpoint | Limit |
|---|---|
GET /schema | 60 / minute |
POST /schema/refresh | 10 / minute |
POST /run-scenario | 30 / minute |
POST /update-inputs | 30 / minute |
GET /outputs | 60 / 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.
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.
POST /schema/refresh never counts: it's a config sync, not a data operation.POST /blocks/{table_name} call uses N calls from your quota, not one — writing zero rows uses none.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).