Skip to main content
RapidDev - Software Development Agency
flutterflow-integrationsFlutterFlow API Call

Notion

Connect FlutterFlow to Notion by calling the Notion REST API through a Firebase Cloud Function proxy that holds your internal integration token. FlutterFlow has no npm runtime, so you use raw REST — not the official Notion SDK. The proxy also flattens Notion's deeply nested property JSON into a clean array that FlutterFlow's JSON-path binding can read. This turns your Notion database into a headless CMS your team edits, while users see a native app.

What you'll learn

  • How to create a Notion internal integration and share a database with it
  • Why the integration token must be proxied through a Firebase Cloud Function
  • How to flatten Notion's nested property JSON in the proxy so FlutterFlow can bind it
  • How to create a FlutterFlow API Group targeting your Cloud Function
  • How to map a Notion database to a Custom Data Type and ListView
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read45 minutesProductivityLast updated July 2026RapidDev Engineering Team
TL;DR

Connect FlutterFlow to Notion by calling the Notion REST API through a Firebase Cloud Function proxy that holds your internal integration token. FlutterFlow has no npm runtime, so you use raw REST — not the official Notion SDK. The proxy also flattens Notion's deeply nested property JSON into a clean array that FlutterFlow's JSON-path binding can read. This turns your Notion database into a headless CMS your team edits, while users see a native app.

Quick facts about this guide
FactValue
ToolNotion
CategoryProductivity
MethodFlutterFlow API Call
DifficultyIntermediate
Time required45 minutes
Last updatedJuly 2026

Use Notion as a Headless CMS for Your FlutterFlow App

Notion databases are already structured tables with typed columns — title, text, date, select, checkbox, URL. That structure makes Notion an excellent headless CMS for FlutterFlow apps: your marketing team, writers, or ops people edit content in Notion's familiar interface, and your app displays it natively without you rebuilding an admin panel. Common use cases are blog feeds, changelogs, FAQs, team directories, product catalogs, and event lists — any content that non-technical team members need to update regularly.

The Notion API is available on all plans including the free tier, with no per-call charge. You authenticate with an internal integration token (a Bearer token tied to your workspace) that you create at notion.so/my-integrations. One crucial step that trips up most developers: you must manually Share the target Notion database with the integration — otherwise queries return a 404 'object not found' error even with a perfectly valid token. It's a permission model, not an auth bug.

The FlutterFlow-specific challenge is twofold. First, the integration token must never live in a FlutterFlow API Call header, because FlutterFlow compiles to client-side code and the token ships in the app bundle. A Firebase Cloud Function holds the token as a server-side environment variable and proxies calls to Notion. Second, Notion's API returns deeply nested JSON — a page's title is nested six levels deep as `properties.Name.title[0].plain_text`. Binding that directly in FlutterFlow's JSON-path tool is painful. The smart move is to flatten the response inside the Cloud Function — return a clean `{title, body, tags, date}` array instead — so FlutterFlow's bindings stay simple and maintainable.

Integration method

FlutterFlow API Call

