Skip to content

Notion Integration Guide

Status: Operational | Last Updated: 2026-02-20


Overview

The Yappa Knowledge Hub uses Notion as a primary knowledge management platform, providing a structured database system for organizing knowledge items, categories, and digest reports. The integration enables bidirectional synchronization between the application and Notion, allowing teams to manage knowledge through both the Slack bot interface and Notion's powerful database features.

Key Features

  • Bidirectional Sync: Changes in Notion reflect in the app and vice versa
  • Three-Database Architecture: Knowledge Items, Categories, and Digest Reports
  • Rich Property Mapping: Full support for Notion's property types
  • Automatic Pagination: Handles large datasets efficiently
  • Rate Limiting: Built-in retry logic with exponential backoff
  • Error Recovery: Comprehensive error handling and logging

Database Architecture

1. Knowledge Items Database

Purpose: Central repository for all knowledge entries captured from Slack or created directly in Notion.

Database ID: 306e292a-15d5-8004-a8cb-c222dcd48bb2

URL: https://notion.so/306e292a15d58004a8cbc222dcd48bb2

Schema:

PropertyTypeRequiredDescription
TitleTitleYesKnowledge item title
ContentRich TextYesMain content (max 2000 chars)
TagsMulti-SelectNoCategorization tags (150+ options)
StatusStatusNoNot started, In progress, Done
CategoriesRelationNoLinks to Categories database
Source URLURLNoOriginal source link
AuthorPeopleNoContent author
AttachmentsFilesNoAttached files
Slack IDRich TextNoSlack user ID reference
ChannelRich TextNoSlack channel reference
TSRich TextNoSlack message timestamp
AI SummaryRich TextNoAI-generated summary

Relations:

  • Categories Links to Categories database (many-to-many)

2. Categories Database

Purpose: Organizational structure for grouping knowledge items by topic, team, or purpose.

Database ID: 306e292a-15d5-805d-ae13-e64bed8519c5

URL: https://notion.so/306e292a15d5805dae13e64bed8519c5

Schema:

PropertyTypeRequiredDescription
TitleTitleYesCategory name
DescriptionRich TextNoCategory description
Default Target GroupsMulti-SelectNoDevelopers, DevOps, Product Managers, etc.
Knowledge ItemsRelationNoLinks to Knowledge Items database
Digest ReportsRelationNoLinks to Digest Reports database
SubscribersPeopleNoPeople subscribed to category
RollupRollupNoCount of Knowledge Items
CreatedCreated TimeNoAuto-populated
IconPage IconNoEmoji icon (set via API)

Relations:

  • Knowledge Items Links to Knowledge Items database (many-to-many)
  • Digest Reports Links to Digest Reports database (many-to-many)

Note: Category icons are set via the page icon API, not as a database property.

3. Digest Reports Database

Purpose: Curated weekly/periodic reports summarizing knowledge items for distribution.

Database ID: 306e292a-15d5-80d7-a0f6-fe8421baff10

URL: https://notion.so/306e292a15d580d7a0f6fe8421baff10

Schema:

PropertyTypeRequiredDescription
TitleTitleYesReport title
Period StartDateYesDigest period start
Period EndDateYesDigest period end
StatusStatusNoNot started, In progress, Done
Items CountNumberNoTotal items in digest
CategoriesRelationNoLinks to Categories database
Generated ByPeopleNoReport generator
Generated AtCreated TimeNoAuto-populated
RecipientsRich TextNoReport recipients
Slack SentCheckboxNoWhether sent via Slack
Target GroupsMulti-SelectNoTarget audience

Relations:

  • Categories Links to Categories database (many-to-many)

Sync Architecture

Bidirectional Synchronization

The application maintains data in both SQLite (local) and Notion (cloud), with the following sync patterns:

Create Flow

User Action (Slack)  Backend API  SQLite (create)  Notion (create)  Link records
  1. User creates knowledge via Slack bot
  2. Backend creates record in SQLite database
  3. Backend creates corresponding page in Notion
  4. SQLite record updated with notionId and notionUrl

