Pricing

How to Connect Notion to Google Sheets

NotionSaved SearchDateDocumentAmount07/01INV-40214,280.0007/02INV-40221,860.5007/05INV-40239,140.0007/08INV-40242,375.2507/09INV-40256,902.7507/12INV-40263,118.40BrookedABCDateDocumentAmount123456707/01INV-40214,280.0007/02INV-40221,860.5007/05INV-40239,140.0007/08INV-40242,375.2507/09INV-40256,902.7507/12INV-40263,118.40Google SheetsNotion report
JW
James Whitfield

Sync a Notion database to Google Sheets with the API, including the sharing step that causes most 404s and the nested property values that arrive as objects.

Notion exports any database view to CSV in two clicks, which handles one-off requests. For anything that needs to stay current you go through the Notion API, and two things reliably break the first attempt: the integration has no access to your databases until you explicitly share each one with it, and every property comes back as a typed object rather than a value. This guide covers both, plus pagination and write-back.

The two-minute option: CSV export

Open the database as a full page, use the ••• menu, and choose Export with format CSV. Import into Sheets via File → Import. As with most tools, the export follows the current view, so its filters, sorts and hidden properties all apply. Sub-pages and page content are not included: you get the database properties, not what is written inside each page.

Notion to Google Sheets for free (Apps Script + API)

Create an integration at notion.so/my-integrations, choose your workspace, and copy the internal integration token (it begins ntn_ or secret_ on older integrations).

Now the step people miss. Creating the integration grants it access to nothing at all. Open the database you want to read, click •••, find Connections, and add your integration. Skip this and every request returns object_not_found as a 404, which looks exactly like a wrong database ID and sends people re-checking the ID for an hour.

Get the database ID from the URL of the database opened as a full page: it is the 32-character hex string before ?v=. The value after ?v= is the view ID and will not work.

