Notion API

It is needed once per few years and each time took some time to recover knowledge, so writing a note for myself

Let's pretend in Notion we have an movies database with some sample fields

We will use following script to get some sample movies

$genres = Invoke-RestMethod "https://api.themoviedb.org/3/genre/movie/list?language=en" -Headers @{ Authorization = "Bearer $($env:TMDB)" } | Select-Object -ExpandProperty genres
$movies = @()
$movies += Invoke-RestMethod "https://api.themoviedb.org/3/movie/popular?language=en-US&page=1" -Headers @{ Authorization = "Bearer $($env:TMDB)" } | Select-Object -ExpandProperty results | Select-Object title, overview, @{n = 'poster'; e = { "https://image.tmdb.org/t/p/w500$($_.poster_path)" } }, @{n = 'rating'; e = { $_.vote_average } }, @{n = 'year'; e = { [int]$_.release_date.Substring(0, 4) } }, @{n = 'genres'; e = { $_.genre_ids | ForEach-Object { $genres | Where-Object id -eq $_ | Select-Object -ExpandProperty name } } }
$movies += Invoke-RestMethod "https://api.themoviedb.org/3/movie/top_rated?language=en-US&page=1" -Headers @{ Authorization = "Bearer $($env:TMDB)" } | Select-Object -ExpandProperty results | Select-Object title, overview, @{n = 'poster'; e = { "https://image.tmdb.org/t/p/w500$($_.poster_path)" } }, @{n = 'rating'; e = { $_.vote_average } }, @{n = 'year'; e = { [int]$_.release_date.Substring(0, 4) } }, @{n = 'genres'; e = { $_.genre_ids | ForEach-Object { $genres | Where-Object id -eq $_ | Select-Object -ExpandProperty name } } }
$movies | ConvertTo-Json | Out-File movies.json

Next step is to configure and database in Notion with corresponding columts

After that is done we gonna need to

https://www.notion.so/profile/integrations

Create integration, and grab its token

Then, in notion, click on tripple dots, and choose created integration from connections menu

After that following sample scripts should work

Invoke-RestMethod -Method Post "https://api.notion.com/v1/search" -Headers @{ Authorization = "Bearer $($env:NOTION)"; "Notion-Version" = "2022-06-28" } -ContentType "applicaion/json" -Body (ConvertTo-Json -Depth 100 -InputObject @{
  query = ""
  filter = @{
    property = "object"
    value = "database"
  }
}) | Select-Object -ExpandProperty results | Where-Object object -EQ "database" | Select-Object id, @{n="title";e={ $_.title.plain_text }}

in my case it was

id title
1d637d44-7d4b-80f8-b5a6-c9977db0f450 movies

note: technically it is in url, but without dashes

having database id in place we should be able to do some basic CRUD

create

Invoke-RestMethod -Method Post "https://api.notion.com/v1/pages" -Headers @{ Authorization = "Bearer $($env:NOTION)"; "Notion-Version" = "2022-06-28" } -ContentType "application/json" -Body (ConvertTo-Json -Depth 100 -InputObject @{
    parent     = @{ database_id = "1d637d44-7d4b-80f8-b5a6-c9977db0f450" }
    properties = @{
      Title    = @{ title = @(@{ text = @{ content = "Inception" } }) }
      Genres   = @{ multi_select = @( @{ name = "Sci-Fi" }, @{ name = "Thriller" } ) }
      Rating   = @{ number = 8.8 }
      Year     = @{ number = 2010 }
      Overview = @{ rich_text = @(@{ text = @{ content = "A thief who steals corporate secrets through dream-sharing tech is given the inverse task of planting an idea." } }) }
      Poster   = @{ files = @( @{ type = "external"; name = "poster.jpg"; external = @{ url = "https://image.tmdb.org/t/p/w500/xlaY2zyzMfkhk0HSC5VUwzoZPU1.jpg" } } ) }
    }
  })

Note: more details of how properties are composed can be found here

read

Invoke-RestMethod -Method Get "https://api.notion.com/v1/pages/1d637d44-7d4b-81dc-a4a3-f943559bd2d2" -Headers @{ Authorization = "Bearer $($env:NOTION)"; "Notion-Version" = "2022-06-28" }

update

Invoke-RestMethod -Method Patch "https://api.notion.com/v1/pages/1d637d44-7d4b-81dc-a4a3-f943559bd2d2" -Headers @{ Authorization = "Bearer $($env:NOTION)"; "Notion-Version" = "2022-06-28" } -ContentType "application/json" -Body (ConvertTo-Json -Depth 100 -InputObject @{
    parent     = @{ database_id = "1d637d44-7d4b-80f8-b5a6-c9977db0f450" }
    properties = @{
      Title      = @{ title = @(@{ text = @{ content = "Inception2" } }) }
      # Genres   = @{ multi_select = @( @{ name = "Sci-Fi" }, @{ name = "Thriller" } ) }
      # Rating   = @{ number = 8.8 }
      Year       = @{ number = 2020 }
      # Overview = @{ rich_text = @(@{ text = @{ content = "A thief who steals corporate secrets through dream-sharing tech is given the inverse task of planting an idea." } }) }
      # Poster   = @{ files = @( @{ type = "external"; name = "poster.jpg"; external = @{ url = "https://image.tmdb.org/t/p/w500/74hLDKjD5aGYOotO6esUVaeISa2.jpg" } } ) }
    }
  })

