Create a Notion Database using the API

Let’s learn how to create databases in Notion, from a super simple example to a database with Relations and Roll Ups.

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.

Bare Minimum Example #

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.

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!

More interesting example: Fish Catches #

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))

Full Code for Creating a Single Database #

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)

Creating a Database with Relations and Rollups: Fishing Trips #

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_valuesto 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.

Notes #

A couple things I ran into while preparing this tutorial

  • I wanted to create a formula using the Rollup properties, but I found I couldn’t do this during the initial create. It can be done using a call to Update.
  • You can’t set format on a Formula (and it doesn’t come back with the response either if it’s set in UI). This might mean it’s part of the View rather than the Database, and there’s no API for views yet.
  • The order of the properties in the response seems to be entirely arbitrary, not the order it’s set or the order in the UI
  • You can relate a database to itself, for example, imagine a People database which has a relation for Manager.