Getting Started with the Notion API Using Python

Get started with the Notion API in Python by creating and connecting an integration, then creating and retrieving a page.

If you’re looking for Python examples using the official Notion API, you’ve come to the right place! In this tutorial, we’ll make a Notion integration, connect a page, retrieve the page, create a child page, and finish off with retrieving that page’s contents, all using the Requests library in Python to access the Notion API. If you want to know more about what a page is in Notion, and some of the other Notion concepts, check out my earlier post on Notion Basics For Developers.

The Notion API is a REST API which returns JSON responses; if you need a refresher on what that means I suggest checking out How to use REST API with Python.

Create your Integration #

The first step to building a Notion integration is creating your integration and getting a token. If you don’t already have a Notion workspace that you have Admin access to, you’ll need to sign up for a Notion account first, then head on over to https://www.notion.so/my-integrations and hit the New Integration button. You can just give it a name (I used “Example Integration”), make sure you’re using your desired workspace if you have access to more than one, and hit Submit. You can change most settings later if you want. Your integration will be first created as an Internal Integration, which is great for development.

Screenshot after creating integration showing Internal Integration

Notion API requests are authenticated using a Bearer Token. When you are working with an internal integration, you use the token from the Secrets section of your Integration page for all requests. When you want to share your integration with a wider audience, you will need to set up an OAuth flow to obtain permission and access tokens per workspace - that will be covered in a future post.

Connect A Page #

Once you’ve gotten a token, you also need to add a connection to your integration from any pages you wish to share. This will give the integration access to that page and it’s child pages. This is now done by adding a connection using the “…” menu in the top right (until recently, it was like sharing with another user). You can use the search box if your integration doesn’t pop up at the top of the list.

For the purposes of the sample code below, create one page and add a connection to your integration.

Screenshot of connecting an integration using “Add connections” and selecting the integration in the menu.

Screenshot of connecting an integration using “Add connections” and selecting the integration in the menu.

Make your first request #

To test that everything is set up correctly, you can retrieve a list of objects shared with your integration using the search endpoint. Pages shared directly with the integration should be available immediately, though sub pages may take some time to be indexed.

To run the sample code, ensure you have the requests library installed in your Python environment, and set an environment variable named NOTION_KEY with the value of your integration’s token.

First of all, we will set up our basic imports and headers. There are 3 headers that are required for all requests: Authorization, with the Bearer token, Content-Type which should be application/json, and Notion-Version, which specifies which version of the API to use. 2022-06-28 is the latest version as of the time I am writing this, but that changes every few months.

import os
import requests
import json

NOTION_KEY = os.environ.get("NOTION_KEY")
headers = {'Authorization': f"Bearer {NOTION_KEY}",
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28'}

To retrieve a list of pages that are shared with the integration, we will use the Search endpoint, which accepts a POST request. In this case, we will filter the response to only pages, but you can also set a query and a sort order. Omitting the parameters here will cause it to retrieve all pages and databases shared with the integration.

search_params = {"filter": {"value": "page", "property": "object"}}
search_response = requests.post(
f'https://api.notion.com/v1/search',
json=search_params, headers=headers)

print(search_response.json())

If everything is correct, you should get a response like this:

{
"object": "list",
"results": [
{
"object": "page",
"id": "1c4ed430-d24d-4eba-be10-d6c11778461a",
"created_time": "2022-09-14T11:57:00.000Z",
"last_edited_time": "2022-09-14T12:05:00.000Z",
"created_by": {
"object": "user",
"id": "41abb3e6-1df6-4bf8-8b14-08a5d21a8c01"
},
"last_edited_by": {
"object": "user",
"id": "41abb3e6-1df6-4bf8-8b14-08a5d21a8c01"
},
"cover": null,
"icon": null,
"parent": {
"type": "workspace",
"workspace": true
},
"archived": false,
"properties": {
"title": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "Page for Example Integration",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Page for Example Integration",
"href": null
}
]
}
},
"url": "https://www.notion.so/Page-for-Example-Integration-1c4ed430d24d4ebabe10d6c11778461a"
}
],
"next_cursor": null,
"has_more": false,
"type": "page_or_database",
"page_or_database": {}
}

This request returns any pages that are connected to the integration and their properties, which for pages not in a database. like this one, is just the title. Retrieving the content of a page is a separate request which we’ll see below.

