Blog

Elevate Google Sheets API: Turn a Spreadsheet Into a Real API

Kin Lane ·May 5, 2026
Table of contents

I am four posts into walking back through the Naftiko Framework use cases, and this is the one I have been looking forward to, because it is the use case that everybody has and nobody talks about. Every operations team I have worked with in the last decade has a Google Sheet somewhere that is quietly running part of the business. Inventory levels. Partner tiers. Pricing tables. Rollout schedules. SKU overrides. The sheet is the system of record, whether the architecture diagram admits it or not.

So that is where this use case lives — Elevate Google Sheets API. Wrap Google Sheets as a capability so spreadsheet rows become a reusable, domain-specific API for traditional clients and AI agents.

What Google’s API actually gives you

Google exposes a Sheets REST API, and it works. You can hit /v4/spreadsheets/{spreadsheetId}/values/{range} with an API key and you get back JSON. The problem is what the JSON looks like.

{
  "range": "Inventory!A2:D",
  "majorDimension": "ROWS",
  "values": [
    ["SKU-001", "Widget", "42", "in_stock"],
    ["SKU-002", "Gadget", "0",  "backorder"],
    ["SKU-003", "Sprocket", "17", "in_stock"]
  ]
}

That is a positional array. Nothing in the response tells a consumer that index 0 is a SKU, that index 2 is a quantity that should be a number, or that index 3 is a status enum. The column names live in row 1 of the sheet, outside the payload. Every downstream system — an app, a script, a copilot — has to re-learn that ordering.

This is the API space’s version of a CSV without a header. It is technically data. It is not a contract.

The capability fix

The Naftiko capability spec treats the Sheets API the same way it treats any other upstream — declare it in consumes, declare the named output shape in exposes, and let positional JSONPath mapping do the work between them.

naftiko: "1.0.0-alpha1"

info:
  title: Inventory From Google Sheets
  description: "Operations inventory sheet exposed as a governed capability for apps and AI agents"

capability:
  consumes:
    - namespace: gsheets
      type: http
      baseUri: "https://sheets.googleapis.com"
      authentication:
        type: apiKey
        in: query
        name: key
        value: ""
      resources:
        - name: values
          path: "/v4/spreadsheets/{spreadsheetId}/values/{range}"
          operations:
            - name: read-range
              method: GET
              inputParameters:
                - name: spreadsheetId
                  in: path
                  type: string
                  required: true
                - name: range
                  in: path
                  type: string
                  required: true

  binds:
    - name: GOOGLE_SHEETS_API_KEY
      source: env

  exposes:
    - type: mcp
      namespace: inventory
      tools:
        - name: list-inventory
          description: "List SKUs, names, quantities, and stock status from the operations inventory sheet"
          hints:
            readOnly: true
            idempotent: true
          call: gsheets.read-range
          with:
            spreadsheetId: "1AbCDeFgHiJkLmNoPqRsTuVwXyZ-example"
            range: "Inventory!A2:D"
          outputParameters:
            - type: array
              mapping: "$.values"
              items:
                type: object
                properties:
                  - name: sku
                    type: string
                    mapping: "$[0]"
                  - name: name
                    type: string
                    mapping: "$[1]"
                  - name: quantity
                    type: integer
                    mapping: "$[2]"
                  - name: status
                    type: string
                    mapping: "$[3]"

    - type: rest
      basePath: "/api/inventory"
      endpoints:
        - method: GET
          path: "/"
          description: "List inventory SKUs and quantities"
          call: gsheets.read-range
          with:
            spreadsheetId: "1AbCDeFgHiJkLmNoPqRsTuVwXyZ-example"
            range: "Inventory!A2:D"

One YAML file. No Python. No Apps Script. The positional mapping — $[0], $[1], $[2], $[3] — is the load-bearing piece. It is how you turn a nameless row of strings into a typed {sku, name, quantity, status} object, and then expose that same object on an MCP tool and a REST endpoint at the same time.

Three dimensions, one sheet

Every API topic has three dimensions. This one is no exception.

Technology. The Sheets API is fine. The problem is that $.values is not a schema. The capability layer adds the schema. outputParameters with positional JSONPath mapping is doing what a header row does for a CSV — except it is declared in Git, typed, and validated. quantity becomes an integer, not a string. status becomes a named field. The engine reshapes the response before anything — app, agent, or reviewer — sees it.

Business. This is the part that actually matters. The spreadsheet that is quietly running the business is usually owned by the operations team, not the platform team. Nobody has ever governed it. The capability spec gives operations a way to keep owning the sheet while the platform team owns the contract. Columns can move, rows can grow, but the exposed contract — the named fields, their types, the basePath — stays stable. Consumers do not break when a column gets added in position 5.

Politics. There is a conversation I have had more times than I can count. Engineering says “the real system of record is the database.” Operations opens a sheet and shows me the live numbers that are actually running fulfillment. Both are telling the truth. The capability spec lets both be true without anybody losing face. The sheet stays the sheet. The API is the capability. The platform team gets an artifact to lint with Spectral. Operations keeps the tool they actually use to run the business.

Features that carry this use case

These are the wiki features that do the real work:

  • Declarative Google Sheets consumption with templated spreadsheetId and range path parameters — one capability can read any sheet, any range
  • API key injection via binds — the key never lives in the spec, it comes from env, file, or vault
  • Row-to-object transformation with positional JSONPath mapping ($[0], $[1], $[2]) — the thing that makes a row actually mean something
  • Typed output parametersinteger, string, number, not “whatever Sheets handed us”
  • Dual-channel exposure — one capability, one reshaping, two listeners: MCP for agents, REST for apps

What this pattern unlocks

The thing I keep coming back to is that every company I have worked with has five to fifty of these spreadsheets. Partner lists. Feature-flag matrices. Regional pricing. Event rosters. The moment you have a capability for one of them, you have a template for all of them. Copy the YAML, change the spreadsheetId, change the positional mappings, rename the output fields. Done. The spreadsheet keeps being the spreadsheet. The API becomes governed.

That is also why this is the use case I trot out when somebody asks “is Naftiko useful if we don’t have an MCP strategy yet?” You do not need one. You need a sheet and an API key. The capability is valuable before a single agent calls it, because it is the first time that sheet has ever had a contract.

Next

Next in the series is Compose AI context — combining multiple consumed APIs into one capability so an agent gets task-ready context from across systems, not one source at a time.

I will keep walking the list.