FlutterFlow calls the Notion REST API (base URL: https://api.notion.com/v1) through a Firebase Cloud Function that holds the internal integration token securely. Because FlutterFlow compiles to a client-side Flutter app, the token cannot live in an API Call header — it would ship inside the app bundle. The Cloud Function also flattens Notion's nested property JSON (rich_text arrays, title arrays) into a simple object array before returning it, so FlutterFlow's JSON-path binding stays manageable.

Prerequisites

  • A Notion account (free plan works — the API is free on all plans)
  • A Notion database with the content you want to display in the app
  • A FlutterFlow project open in your browser
  • A Firebase project with Cloud Functions enabled
  • Your Notion internal integration token from notion.so/my-integrations

Step-by-step guide

1

Step 1: Create a Notion internal integration and share the database

Go to notion.so/my-integrations in your browser and click '+ New integration'. Give it a name like 'FlutterFlow App', select your workspace, and set the capabilities you need — at minimum 'Read content'. For apps that create or update pages, also enable 'Insert content' and 'Update content'. Click Submit, then copy the 'Internal Integration Token' shown on the next screen — this is your secret that authenticates API calls. Now go to the Notion database you want the app to read. Open it in full-page view, click the three-dot '...' menu in the top-right corner, and scroll to 'Connections'. Click 'Add connections', search for the integration name you just created, and click 'Confirm'. This sharing step is what most developers miss — without it, the API returns a 404 'Could not find database with ID' error regardless of how valid your token is. Note the database ID from the URL: when you open a Notion database in your browser, the URL looks like `notion.so/workspace/DATABASE-ID?v=VIEW-ID`. The DATABASE-ID is a 32-character string (with hyphens like a UUID). Copy it — you'll use it in the Cloud Function to specify which database to query.

Pro tip: Create a dedicated Notion integration for each app you build, rather than sharing one integration across projects. This way you can revoke or update one app's access without affecting others.

Expected result: Your integration appears under 'Connections' on the Notion database page, and you have the integration token and database ID copied.

2

Step 2: Deploy a Cloud Function that queries Notion and flattens the JSON

Deploy a Firebase Cloud Function that handles two jobs: proxying the request to Notion with the integration token in the Authorization header, and flattening Notion's deeply nested property JSON into a simple array of objects. Notion's raw API response for a database query looks like this: `results[].properties.Name.title[0].plain_text` for a title field, or `results[].properties.Body.rich_text[0].plain_text` for a text field. Trying to write JSON paths like `$.results[*].properties.Name.title[0].plain_text` in FlutterFlow's binding tool is tedious and breaks whenever you add or rename a database column. The smarter approach: the Cloud Function parses the raw Notion response and returns a simplified array like `[{"title": "...", "body": "...", "date": "...", "id": "..."}]`. FlutterFlow then just needs simple paths like `$[*].title`. Deploy this function with your Notion token stored as a Firebase Functions config variable (never in source code): run `firebase functions:config:set notion.token="your-token" notion.database_id="your-db-id"` then deploy.

index.js
1// Firebase Cloud Function — Notion headless CMS proxy
2// index.js
3const functions = require('firebase-functions');
4const https = require('https');
5
6exports.notionQuery = functions.https.onRequest((req, res) => {
7 res.set('Access-Control-Allow-Origin', '*');
8 res.set('Access-Control-Allow-Methods', 'POST, OPTIONS');
9 res.set('Access-Control-Allow-Headers', 'Content-Type');
10 if (req.method === 'OPTIONS') { res.status(204).send(''); return; }
11
12 const token = functions.config().notion.token;
13 const dbId = functions.config().notion.database_id;
14 const body = JSON.stringify({ page_size: 50 });
15
16 const options = {
17 hostname: 'api.notion.com',
18 path: `/v1/databases/${dbId}/query`,
19 method: 'POST',
20 headers: {
21 'Authorization': `Bearer ${token}`,
22 'Notion-Version': '2022-06-28',
23 'Content-Type': 'application/json',
24 'Content-Length': Buffer.byteLength(body)
25 }
26 };
27
28 const proxyReq = https.request(options, (proxyRes) => {
29 let data = '';
30 proxyRes.on('data', chunk => { data += chunk; });
31 proxyRes.on('end', () => {
32 const json = JSON.parse(data);
33 // Flatten Notion's nested properties to simple objects
34 const items = (json.results || []).map(page => ({
35 id: page.id,
36 title: page.properties?.Name?.title?.[0]?.plain_text || '',
37 body: page.properties?.Body?.rich_text?.[0]?.plain_text || '',
38 tags: (page.properties?.Tags?.multi_select || []).map(t => t.name).join(', '),
39 date: page.properties?.Date?.date?.start || ''
40 }));
41 res.status(200).json({ items });
42 });
43 });
44 proxyReq.on('error', (e) => res.status(500).json({ error: e.message }));
45 proxyReq.write(body);
46 proxyReq.end();
47});

Pro tip: Adjust the property names in the mapping (Name, Body, Tags, Date) to match your actual Notion database column names exactly — Notion's API is case-sensitive for property names.

Expected result: A deployed Cloud Function that returns a clean JSON array like `{"items": [{"id": "...", "title": "...", "body": "...", "tags": "...", "date": "..."}]}` when called with a POST request.

3

Step 3: Create a FlutterFlow API Group targeting your Cloud Function

In FlutterFlow, click 'API Calls' in the left navigation panel. Click '+ Add' and choose 'Create API Group'. Name the group 'Notion' and set the Base URL to your Cloud Function's HTTPS trigger URL (e.g., `https://us-central1-your-project.cloudfunctions.net/notionQuery`). No shared headers are needed at the group level — the function handles Notion auth internally. Click '+ Add API Call' inside the Notion group. Name this call 'Query Database'. Set the Method to POST. Leave the endpoint path blank (the function is at the root URL). In the Headers tab, add a `Content-Type` header with value `application/json` — this is needed because you're sending a POST body, even if it's empty from FlutterFlow's side (the function uses its own hardcoded database ID). Click 'Response & Test' and paste a sample response from your Cloud Function in the Response Body field. A sample looks like: `{"items": [{"id": "abc", "title": "Example Entry", "body": "Content here", "tags": "News", "date": "2026-03-15"}]}`. Click 'Generate JSON Paths' — FlutterFlow will identify `$.items[*].id`, `$.items[*].title`, `$.items[*].body`, `$.items[*].tags`, and `$.items[*].date` as bindable paths. Click 'Test API Call' to verify the live connection returns data.

Pro tip: If you need to query multiple Notion databases (e.g., blog posts AND FAQs), create separate Cloud Function endpoints or pass the database ID as a query parameter, and add a separate API Call in the same FlutterFlow API Group for each.

Expected result: The API Call test returns 200 with a clean items array. JSON paths for title, body, tags, and date appear in the JSON Path Extractor panel, ready to bind.

4

Step 4: Map the Notion database to a Custom Data Type and build the list screen

Go to Settings → Data Types → + Add Data Type. Name it 'NotionItem'. Add the following fields: `id` (String), `title` (String), `body` (String), `tags` (String), `date` (String). These match the flattened output from your Cloud Function. Now open your list screen in FlutterFlow. Add a ListView widget to the page. In the ListView's 'Backend Query' panel on the right, set the query type to 'API Call' and select your 'Query Database' call. Under 'JSON Path', map the response to your NotionItem Data Type by linking each JSON path to its corresponding field: `$.items[*].title` → title, `$.items[*].body` → body, and so on. Inside the ListView, build your list item widget. Add a Column widget as the list item container. Inside the column, add a Text widget for the title — in its properties, click the binding icon (orange lightning bolt) and select the current list item's `title` field. Add another Text widget below it for the body preview (use a `maxLines` property to limit to 2 lines and show '...' for overflow). Add a third Text widget for the date. For a blog-style detail screen: wire the list item's On Tap action to navigate to a detail page, passing the entire NotionItem as a page parameter. On the detail page, render the `body` field in a larger Text widget. If you need formatted rich text (bold, italic, links), you'll need to fetch the full page blocks from Notion's /blocks endpoint and render them — keep that for a follow-up iteration.

Pro tip: Cache the list response in App State (as a list of NotionItems) so that navigating back from the detail page doesn't trigger another API call. Set a short cache timeout (5–10 minutes) since Notion content doesn't change by the second.

Expected result: A scrollable ListView showing entries from your Notion database with title, preview text, and date. Tapping an item navigates to a detail screen showing the full content.

5

Step 5: Handle write-back (creating or updating Notion pages from the app)

If your use case requires users to add entries to the Notion database from the app — submitting a support request, adding a task, logging a form response — you need to extend the Cloud Function with a write path. The same integration token works for writes, provided you enabled 'Insert content' capability when creating the integration at notion.so/my-integrations. Add a second export to your Cloud Function named `notionCreate` (or handle POST vs GET in the same function with a method check). This function receives the new page's properties as a JSON body from FlutterFlow, then calls `POST /v1/pages` on the Notion API with the parent database ID and the properties object. The properties must match Notion's typed format: a title field is `{"title": [{"text": {"content": "Your value"}}]}`, not just a plain string. In FlutterFlow, add a second API Call named 'Create Page' in your Notion API Group. Set the Method to POST. In the Body tab, add the properties JSON as a variable — use FlutterFlow's Variables tab to define a `properties` variable (JSON type) and reference it as `{{ properties }}` in the body. Wire this API Call to a form's submit button via the Action Flow Editor: Form Submit → Add Action → Backend/API Call → Notion / Create Page. For the Notion-Version header: both the query and create calls must include `Notion-Version: 2022-06-28` in the request headers. Add this in the proxy function headers, not in FlutterFlow's API Call config — keeping all Notion-specific headers server-side simplifies future version updates.

Pro tip: If you'd rather skip the Cloud Function setup entirely, RapidDev's team builds FlutterFlow integrations like this every week — free scoping call at rapidevelopers.com/contact.

Expected result: Submitting the form from the FlutterFlow app creates a new page in your Notion database, visible immediately in Notion's web interface.

Common use cases

App changelog screen powered by a Notion database

A SaaS mobile app that shows its release changelog natively. The product team adds new entries in a Notion database with columns for Version, Release Date, and Description. The FlutterFlow app fetches and displays these as a scrollable native list, with no app update needed to add changelog entries.

FlutterFlow Prompt

Build a 'What's New' changelog screen that fetches entries from my Notion database and shows version number, release date, and description in a card list sorted by date descending.

Copy this prompt to try it in FlutterFlow

FAQ screen with Notion as the content source

A support app where the customer success team maintains FAQs in a Notion database with Question and Answer columns. The FlutterFlow app renders these as an expandable accordion list — no engineering work needed when FAQs change, just update the Notion database.

FlutterFlow Prompt

Create a FAQ screen that loads questions and answers from a Notion database and displays them as an expandable accordion where tapping a question reveals the answer.

Copy this prompt to try it in FlutterFlow

Team directory app backed by a Notion team database

An internal company app where HR maintains the team directory in a Notion database with Name, Role, Department, and Photo URL columns. The FlutterFlow app shows a searchable list of team members with their photos and roles, always up to date without needing an app release.

FlutterFlow Prompt

Build a team directory screen that loads employee cards from a Notion database — showing name, role, department, and a photo — with a search bar to filter by name or department.

Copy this prompt to try it in FlutterFlow

Troubleshooting

404 'Could not find database with ID' even though the database exists and the token is valid

Cause: The Notion database has not been shared with the integration. Notion's permission model requires an explicit 'Share' step — the integration cannot access any database by default, even within the same workspace.

Solution: Open the target Notion database in full-page view → click the '...' menu → Connections → Add connections → find your integration name → Confirm. The 404 error disappears immediately after sharing.

XMLHttpRequest error in FlutterFlow Test Mode — the API Call fails in the browser

Cause: CORS error. The browser enforces CORS on all outbound requests; if your Cloud Function does not return Access-Control-Allow-Origin headers, Test Mode (which runs in a browser) will block the response. Native iOS/Android builds are not affected.

Solution: Ensure your Cloud Function sets `res.set('Access-Control-Allow-Origin', '*')` and handles OPTIONS preflight requests by returning 204. The example code in Step 2 includes this. Rebuild and redeploy the function after adding CORS headers.

Empty or undefined text fields in the FlutterFlow list even though the Notion database has content

Cause: The property names in the Cloud Function's mapping code don't match the actual Notion database column names, or the column types don't match (e.g., trying to read a title field as rich_text or vice versa).

Solution: Log the raw Notion API response inside the Cloud Function (console.log(JSON.stringify(json.results[0]?.properties))) and check the Firebase Functions logs. Match the property names and type accessors exactly to what Notion returns — Notion's title type uses `.title[0].plain_text`, rich_text type uses `.rich_text[0].plain_text`, and select type uses `.select.name`. Update the mapping in the function and redeploy.

403 Unauthorized on write operations (creating pages)

Cause: The integration was created with 'Read content' capability only. Write operations require 'Insert content' (for creating pages) or 'Update content' (for updating existing pages) capabilities to be enabled.

Solution: Go to notion.so/my-integrations → select your integration → edit capabilities → enable 'Insert content' and/or 'Update content' → save. The token does not change; the permission update takes effect immediately.

Best practices

  • Always Share the Notion database with your integration explicitly — the 404 'object not found' error is caused by missing this step, not a token problem.
  • Flatten Notion's nested property JSON in the Cloud Function before returning it to FlutterFlow — binding six levels of nesting directly in FlutterFlow's JSON-path tool is fragile and breaks when you rename columns.
  • Never put the Notion integration token in a FlutterFlow API Call header — it grants write access to your workspace and ships in the compiled app bundle; keep it in the Cloud Function only.
  • Add the Notion-Version header (2022-06-28) in the Cloud Function, not in FlutterFlow — this way a future API version update only requires a server-side change.
  • Cache the Notion response in App State with a short TTL (5–10 minutes) to reduce API calls and speed up navigation — Notion content rarely changes by the minute.
  • Use page_size: 50 in the database query and implement cursor-based pagination if your database has more than 50 entries — Notion's API returns a next_cursor field for pagination.
  • For rich text content with formatting, fetch the full page blocks from the Notion /blocks/{id}/children endpoint and flatten the block types (paragraph, heading_1, bulleted_list_item) in the proxy — don't try to render raw block JSON in FlutterFlow widgets.

Alternatives

Frequently asked questions

Can FlutterFlow use the official Notion SDK instead of raw REST?

No — FlutterFlow has no npm runtime, so you cannot install the official Notion SDK (`@notionhq/client`) directly in FlutterFlow. The SDK is a Node.js library. The correct approach is to use the SDK inside your Firebase Cloud Function (which does run Node.js), and return the simplified result to FlutterFlow via a plain HTTP call.

Is the Notion API free to use?

Yes — the Notion REST API is available on all Notion plans including the free tier, with no per-call charge. You only need to upgrade your Notion plan if you need features like version history, advanced permissions, or larger database limits — not for API access itself. Check Notion's current documentation for the latest rate limits (approximately 3 requests/second average).

How do I display images from a Notion database in my FlutterFlow app?

Notion file/image properties return a signed URL that expires after 1 hour. Extract the URL from `properties.Cover.files[0].file.url` (for Notion-hosted files) or `properties.Cover.files[0].external.url` (for external URLs) in your Cloud Function. Return it as a string field and bind it to an Image widget in FlutterFlow using a network image. For external URLs (e.g., Unsplash links), expiry is not an issue; for Notion-hosted images, re-fetch the URL on screen load.

Can I filter or sort the Notion database query from FlutterFlow?

Yes — extend your Cloud Function to accept filter and sort parameters as query parameters or a POST body from FlutterFlow. The Notion query endpoint (`POST /v1/databases/{id}/query`) supports a `filter` object and `sorts` array in the request body. For example, to filter by a 'Status' select property, pass `{"filter": {"property": "Status", "select": {"equals": "Published"}}}`. Your Cloud Function passes this along to Notion, and FlutterFlow sends the filter criteria as API Call variables.

What happens if Notion changes its API version?

The `Notion-Version` header (currently `2022-06-28`) pins your calls to a specific API version. Notion maintains backward compatibility within a version, so existing calls won't break when a new version is released. When you're ready to upgrade, update the header value in your Cloud Function and redeploy — no FlutterFlow changes needed, since the version logic lives entirely server-side.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Integrations are where projects stall

We wire FlutterFlow integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.