Notice that the title property is returned as an array of objects, rather than just text. While in this case our title has no formatting, this is an example of Notion’s rich text formatting; various annotations and links can be applied to parts of text, and text is always returned as an array of objects.

If you haven’t connected any pages, but your token is correct, you’ll get a response like this:

{
"object": "list",
"results": [],
"next_cursor": null,
"has_more": false,
"type": "page_or_database",
"page_or_database": {}
}

If you’ve got an invalid token, you’ll get a response like this:

{
"object": "error",
"status": 401,
"code": "unauthorized",
"message": "API token is invalid."
}

Create a Page #

Now that we’ve gotten a page, we can create a sub page. Here we’re creating a page with a title of “Hello World!” and one paragraph of text. Pages need a parent object, in this case, it is the initial page object. While pages can have the workspace as a parent, as in the response above, it is not possible to create a top level page using the API.

search_results = search_response.json()["results"]
page_id = search_results[0]["id"]

create_page_body = {
"parent": { "page_id": page_id },
"properties": {
"title": {
"title": [{
"type": "text",
"text": { "content": "Hello World!" } }]
}
},
"children": [
{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{
"type": "text",
"text": {
"content": "This page was made using an Api call!"
}
}]
}
}
]
}

create_response = requests.post(
"https://api.notion.com/v1/pages",
json=create_page_body, headers=headers)
print(create_response.json())

If everything worked, you should see the page in your Notion UI and have see a response like below. Unlike the search response, it represents a single page so the object property is page and there are no pagination properties, but the content is similar.

{
"object": "page",
"id": "93eb7e7f-2c34-4c34-8379-8d097ad5664e",
"created_time": "2022-09-14T12:15:00.000Z",
"last_edited_time": "2022-09-14T12:15:00.000Z",
"created_by": {
"object": "user",
"id": "3351e084-4643-47eb-8382-516878461920"
},
"last_edited_by": {
"object": "user",
"id": "3351e084-4643-47eb-8382-516878461920"
},
"cover": null,
"icon": null,
"parent": {
"type": "page_id",
"page_id": "1c4ed430-d24d-4eba-be10-d6c11778461a"
},
"archived": false,
"properties": {
"title": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "Hello World!",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Hello World!",
"href": null
}
]
}
},
"url": "https://www.notion.so/Hello-World-93eb7e7f2c344c3483798d097ad5664e"
}

Notice that while we only needed to include a simple content property to create the title, it is returned with default values for other properties like annotations.

You can create a blank page by omitting most fields, but the properties object is required even if it’s empty.

create_blank_page_body = {
"parent": { "page_id": page_id },
"properties": {
}
}

create_blank_page_response = requests.post(
"https://api.notion.com/v1/pages",
json=create_blank_page_body, headers=headers)

Retrieve Page Content #

To retrieve the content of a page, we need to retrieve the page’s blocks. This is a GET request with the page’s id. The page itself is the parent for the top level blocks. In some cases, blocks can have children, indicated by the has_children property - for example, a link to a sub-page, and to retrieve those, you make another call to the blocks endpoint.

created_id = create_response.json()["id"]
blocks_response = requests.get(
f"https://api.notion.com/v1/blocks/{created_id}/children",
headers=headers)
print(blocks_response.json())
{
"object": "list",
"results": [
{
"object": "block",
"id": "1f5a4324-54a5-4e1c-85f0-155b175f5d4b",
"parent": {
"type": "page_id",
"page_id": "aab5cf3d-2e28-439e-83e6-fc61b0abd5fa"
},
"created_time": "2022-09-14T13:02:00.000Z",
"last_edited_time": "2022-09-14T13:02:00.000Z",
"created_by": {
"object": "user",
"id": "3351e084-4643-47eb-8382-516878461920"
},
"last_edited_by": {
"object": "user",
"id": "3351e084-4643-47eb-8382-516878461920"
},
"has_children": false,
"archived": false,
"type": "paragraph",
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": "This page was made using an APi call!",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "This page was made using an APi call!",
"href": null
}
],
"color": "default"
}
}
],
"next_cursor": null,
"has_more": false,
"type": "block",
"block": {}
}

The basics of this response look a lot like the Search response, but, the type is block rather than page_or_database

That covers the basics of working with pages in the Notion API using Python! The full code sample is on GitHub Gists.