Note: with Patch you may update wanted fields

delete

Invoke-RestMethod -Method Patch "https://api.notion.com/v1/pages/1d637d44-7d4b-81dc-a4a3-f943559bd2d2" -Headers @{ Authorization = "Bearer $($env:NOTION)"; "Notion-Version" = "2022-06-28" } -ContentType "application/json" -Body (ConvertTo-Json -Depth 100 -InputObject @{ archived = $true })

Now, when we have basic CRUD, lets fill the database with movies

seed

$movies = Get-Content movies.json | ConvertFrom-Json
foreach ($movie in $movies) {
  Invoke-RestMethod -Method Post "https://api.notion.com/v1/pages" -Headers @{ Authorization = "Bearer $($env:NOTION)"; "Notion-Version" = "2022-06-28" } -ContentType "application/json" -Body (ConvertTo-Json -Depth 100 -InputObject @{
      parent     = @{ database_id = "1d637d44-7d4b-80f8-b5a6-c9977db0f450" }
      properties = @{
        Title    = @{ title = @(@{ text = @{ content = $movie.title } }) }
        Genres   = @{ multi_select = $movie.genres | ForEach-Object { @{ name = $_ } } }
        Rating   = @{ number = $movie.rating }
        Year     = @{ number = $movie.year }
        Overview = @{ rich_text = @(@{ text = @{ content = $movie.overview } }) }
        Poster   = @{ files = @( @{ type = "external"; name = "poster.jpg"; external = @{ url = $movie.poster } } ) }
      }
    })
}

and very last one

query

Invoke-RestMethod -Method Post "https://api.notion.com/v1/databases/1d637d44-7d4b-80f8-b5a6-c9977db0f450/query" -Headers @{ Authorization = "Bearer $($env:NOTION)"; "Notion-Version" = "2022-06-28" } -ContentType "application/json" -Body (ConvertTo-Json -Depth 100 -InputObject @{
    filter = @{
      and = @(
        @{
          property     = "Genres"
          multi_select = @{
            contains = "Action"
          }
        }
        @{
          property = "Year"
          number   = @{
            greater_than = 2020
          }
        }
      )
    }
  }) | Select-Object -ExpandProperty results | Select-Object @{n='title';e={ $_.Properties.Title.title.plain_text }}, @{n='year';e={ $_.Properties.Year.number }}, @{n='genres';e={ $_.Properties.Genres.multi_select | Select-Object -ExpandProperty name }}

note: more on how to query fields can be found here

rich_text

note, rich text fields may contain basic markup

that's the reason why it is returned as array

once you have some markup it may look like:

[
  {
    "type": "text",
    "text": {
      "content": "The adventures of a group of explorers who make use of a newly "
    }
  },
  {
    "type": "text",
    "text": {
      "content": "discovered"
    },
    "annotations": {
      "bold": true
    }
  },
  {
    "type": "text",
    "text": {
      "content": " wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage."
    }
  }
]

in example above, i have removed defaulted falsy properties, note how one bold word is extracted into dedicated array item.

while creating - same shape should be used.

children

each record in database is actual page, so it can contain any children

Invoke-RestMethod -Method Post "https://api.notion.com/v1/pages" -Headers @{ Authorization = "Bearer $($env:NOTION)"; "Notion-Version" = "2022-06-28" } -ContentType "application/json" -Body (ConvertTo-Json -Depth 100 -InputObject @{
    parent     = @{ database_id = "1d637d44-7d4b-80f8-b5a6-c9977db0f450" }
    properties = @{
      Title    = @{ title = @(@{ text = @{ content = "Children2" } }) }
      Genres   = @{ multi_select = @( @{ name = "Sci-Fi" }, @{ name = "Thriller" } ) }
      Rating   = @{ number = 8.8 }
      Year     = @{ number = 2010 }
      Overview = @{ rich_text = @(@{ text = @{ content = "A thief who steals corporate secrets through dream-sharing tech is given the inverse task of planting an idea." } }) }
      Poster   = @{ files = @( @{ type = "external"; name = "poster.jpg"; external = @{ url = "https://image.tmdb.org/t/p/w500/74hLDKjD5aGYOotO6esUVaeISa2.jpg" } } ) }
    }
    children   = @(
      @{
        object    = "block"
        type      = "paragraph"
        paragraph = @{
          rich_text = @(
            @{
              type = "text"
              text = @{
                content = "A thief who steals corporate secrets through dream-sharing tech is given the inverse task of planting an idea."
              }
            }
          )
        }
      },
      # header
      @{
        object    = "block"
        type      = "heading_2"
        heading_2 = @{
          rich_text = @(
            @{
              type = "text"
              text = @{
                content = "Children"
              }
            }
          )
        }
      }
      # image
      @{
        object = "block"
        type   = "image"
        image  = @{
          type     = "external"
          external = @{
            url = "https://image.tmdb.org/t/p/w500/74hLDKjD5aGYOotO6esUVaeISa2.jpg"
          }
        }
      }
    )
  })

note: technically this children is same as properties but can be anything