Code.gs
function importNotionDatabase() {  const TOKEN = "ntn_XXXXXXXXXXXXXXXX";  const DATABASE_ID = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4";   const headers = {    Authorization: "Bearer " + TOKEN,    "Notion-Version": "2022-06-28",   // required; omitting it returns 400    "Content-Type": "application/json",  };   const pages = [];  let cursor = null;   do {    const body = { page_size: 100 };   // 100 is the maximum    if (cursor) body.start_cursor = cursor;     const res = JSON.parse(UrlFetchApp.fetch(      "https://api.notion.com/v1/databases/" + DATABASE_ID + "/query",      { method: "post", headers: headers, payload: JSON.stringify(body),        muteHttpExceptions: true }    ).getContentText());     if (res.object === "error") throw new Error(res.code + ": " + res.message);     pages.push(...res.results);    cursor = res.has_more ? res.next_cursor : null;    if (cursor) Utilities.sleep(350);  // ~3 requests/second average limit  } while (cursor);   if (!pages.length) return;   const columns = Object.keys(pages[0].properties);  const rows = pages.map((p) =>    columns.map((c) => readProperty(p.properties[c]))  );   const ss = SpreadsheetApp.getActiveSpreadsheet();  const sheet = ss.getSheetByName("Notion") || ss.insertSheet("Notion");  sheet.clearContents();  sheet.getRange(1, 1, 1, columns.length).setValues([columns]);  sheet.getRange(2, 1, rows.length, columns.length).setValues(rows);} // Every property is a typed object. Each type unwraps differently, which is// why writing the property straight into a cell yields [object Object].function readProperty(prop) {  if (!prop) return "";  switch (prop.type) {    case "title":        return (prop.title[0] || {}).plain_text || "";    case "rich_text":    return prop.rich_text.map((t) => t.plain_text).join("");    case "number":       return prop.number;    case "select":       return prop.select ? prop.select.name : "";    case "status":       return prop.status ? prop.status.name : "";    case "multi_select": return prop.multi_select.map((s) => s.name).join(", ");    case "date":         return prop.date ? prop.date.start : "";    case "people":       return prop.people.map((u) => u.name).join(", ");    case "checkbox":     return prop.checkbox;    case "url":          return prop.url || "";    case "email":        return prop.email || "";    case "relation":     return prop.relation.length;   // count; IDs are opaque    case "formula":      return prop.formula[prop.formula.type];    case "rollup":       return prop.rollup[prop.rollup.type];    default:             return "";  }}

Run once to authorise, then add a time-driven trigger. Note the Notion-Version header is mandatory, not optional: requests without it are rejected outright. And relation properties return arrays of page IDs rather than titles, so displaying the related page's name means a second lookup per ID, which is why the example returns a count instead.

Limits: the API averages around three requests per second, Apps Script stops at six minutes, and page content (the body of each Notion page) requires separate block-level calls entirely.

The Brooked method

Brooked handles the cursor pagination and unwraps each property type into a plain column, including resolving relations to the titles of the pages they point at rather than leaving opaque IDs. Several databases can import into one spreadsheet on a single schedule.

Step 1: Install Brooked

Install Brooked from the Google Workspace Marketplace. Free tier includes 100 imports per month, no credit card required.

Step 2: Connect Notion and share the database

Choose Notion in Brooked and paste your integration token. Then share each database with the integration through its ••• → Connections menu. Brooked can only list databases that have been shared, so if the list looks empty this sharing step is almost always why.

Step 3: Schedule the refresh

Set the import to re-run hourly, daily or weekly. Since it re-reads the database rather than reacting to individual page events, edits made while an automation was failing still get picked up on the next run.

BrookedBrooked AI, Notion
Live
Ask anything about your Notion data…

Method comparison

MethodCostSetupRefreshTwo-wayBest for
Notion CSV exportFree2 minManual, per viewNoA one-off snapshot
Apps Script + Notion APIFree25 to 40 minTime-driven triggerDIYCustom property mapping, no budget
Zapier / Make / n8nFree tier limited · paid for volume10 to 20 minOn page created or updatedDIY (second workflow)Reacting to individual page changes
BrookedFree tier · Pro $29/user/mo~3 minScheduled (hourly, daily, weekly)YesScheduled reporting across databases

Common use cases

  • Roadmap reporting: roll a Notion project database into charts leadership reads in Sheets
  • Content calendars: report on editorial pipelines without giving every stakeholder workspace access
  • CRM-lite tracking: pull a Notion pipeline into a sheet to join against billing or product data
  • Historical snapshots: schedule a daily import so you keep history Notion itself does not retain
  • Cross-database reporting: combine several Notion databases into one table

Troubleshooting common issues

Frequently asked questions

Why does the Notion API return object_not_found for a database I can see?

This is the most common Notion API problem and it is not a permissions bug in the usual sense. An integration starts with access to nothing: you have to explicitly share each database with it. Open the database as a full page, use the ••• menu, find Connections, and add your integration. Until you do, the API behaves as though the database does not exist, returning a 404 rather than a 403, which is why it reads like a wrong ID.

Where do I find my Notion database ID?

Open the database as a full page and look at the URL. The database ID is the 32-character hexadecimal string after your workspace name and before the ?v= view parameter. Note that the part after ?v= is the view ID, not the database ID, and using it produces a 404. Hyphenated and unhyphenated forms both work.

Can I connect Notion to Google Sheets for free?

Yes. Notion exports any database view to CSV from the ••• menu, which is free and takes two minutes for a one-off. For a refreshing sync, the Apps Script method in this guide uses a free internal integration token and a free time-driven trigger. Brooked's free tier covers 100 imports a month if you would rather not maintain the script.

Why are my Notion columns full of [object Object]?

Notion returns every property as a typed object rather than a plain value, and the shape differs per type. A title is an array of rich text objects you read plain_text from, a select is an object with a name, a multi-select is an array of those, and a date is an object with start and end. Each property type needs unwrapping individually, which the code example in this guide does.

Why does my import stop after 100 rows?

100 is the maximum page size on the Notion API. The query response includes has_more and next_cursor; pass that cursor as start_cursor on the following request and repeat until has_more is false. Code that reads a single response silently truncates every database larger than 100 pages.

Can I write from Google Sheets back into Notion?

Yes for editable properties, and Brooked supports write-back on that basis. Formula and rollup properties are computed by Notion and reject writes, as do created and last-edited time and user properties. Update the underlying property the formula reads instead.

Connect Notion to Google Sheets today.

Free tier: 100 imports per month, no credit card required. Or see the full Notion integration details.

Install Brooked free →

Also in Project Management

More Project Management guides

Project Management

How to Connect Monday.com to Google Sheets

Export Monday.com boards to Google Sheets with the GraphQL API, including the items query that was removed and the complexity budget that throttles large boards.

JW
James Whitfield
Read
Project Management

How to Connect Clockify to Google Sheets

Export Clockify time entries to Google Sheets, including the per-user endpoint that hides your team's hours and the ISO durations that will not sum.

JW
James Whitfield
Read
Project Management

How to Connect ClickUp to Google Sheets

Export ClickUp tasks to Google Sheets, including the closed tasks and subtasks left out by default and the timestamps that arrive as millisecond strings.

JW
James Whitfield
Read

Get your spreadsheet hours back

Brooked sets up in seconds. Your team is querying live data before lunch.

Get started free