Go edit forum

Accessing Notion content

Notion.so is a web-based note-taking app. https://github.com/kjk/notionapi is a Go library that uses reverse-engineered API to download Notion pages. Those pages can then be converted to HTML.

Full API docs are at https://godoc.org/github.com/kjk/notionapi

Real-life usage scenario

One example of using the library is publishing a website. https://blog.kowalczyk.info is generated from the content stored in Notion and deployed to Netlify using a Go program.

Downloading a page

Content of Notion consists of pages.

Each page has a unique id. The id is the last part of Notion URL. For example pageĀ https://www.notion.so/Test-page-all-c969c9455d7c4dd79c7f860f3ace6429 has id c969c9455d7c4dd79c7f860f3ace6429.

You can retrieve the content of public page given its id:

import (

client := &notionapi.Client{}
pageID := "c969c9455d7c4dd79c7f860f3ace6429"
page, err := client.DownloadPage(pageID)
if err != nil {
    log.Fatalf("DownloadPage() failed with %s\n", err)
// look at page.Page to see structured content

Accessing non-public pages

To access non-public pages you need to find out authentication token.

Auth token is the value of token_v2 cookie.

In Chrome: open developer tools (Menu More Tools\Developer Tools), navigate to Application tab, look under Storage \ Cookies and copy the value of token_v2 cookie. You can do similar things in other browsers.

Then configure Client with access token::

client := &notionapi.Client{}
client.AuthToken = "value of token_v2 value"

Anatomy of a Notion page

A notion page consists of blocks. A block represents a piece of content: a text block, an image, a block of code, a sub-page, a list item etc. Each block has a Block.Type represented by one of the Block* constants.

Some blocks can have sub-blocks Block.Content.

Page.Root is the top-level block representing a page.

Getting a list of sub-pages

Notion pages are nested. If you have a notionapi.Page you can find out list of sub-pages by recursively traversing blocks:

#note could improve it

func findSubPageIDs(page *notionapi.Page) []string {
	blocks := page.Root.Content
	pageIDs := map[string]struct{}{}
	seen := map[string]struct{}{}
	toVisit := blocks
	for len(toVisit) > 0 {
		block := toVisit[0]
		toVisit = toVisit[1:]
		id := notionapi.NormalizeID(block.ID)
		if block.Type == notionapi.BlockPage {
			pageIDs[id] = struct{}{}
			seen[id] = struct{}{}
		for _, b := range block.Content {
			if b == nil {
			id := notionapi.NormalizeID(block.ID)
			if _, ok := seen[id]; ok {
			toVisit = append(toVisit, b)
	res := []string{}
	for id := range pageIDs {
		res = append(res, id)
	return res

Note that we keep track of seen ids to avoid infinite loops if blocks form loops.

You can see a complete example of recursively downloading and caching notion pages at https://github.com/kjk/blog/blob/master/notion_import.go

Converting pages to HTML

To convert a page to HTML, recursively traverse blocks starting at Page.Root.Content and convert each block to HTML. For a full example see https://github.com/kjk/blog/blob/master/notion_to_html.go

Writing data to Notion

Currently the library has very limited capabilities for writing data to Notion.

You can change page title and change format of the page.

For example, to change page title:

page, err := client.DownloadPage(pageID)
if err != nil {
    log.Fatalf("DownloadPage() failed with %s\n", err)
err = page.SetTitle("new title")
if err != nil {
    log.Fatalf("SetTitle() failed with %s\n", err)

There's no technical reason write capabilities are so limited, more could be implemented. If you need more functionality, open an issue at https://github.com/kjk/notionapi/issues

  ↑ ↓ to navigate     ↵ to select     Esc to close