May 13, 2021

Sending Contact Form Submissions to Notion

With Notion launching their public API beta a couple of hours ago, I wanted to try out to pipe my newly-added contact form submissions to a Notion database.

Database pages in notion are made up of a combination of page properties and content. Properties such as the page name (title), when the page was created, and custom properties are structured as key-value properties in the API, whereas page content is an array of blocks, such as paragraphs, to-do items, bullet points, and so on.

My contact form allows me to send submissions as webhoooks, including all submitted content. This can be transformed on the fly to result in a page creation request to the Notion API.

Since I wanted to prototype this quickly, I used my existing API built with Go to receive webhook requests containing the submission and transform them into Notion API calls.

Preparing your database

Before we start, we need to create a new database for our form submissions. We'll create two additional properties, Person (text) which represents the name of the sender, and Email (email). These two fields are the important metadata we want to have attached for every submission.

Creating an integration

Head over to the new integration page and create a new integration, linked only to your workspace of choice. This will give you a secret (internal integration token) you can copy for later on.

Sharing your database with the integration

Now that you have created an integration, it's time to share your database with it. This is important, as the integration does not have permission to access any of your content unless you say so.

In your database of choice, click the Share button on the top right of Notion, and tap on add people, emails, groups, or integrations. Then select your newly-created integration. You should now your integration in the list of entities with access, with Can edit as the permission set.

To identify your database page later on, make sure to click on Copy link in the same share dialog, then pick out the ID behind your workspace name (e.g. https://www.notion.so/<workspace>/<database_id> ). Copy this for later.

Transforming webhooks into Notion API calls

Now it's time to send a request to the Notion API, to create a page. Our form submission knows about the user's name, email address, and has a subject, as well as multi-line content. To keep it simple, except for the content, we'll store all details as properties. The content will then be used as the page content.

Let's first start by declaring both the Notion API secret and database ID. For production use, you should supply both as environment variables, not hard-coded.

const NotionSecret = "< your integration secret >"
const DatabaseId = "< your database id >"

The following types are rough representations of what we'll need, but keep in mind, that the API resources contain much more than that. For more details, visit the API reference.

type DatabaseParent struct {
	DatabaseID string `json:"database_id"`
}

type TextContent struct {
	Content string `json:"content"`
}

type Text struct {
	Type string      `json:"type"`
	Text TextContent `json:"text"`
}

type Paragraph struct {
	Text []Text `json:"text"`
}

type BlockObject struct {
	Object    string     `json:"object"`
	Type      string     `json:"type"`
	Paragraph *Paragraph `json:"paragraph"`
}

type CreatePage struct {
	Parent     *DatabaseParent        `json:"parent"`
	Properties map[string]interface{} `json:"properties"`
	Children   []*BlockObject         `json:"children"`
}

So now that we have our types in place, let's build and send the final request

func pipeIntoNotion(
	ctx context.Context,
  client *http.Client,
	name,
	email,
	subject,
	content string
) (*http.Response, error) {
	body := CreatePage{
		Parent: &DatabaseParent{
			DatabaseID: DatabaseId,
		},
		Properties: map[string]interface{}{
			// Page name / title -> Subject
			"Name": map[string]interface{}{
				"title": []map[string]interface{}{
					{
						"type": "text",
						"text": map[string]interface{}{
							"content": subject,
						},
					},
				},
			},
			// Email of user
			"Email": map[string]interface{}{
				"email": email,
			},
			// Name of user
			"Person": map[string]interface{}{
				"text": []map[string]interface{}{
					{
						"type": "text",
						"text": map[string]interface{}{
							"content": name,
						},
					},
				},
			},
		},
		// Content of submission
		Children: []*BlockObject{
			{
				Object: "block",
				Type:   "paragraph",
				Paragraph: &Paragraph{
					Text: []Text{
						{
							Type: "text",
							Text: TextContent{
								Content: content,
							},
						},
					},
				},
			},
		},
	}

	bodyJson, err := json.Marshal(body)
	if err != nil {
		return nil, fmt.Errorf("could not marshal body")
	}

	bodyReader := bytes.NewReader(bodyJson)

	req, err := http.NewRequestWithContext(
		ctx,
		"POST",
		"https://api.notion.com/v1/pages",
		bodyReader,
	)
	if err != nil {
		return nil, fmt.Errorf("could not create request")
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", NotionSecret))

	res, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("could not send notion api request")
	}

	return res, nil
}

As you can see, apart from building the request, we didn't do anything special. We added an authorization header based on your API secret, and sent the request.

As you can see, the page properties are a blob of key-value JSON. If you would like to learn more about property values, check out the reference.

The page content itself is a simple text block, once again, check out the reference for more details.

Putting it all together

Now that we prepared the request to be sent to Notion, we can try submitting the form.

As you can see, while there's a slight delay between submitting the form and receiving it in Notion, it eventually arrives in all its beauty.

Extra: cURL

If you need to implement this solution in another language, it might help to check out the cURL representation of the request we sent to the Notion API

curl --request POST \
  --url https://api.notion.com/v1/pages \
  --header 'Authorization: Bearer <secret>' \
  --header 'Content-Type: application/json' \
  --data '{
	"parent": {
		"database_id": "<database id>"
	},
	"properties": {
		"Name": {
			"title": [
				{
					"text": {
						"content": "<your form submission subject>"
					}
				}
			]
		},
		"Email": {
			"email": "<email of sender>"
		},
		"Person": {
			"text": {
        "type": "text",
				"text": {
          "content": "<name of sender>"
        }
			}
    }
	},
	"children": [
		{
			"object": "block",
			"type": "paragraph",
			"paragraph": {
				"text": [
					{
						"type": "text",
						"text": {
							"content": "<The submission content goes here>"
						}
					}
				]
			}
		}
	]
}'