Read Flow

API Request  Notion Query  Transform  Return to Client
  1. API queries Notion database directly
  2. Results transformed using NotionPropertyMapper
  3. Data returned to Slack bot or frontend

Update Flow

Update Request  SQLite (update)  Notion (update)  Confirm
  1. Update applied to SQLite record
  2. Corresponding Notion page updated via API
  3. Changes synchronized bidirectionally

Search Flow

Search Query  SQLite Search + Notion Search  Merge Results  Return
  1. Query executed against both SQLite and Notion
  2. Results merged with Notion results prioritized
  3. Deduplicated results returned

Sync Service Architecture

The NotionSyncService handles all synchronization operations:

File: https://github.com/undead2146/KnowledgeHub/blob/main/backend/src/Service/Notion/NotionSyncService.php

Key Methods:

  • createKnowledge() - Creates in both SQLite and Notion
  • searchKnowledge() - Searches both sources and merges results
  • getKnowledge() - Retrieves from SQLite or falls back to Notion
  • syncToNotion() - Batch syncs existing SQLite records to Notion

Sync Status Tracking:

  • SQLite entities have notionId and notionUrl fields
  • Records without notionId are considered unsynced
  • Batch sync processes unsynced records in configurable batches

Property Mappings

PHP to Notion Mapping

The NotionPropertyMapper class handles all property transformations:

File: https://github.com/undead2146/KnowledgeHub/blob/main/backend/src/Service/Notion/NotionPropertyMapper.php

PHP TypeNotion TypeMapper MethodExample
stringtitletitle($text)['title' => [['text' => ['content' => 'Title']]]]
stringrich_textrichText($text)['rich_text' => [['text' => ['content' => 'Text']]]]
arraymulti_selectmultiSelect($values)['multi_select' => [['name' => 'tag1'], ['name' => 'tag2']]]
stringselectselect($value)['select' => ['name' => 'Option']]
stringstatusstatus($status)['status' => ['name' => 'In progress']]
arrayrelationrelation($pageIds)['relation' => [['id' => 'uuid1'], ['id' => 'uuid2']]]
stringurlurl($url)['url' => 'https://example.com']
stringdatedate($start, $end)['date' => ['start' => '2026-02-20', 'end' => null]]
intnumbernumber($value)['number' => 42]
boolcheckboxcheckbox($value)['checkbox' => true]

Notion to PHP Extraction

Notion TypeExtractor MethodReturn Type
titleextractTitle($property)string
rich_textextractRichText($property)string
multi_selectextractMultiSelect($property)array
selectextractSelect($property)string|null
statusextractStatus($property)string|null
relationextractRelation($property)array
urlextractUrl($property)string|null
dateextractDate($property)array|null
numberextractNumber($property)int|null
checkboxextractCheckbox($property)bool

Special Handling

Rich Text Truncation:

php
// Notion has a 2000 character limit for rich text
NotionPropertyMapper::richText(substr($text, 0, 2000))

Relation Validation:

php
// Only add relations if valid Notion UUID format
if ($this->isValidNotionId($categoryId)) {
    $properties['Categories'] = NotionPropertyMapper::relation([$categoryId]);
}

Icon Handling:

php
// Icons are set via page icon API, not as properties
$icon = ['type' => 'emoji', 'emoji' => ''];
$this->client->createPageWithIcon($databaseId, $properties, $icon);

Rate Limiting and Error Handling

Rate Limits

Notion API Limits:

  • Free tier: 3 requests per second
  • Team plan: Higher limits (varies)
  • Burst limit: Short bursts allowed, then throttled

Retry Strategy

The NotionClient implements exponential backoff for rate limit errors:

Configuration:

php
private const MAX_RETRIES = 3;
private const RETRY_DELAY_MS = 1000;

Retry Logic:

  1. 429 (Rate Limited): Exponential backoff (1s, 2s, 4s)
  2. 5xx (Server Error): Fixed 1s delay between retries
  3. 4xx (Client Error): No retry, throw exception immediately

Example:

php
// Attempt 1: Immediate
// Attempt 2: Wait 1000ms (1s)
// Attempt 3: Wait 2000ms (2s)
// Attempt 4: Wait 4000ms (4s)
$delay = RETRY_DELAY_MS * pow(2, $attempt - 1);

Error Handling

Exception Types:

  • NotionException - Custom exception for Notion-specific errors
  • RuntimeException - Generic runtime errors
  • GuzzleException - HTTP client errors

Error Logging:

php
$this->logger->warning("Notion API error", [
    'method' => $method,
    'endpoint' => $endpoint,
    'attempt' => $attempt,
    'status' => $statusCode,
]);

Common Error Codes:

  • 400 - Invalid request (bad property format)
  • 401 - Unauthorized (invalid API key)
  • 403 - Forbidden (no access to resource)
  • 404 - Not found (database/page doesn't exist)
  • 429 - Rate limited (too many requests)
  • 500 - Server error (Notion internal error)
  • 503 - Service unavailable (Notion maintenance)

Pagination Handling

Automatic Pagination:

php
public function queryDatabaseWithPagination(
    string $databaseId,
    array $filter = [],
    array $sorts = [],
    int $pageSize = 100
): array {
    $allResults = [];
    $hasMore = true;
    $startCursor = null;

    while ($hasMore) {
        $payload = ['page_size' => $pageSize];

        if ($startCursor !== null) {
            $payload['start_cursor'] = $startCursor;
        }

        $response = $this->request('POST', "databases/{$databaseId}/query", $payload);

        $allResults = array_merge($allResults, $response['results'] ?? []);
        $hasMore = $response['has_more'] ?? false;
        $startCursor = $response['next_cursor'] ?? null;
    }

    return ['results' => $allResults, 'has_more' => false, 'next_cursor' => null];
}

Benefits:

  • Automatically fetches all pages
  • No manual cursor management required
  • Handles large datasets (1000+ items)
  • Logs progress for monitoring

Troubleshooting

Common Issues

1. "unauthorized" or "invalid_api_key"

Symptoms: All API requests fail with 401 error

Causes:

  • API key is invalid or expired
  • API key not properly set in environment variables
  • Integration doesn't have access to workspace

Solutions:

bash
# Verify API key in .env
cat backend/.env | grep NOTION_API_KEY

# Regenerate integration token
# 1. Go to https://www.notion.so/my-integrations
# 2. Select your integration
# 3. Click "Show" under Internal Integration Token
# 4. Copy and update .env file

# Restart backend server
cd backend
php -S localhost:8000 -t public

2. "object_not_found" for database

Symptoms: Database queries fail with 404 error

Causes:

  • Database ID is incorrect
  • Integration doesn't have access to database
  • Database was deleted or moved

Solutions:

bash
# Verify database ID
cat backend/.env | grep NOTION_DATABASE

# Share database with integration
# 1. Open database in Notion
# 2. Click "..." menu (top right)
# 3. Click "Add connections"
# 4. Select your integration
# 5. Click "Confirm"

# Test database access
curl -X POST "https://api.notion.com/v1/databases/YOUR_DB_ID/query" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Notion-Version: 2022-06-28" \
  -H "Content-Type: application/json" \
  -d '{"page_size": 1}'

3. "validation_error" on page creation

Symptoms: Creating pages fails with 400 error

Causes:

  • Property names don't match database schema
  • Property types are incorrect
  • Multi-select values don't exist in database
  • Relation IDs are invalid

Solutions:

php
// Verify property names (case-sensitive)
// Correct: 'Title', 'Content', 'Tags'
// Wrong: 'title', 'content', 'tags'

// Check property types match
$properties = [
    'Title' => NotionPropertyMapper::title($data['title']),  // Not richText!
    'Content' => NotionPropertyMapper::richText($data['content']),
    'Tags' => NotionPropertyMapper::multiSelect($data['tags']),  // Not select!
];

// Validate relation IDs
if ($this->isValidNotionId($categoryId)) {
    $properties['Categories'] = NotionPropertyMapper::relation([$categoryId]);
}

4. Empty query results

Symptoms: Queries return no results despite database having content

Causes:

  • Integration doesn't have read permissions
  • Filter is too restrictive
  • Database is actually empty

Solutions:

bash
# Verify database has content
# Open database in Notion and check for pages

# Test without filters
curl -X POST "https://api.notion.com/v1/databases/YOUR_DB_ID/query" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Notion-Version: 2022-06-28" \
  -H "Content-Type: application/json" \
  -d '{"page_size": 10}'

# Check integration permissions
# 1. Open database in Notion
# 2. Click "..." menu
# 3. Click "Connections"
# 4. Verify your integration is listed

5. Relation property not working

Symptoms: Relations don't appear in Notion or fail to create

Causes:

  • Relation not configured in database schema
  • Invalid page ID format
  • Related page doesn't exist
  • Integration doesn't have access to related database

Solutions:

php
// Validate UUID format
private function isValidNotionId(string $id): bool {
    return preg_match(
        '/^[a-f0-9]{8}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{12}$/i',
        $id
    ) === 1;
}

// Check relation exists in schema
// 1. Open database in Notion
// 2. Check property list for relation property
// 3. Verify it points to correct database

// Ensure both databases are shared with integration
// Relations require access to both databases

6. Rate limiting (429 errors)

Symptoms: Requests fail with "rate_limited" error

Causes:

  • Too many requests in short time
  • Batch operations without delays
  • Multiple concurrent processes

Solutions:

php
// Use built-in retry logic (automatic)
// NotionClient handles 429 with exponential backoff

// Add delays in batch operations
foreach ($items as $item) {
    $this->notionClient->createPage($databaseId, $properties);
    usleep(350000); // 350ms delay = ~3 requests/second
}

// Use pagination instead of multiple queries
$results = $this->client->queryDatabaseWithPagination($databaseId);

Debugging Tips

Enable Debug Logging:

php
// Check backend logs
tail -f backend/var/log/dev.log

// Look for Notion API requests
grep "Notion API" backend/var/log/dev.log

Test API Directly:

bash
# Get database schema
curl "https://api.notion.com/v1/databases/YOUR_DB_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Notion-Version: 2022-06-28"

# Query database
curl -X POST "https://api.notion.com/v1/databases/YOUR_DB_ID/query" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Notion-Version: 2022-06-28" \
  -H "Content-Type: application/json" \
  -d '{"page_size": 10}'

# Get specific page
curl "https://api.notion.com/v1/pages/PAGE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Notion-Version: 2022-06-28"

Verify Environment:

bash
# Check all Notion environment variables
cd backend
php -r "
echo 'API Key: ' . (getenv('NOTION_API_KEY') ? 'Set' : 'Missing') . PHP_EOL;
echo 'Version: ' . getenv('NOTION_VERSION') . PHP_EOL;
echo 'Knowledge DB: ' . getenv('NOTION_DATABASE_KNOWLEDGE') . PHP_EOL;
echo 'Categories DB: ' . getenv('NOTION_DATABASE_CATEGORIES') . PHP_EOL;
echo 'Digests DB: ' . getenv('NOTION_DATABASE_DIGESTS') . PHP_EOL;
"

Best Practices

1. Property Naming

  • Use exact property names from Notion (case-sensitive)
  • Verify property names before deployment
  • Document custom properties in code comments

2. Data Validation

  • Always validate relation IDs before using
  • Check multi-select values exist in database
  • Truncate rich text to 2000 characters
  • Validate URLs before setting URL properties

3. Error Handling

  • Always wrap Notion API calls in try-catch blocks
  • Log errors with context (database ID, page ID, operation)
  • Provide fallback behavior for non-critical operations
  • Don't retry on 4xx errors (except 429)

4. Performance

  • Use pagination for large datasets
  • Batch operations with appropriate delays
  • Cache frequently accessed data
  • Use filters to reduce result sets

5. Sync Strategy

  • Sync immediately for user-initiated actions
  • Use batch sync for background operations
  • Track sync status with notionId field
  • Handle partial failures gracefully