{ }
Published on

Send Notion Updates to Telegram with Cloudflare Workers (2025 Guide)

Authors
  • avatar
    Name
    Ahmed Farid
    Twitter
    @

TIP

Cloudflare’s free Workers + Cron give you 1 000 000 calls/day—perfect to glue Notion and Telegram without a server.

We’ll create a Cloudflare Worker that runs every 5 minutes:

  1. Queries a Notion database via REST API.
  2. Stores the lastSynced timestamp in KV.
  3. Filters pages created/updated after that timestamp.
  4. Sends nicely-formatted messages to a Telegram bot.
  5. Updates lastSynced.

All for free, no external hosts.

Table of Contents

1. Prerequisites & Terminology

  • Cloudflare account (Workers & KV enabled).
  • Notion integration created & added to your workspace (with DB read access).
  • Telegram bot token & chat ID.
TermMeaning
Cron TriggerCloudflare feature to run worker on schedule
KVGlobal key-value store with eventual consistency
Filter timestamplastSynced ISO value stored in KV

2. Create & Configure Worker

npm create cloudflare@latest notion-tg
cd notion-tg

Choose TypeScript and KV.

wrangler.toml:

name = "notion-tg"
main = "src/index.ts"
compatibility_date = "2025-07-30"

[[kv_namespaces]]
binding = "STORE"
id = "<KV_NAMESPACE_ID>"

[[triggers]]
crons = ["*/5 * * * *"]

Add secrets:

wrangler secret put NOTION_TOKEN
wrangler secret put NOTION_DATABASE_ID
wrangler secret put TG_TOKEN
wrangler secret put TG_CHAT_ID

3. Worker Code

src/index.ts:

import { Env } from './types'

export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    const since = (await env.STORE.get('lastSynced')) ?? '1970-01-01T00:00:00Z'

    const notionResp = await fetch(
      'https://api.notion.com/v1/databases/' + env.NOTION_DATABASE_ID + '/query',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${env.NOTION_TOKEN}`,
          'Notion-Version': '2022-06-28',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          filter: {
            timestamp: 'last_edited_time',
            last_edited_time: { after: since },
          },
          sorts: [{ timestamp: 'created_time', direction: 'ascending' }],
        }),
      }
    )

    const { results } = await notionResp.json<any>()
    if (!results.length) return

    const messages = results.map((p: any) => {
      const title = p.properties.Name?.title?.[0]?.plain_text ?? 'Untitled'
      const url = p.url
      const edited = p.last_edited_time.split('T')[0]
      return `📝 <b>${title}</b>\n${url}\nEdited: ${edited}`
    })

    await Promise.all(messages.map((m) => sendTelegram(env, m)))
    await env.STORE.put('lastSynced', new Date().toISOString())
  },
}

async function sendTelegram(env: Env, text: string) {
  const url = `https://api.telegram.org/bot${env.TG_TOKEN}/sendMessage`
  return fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      chat_id: env.TG_CHAT_ID,
      text,
      parse_mode: 'HTML',
      disable_web_page_preview: false,
    }),
  })
}

src/types.d.ts:

export interface Env {
  NOTION_TOKEN: string
  NOTION_DATABASE_ID: string
  TG_TOKEN: string
  TG_CHAT_ID: string
  STORE: KVNamespace
}

4. Deploy & Test

wrangler deploy

Trigger manually:

wrangler tail &
wrangler schedule --cron "*/1 * * * *" --env production

You should see Telegram messages in chat when you add/edit pages.

5. Rate-Limiting & Error Handling

  • Notion API limit: 3 req/sec. Worker only calls once per run → safe.
  • Telegram limit: 20 msgs/minute. Use Promise.all plus setTimeout if huge bursts.
  • Wrap fetch in try/catch and ctx.waitUntil to ensure completion after response.

6. Security & Maintenance

✅ Secrets stored via wrangler secret.
✅ KV namespace is free up to 1 GiB.
✅ Rotate Notion integration token yearly.
✅ Add Sentry’s @sentry/cloudflare-worker for error monitoring.

7. Extending Further

  • Support rich embeds (photo, document).
  • Filter by Status property (e.g., only Published).
  • Include diffs in updates via before/after comparison.
  • Use Web Subscriptions once Notion releases push webhooks.

8. Conclusion

With <100 lines of code you’ve built a serverless bridge from Notion to Telegram that runs for free and needs zero DevOps. Happy automating! 🤖