In this tutorial, weâll create the simplest possible database, then create a more interesting database to track fish catches with a few different kinds of properties, finally, weâll create a database for fishing trips which shows relations and rollups to the catches database. This tutorial assumes youâve set up an integration already; if you havenât, check out Getting Started with the Notion API.
The two main types of objects in Notion are pages and databases. Like a page, a database must have a parent. The objects in a database are all pages, but unlike a page outside of a database, they can have a wide variety of named properties, that hold a specific type of data, like number, date, rich text, formulas, and more, and, like all pages, have a title.
Weâll start off with an example showing the bare minimum needed to create a database. In order to create a database, you need to send a POST request to the Database endpoint with a body that specifies the parent and at least one property.
A database needs exactly one property of type title
, so if weâre only setting one property it needs to be a title. So the bare minimum body of a create database request with a page parent, is:
minimum = {
"parent": {
"type": "page_id",
"page_id": "bbb440744be74c1fa6136499aa1d446f"
},
"properties": {
"Name": {
"title": {}
}
}
}
This will create a database which is a child of the page specified by the ID, with one property, Name.
By default in the UI, and in this example, the title property is called is âNameâ. This is distinct from the title of the database, which shows up in the Notion UI at the top of the database and where itâs linked. This of course can be set while creating the database, which weâll see in the next example, but itâs not required - as in this example, if itâs not set it will show up as âUntitledâ.
A full code sample you can run is below. Youâll need an integration key in the NOTION_KEY environment variable, and set parent_page_id
to the ID of a page that is connected to the integration. If you need a refresher on how to do that, check out my introductory post.
import os
import requests
# Load environment variables from .env file
from dotenv import load_dotenv
load_dotenv(override=True)
notion_key = os.getenv('NOTION_KEY')
# Replace this with an ID of a page in your workspace that is connected to the integration
parent_page_id = "<PAGE_ID>"
headers = {'Authorization': f"Bearer {notion_key}",
'Content-Type': 'application/json', 'Notion-Version': '2022-06-28'}
minimum = {
"parent": {
"type": "page_id",
"page_id": parent_page_id
},
"properties": {
"Name": {
"title": {}
}
}
}
url = "https://api.notion.com/v1/databases"
response = requests.post(url, json=minimum, headers=headers)
print(response.text)
If you run that code, you should see something like this in the UI on the parent page.
Screenshot of successfully created âUntitledâ database in the UI.
If you donât specify a title property, youâll get a response like this: {'object': 'error', 'status': 400, 'code': 'validation_error', 'message': 'Title is not provided'}
, and if you try to set more than one title youâll get this error instead: {'object': 'error', 'status': 400, 'code': 'validation_error', 'message': 'Multiple title properties provided'}
.
So now youâve created a simple database! But most databases you create will have more properties than the minimum, and thereâs more options that can be set. On to the next example!
In this example, weâll build a simple database with a few different types of properties to track fish weâve caught.
As in some previous posts, Iâll set up a client class with a re-usable session with our authentication headers rather than simply calling requests directly each time. This makes it easier and more performant when weâre making multiple requests.
class NotionClient():
def __init__(self, notion_key):
self.notion_key = notion_key
self.default_headers = {'Authorization': f"Bearer {self.notion_key}",
'Content-Type': 'application/json', 'Notion-Version': '2022-06-28'}
self.session = requests.Session()
self.session.headers.update(self.default_headers)
The next step is to add a method to call create database and return the response.
def create_database(self, data):
url = "https://api.notion.com/v1/databases"
response = self.session.post(url, json=data)
return response.json()
The full database create object might look something like below - maybe you can think of more properties to add!
catches = {
"parent": {
"type": "page_id",
"page_id": page_id
},
"icon": {
"type": "emoji",
"emoji": "đ"
},
"title": [
{
"type": "text",
"text": {
"content": "Catches",
"link": None
}
}
],
"properties": {
"Name": {
"title": {}
},
"Species": {
"select": {
"options": [
{
"name": "Northern Pike",
"color": "green"
},
{
"name": "Walleye",
"color": "red"
},
{
"name": "Smallmouth Bass",
"color": "pink"
}
]
}
},
"Weight (lbs)": {
"number": {}
},
"Location": {
"rich_text": {}
},
"Date": {
"date": {}
}
}
}
Letâs break that down a bit. Like in the minimal example, we specify a parent, but we add in an icon and a database title. The title is the name of the whole database that will show up in various parts of the UI, not to be confused with the title property, which shows up as a column in table view and in various places in other views. The icon is entirely optional, but adds a bit of visual flair to the database that will show up next to the title. The fish emoji seemed appropriate here! You can also specify an external image instead of an emoji; Notionâs Icons arenât currently supported by the API.
"parent": {
"type": "page_id",
"page_id": page_id
},
"icon": {
"type": "emoji",
"emoji": "đ"
},
"title": [
{
"type": "text",
"text": {
"content": "Catches",
"link": None
}
}
],
Finally, we get to the properties. The properties of the database show up as the columns in table view, and represent the types of information stored with each page in the database. Properties are specified as an object where the name of the property is the key, and the type of the property and sometimes other configuration is specified in the value.
The documentation for how to define properties and all of their options is under Property Schema Objects. The general structure of a property looks like this, though very often there are no configuration options or they are optional.
"properties": {
"Property Name" : {
"property_type": {"configuration_option": "configuration value"}
}
}
Lets look at the simple properties first. Like title
, these all specify the type with an empty configuration object {}
. Of these types, number
is the only one that currently supports any extra configuration, a format, but the default is fine for this use case (it would let us set things like percentage, currency, or a thousands separator).
"Name": {
"title": {}
},
"Weight (lbs)": {
"number": {}
},
"Location": {
"rich_text": {}
},
"Date": {
"date": {}
},
We also have one field thatâs a bit more complicated: a select
field, which allows you to select values from a list. When defining a select field you can define the available options, with a name and optionally a color (it defaults to gray). You can also add to this list later via the UI or a separate API call.
"Species": {
"select": {
"options": [
{
"name": "Northern Pike",
"color": "green"
},
{
"name": "Walleye",
"color": "red"
},
{
"name": "Smallmouth Bass",
}
]
}
}
Now we can pass the catches object to create_database
to create a new database.
catches_create_response = notion_client.create_database(catches)
print(json.dumps(catches_create_response, indent=2))
The full code up to this point, with some argument parsing and loading environment variables, looks like this:
import json
import os
import requests
class NotionClient():
def __init__(self, notion_key):
self.notion_key = notion_key
self.default_headers = {'Authorization': f"Bearer {self.notion_key}",
'Content-Type': 'application/json', 'Notion-Version': '2022-06-28'}
self.session = requests.Session()
self.session.headers.update(self.default_headers)
def create_database(self, data):
url = "https://api.notion.com/v1/databases"
response = self.session.post(url, json=data)
return response.json()
def main(page_id):
notion_client = NotionClient(os.getenv('NOTION_KEY'))
# Create a database with some properties
catches = {
"parent": {
"type": "page_id",
"page_id": page_id
},
"icon": {
"type": "emoji",
"emoji": "đ"
},
"title": [
{
"type": "text",
"text": {
"content": "Catches",
"link": None
}
}
],
"properties": {
"Name": {
"title": {}
},
"Weight (lbs)": {
"number": {}
},
"Location": {
"rich_text": {}
},
"Date": {
"date": {}
},
"Species": {
"select": {
"options": [
{
"name": "Northern Pike",
"color": "green"
},
{
"name": "Walleye",
"color": "red"
},
{
"name": "Smallmouth Bass",
"color": "pink"
}
]
}
}
}
}
catches_create_response = notion_client.create_database(catches)
print(json.dumps(catches_create_response, indent=2))
if __name__ == "__main__":
import argparse
from dotenv import load_dotenv
load_dotenv(override=True)
parser = argparse.ArgumentParser(description='Example script to create notion databases.')
parser.add_argument('page_id', type=str,
help='A Notion Page ID to create the databases under')
args = parser.parse_args()
main(args.page_id)
Now that weâve created a database, we can create a second database with a relation property.
Letâs say we want to keep track of the catches weâve made on different fishing trips, and provide some summary information about the trip. In order to do this, we need to create a relation to the Catches database, and, set up some rollups.
First off, weâll set up the parent page and title like before.
"parent": {
"type": "page_id",
"page_id": page_id
},
"title": [
{
"type": "text",
"text": {
"content": "Trips",
"link": None
}
}
],
Another thing we can add to a database is a cover image, which shows at the top of the page in the UI. Letâs add a scenic fishing photo from Unsplash:
"cover": {
"type": "external",
"external": {
"url": "https://images.unsplash.com/photo-1597956959732-9b8cb12f8960?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80"
}
},
And weâll start off our properties with our title
property, called Trip Name:
"properties": {
"Trip Name": {
"title": {}
},
In order to add a Relation property to another database, we need the ID of that other database. This is returned in the response to a create database request, so we can get our catches id in the same script by running: catches_create_response.get("id")
. The other option on relation properties is the type, which can be dual_property
or single_property
. A single_property
relation will add a property to just the current database, while dual_property
will add it to both databases, making it a âtwo wayâ relation.
So if we want to add a relation to the Catches database, that shows up in both databases, it looks like this.
"Catches": {
"relation": {
"database_id": catches_create_response.get("id"),
"type": "dual_property",
"dual_property": {}
}
}
When youâve got a Relation property, very often you also want to define one or more Rollup properties. Rollups bring in data from the related database, and have a function to aggregate that data, like sum
to add up the values, count_values
to count how many values there are, or range
to compute the time between the latest and earliest dates in a date property.
Hereâs an example of two rollups we might have in our trips database, to summarize how much we caught:
"Total Weight Caught": {
"rollup": {
"rollup_property_name": "Weight (lbs)",
"relation_property_name": "Catches",
"function": "sum"
}
},
"Total Fish Caught": {
"rollup": {
"rollup_property_name": "Name",
"relation_property_name": "Catches",
"function": "count_values"
}
}
So all together, our database schema object and the request to make the database looks like:
trips = {
"parent": {
"type": "page_id",
"page_id": page_id
},
"title": [
{
"type": "text",
"text": {
"content": "Trips",
"link": None
}
}
],
"cover": {
"type": "external",
"external": {
"url": "https://images.unsplash.com/photo-1597956959732-9b8cb12f8960?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80"
}
},
"properties": {
"Trip Name": {
"title": {}
},
"Catches": {
"relation": {
"database_id": catches_create_response.get("id"),
"type": "dual_property",
"dual_property": {}
}
},
"Total Weight Caught": {
"rollup": {
"rollup_property_name": "Weight (lbs)",
"relation_property_name": "Catches",
"function": "sum"
}
},
"Total Fish Caught": {
"rollup": {
"rollup_property_name": "Name",
"relation_property_name": "Catches",
"function": "count_values"
}
}
}
}
print("*** Creating Trips Database ***")
trips_create_response = notion_client.create_database(trips)
print(json.dumps(trips_create_response, indent=2))
The full code example is available on Github Gists.
A couple things I ran into while preparing this tutorial