Telegram Task Bot: Build One With the quik.md API
Build a Telegram task bot in an afternoon: message the bot and tasks land filed in the right quik.md project. Real code, cron-friendly, with a morning digest.
A Telegram task bot turns any Telegram message into a filed task: you message the bot, a small script polls getUpdates, posts the text to quik.md's capture endpoint, and the AI organizer files it into the right project. This guide builds the whole loop with two cron jobs and about sixty lines of code.
Telegram is where a lot of people already think out loud: saved messages, quick forwards, voice notes recorded mid-walk. A bot that listens to one private chat turns that habit into a task inbox without a new app to open. The working recipe below is the same one in our Telegram cron bot example, expanded with the reasoning and the reliability details.

What does the finished bot do?
The finished bot does three things. It captures: every text message you send it becomes a quik.md item, AI-filed into the right project. It reports: every morning at 07:30 it posts your open todos due today back into the chat. And it closes the loop: replying to a digest line marks that task done. Everything runs on two cron jobs, so there is no server to babysit and no webhook infrastructure to secure.
The architecture is deliberately boring. Telegram's bot API exposes getUpdates for polling, which means a Cloudflare Worker, a Vercel cron, or a spare box all work as the host. quik.md supplies the capture endpoint and the AI filing. Your script is just the glue.
Step 1: create the Telegram bot and the quik.md key
Open Telegram, message @BotFather, and run /newbot. BotFather returns a token that authenticates every call your script makes. Then in quik.md, open Settings, then Developer, and create an API key. Keys are prefixed qk_ and shown once, so store it immediately.
Put both values into environment variables on whatever runs your cron:
TELEGRAM_BOT_TOKEN=123456:abc...
TELEGRAM_CHAT_ID=<your chat id>
QUIK_KEY=qk_...
The chat id is what lets the bot message you back for the digest. The simplest way to learn yours is to send the bot any message, then call getUpdates once by hand and read message.chat.id from the response.
Step 2: how do you capture messages on a cron?
Run a poller every minute. It calls getUpdates with the last seen offset, posts each new message's text to /api/v1/capture with organize: true, and advances the offset. This is the working code:
// poll-telegram.mjs. run on a 1-minute cron
const TG = process.env.TELEGRAM_BOT_TOKEN
const QK = process.env.QUIK_KEY
let offset = Number(globalThis.OFFSET ?? 0)
const updates = await fetch(
`https://api.telegram.org/bot${TG}/getUpdates?offset=${offset}&timeout=0`,
).then((r) => r.json())
for (const u of updates.result ?? []) {
offset = u.update_id + 1
const text = u.message?.text
if (!text) continue
await fetch("https://quik.md/api/v1/capture", {
method: "POST",
headers: {
Authorization: `Bearer ${QK}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text, organize: true, source: "api" }),
})
}
globalThis.OFFSET = offset
Three details matter. The offset parameter tells Telegram which updates you have already seen, so each update is delivered once. The organize: true flag is what turns a raw message into a filed task: quik.md's server-side AI reads the text, picks the project, and writes the next step. And source: "api" marks the item so you can tell bot captures apart from keyboard ones in review.
Step 3: how do you capture voice messages?
Voice messages take one extra hop. Telegram gives you a file_id on the message; the getFile method turns that into a downloadable path. Download the bytes, forward them to quik.md's /api/v1/voice/transcribe endpoint, and pipe the returned text into /api/v1/capture exactly like a typed message.
The result is a genuine voice-driven telegram to do bot: you hold the mic in Telegram, say "follow up with Maya about the onboarding doc, Tuesday", and a filed task appears, transcript preserved. Voice messages are where this bot earns its keep, because the alternative is opening a task app with your hands full.
Step 4: send the morning digest back to Telegram
Capture without a report back is a one-way valve. A daily cron at 07:30 queries your open todos due today and posts them into the same chat:
// morning-digest.mjs. daily cron
const QK = process.env.QUIK_KEY
const TG = process.env.TELEGRAM_BOT_TOKEN
const CHAT = process.env.TELEGRAM_CHAT_ID
const today = new Date()
today.setHours(23, 59, 59, 999)
const r = await fetch(
`https://quik.md/api/v1/items/search?status=todo&due_before=${today.toISOString()}&limit=20`,
{ headers: { Authorization: `Bearer ${QK}` } },
).then((r) => r.json())
const lines = r.items.map((i) => `• ${i.title || i.content_md.slice(0, 80)}`)
const text = lines.length
? `Today's plan:\n${lines.join("\n")}`
: "Inbox zero. Take it easy."
await fetch(`https://api.telegram.org/bot${TG}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: CHAT, text }),
})
The search endpoint does the filtering: status=todo plus due_before set to the end of today, capped at 20 items. If the query comes back empty, the bot says so, which is the one notification worth getting. For the thinking behind a calm, reviewable inbox that only surfaces what matters, see the AI inbox use case.
Step 5: how do you complete tasks from Telegram?
Reply to a digest line and have the bot complete the task. The mechanics: parse the item id out of the digest line the user replied to, then call POST /api/v1/items/:id/toggle with { "is_completed": true }. Subtasks cascade automatically, so completing a parent closes its children without extra calls.
For batch completion, search first and bulk-toggle second: one call to /api/v1/items/search collects the ids, one call to /api/v1/items/bulk with op toggle marks them all done, up to 100 per request. The same search-then-bulk pattern powers most of the heavier automations in our Claude Code task inbox walkthrough, where an agent captures follow-ups it could not finish itself.
How do you keep the bot reliable?
The bot fails in three predictable ways, and all three are cheap to fix.
- Persist the offset. The example stores
globalThis.OFFSET, which resets when the runtime restarts. On a serverless cron, write the offset to a file, KV, or a database row. A resetting offset replays old updates, and every replay becomes a duplicate capture. - Respect rate limits. quik.md allows 60 requests per minute and 1,000 per day on free, 300 per minute and 10,000 per day on Pro. A one-minute poller sending a handful of messages stays far inside both. If you ever hit a 429, the
retry_aftervalue in the body tells you exactly how long to wait. - Watch the AI quota. Every
organize: truecapture spends AI budget. A chatty bot on a busy chat can burn through it; when that happens, items still capture, they just arrive unfiled. Passing aclient_iduuid per message also makes retries idempotent, so a flaky network cannot double-capture.
FAQ
How do I build a Telegram task bot?
Create a bot with @BotFather, mint a quik.md API key in Settings, then Developer, and run a one-minute cron that polls Telegram's getUpdates and POSTs each message text to /api/v1/capture. With organize set to true, quik.md's AI files each message into the right project. The whole capture loop is about twenty lines.
Can a Telegram bot turn voice messages into tasks?
Yes. Download the voice file with Telegram's getFile method, forward the bytes to quik.md's /api/v1/voice/transcribe endpoint, and pipe the returned text into /api/v1/capture. The result is the same as a typed message: a transcribed, AI-filed task.
Do I need quik.md Pro for a Telegram task bot?
No for basic capture. Posting text to /api/v1/capture with organize off works on the free plan, and items land in your inbox unfiled. AI filing with organize: true is Pro-only, so the self-filing version of the bot needs Pro. The bot in this guide uses polling, so it does not need the Pro-only webhooks feature either.
How does the bot file tasks into the right project?
Two ways. Pass organize: true and quik.md's server-side AI reads the message, picks the project, and writes the next step. Or pass a project_id explicitly if your bot already knows where the item belongs. The second option skips the AI quota cost entirely.
How do I stop duplicate tasks from my Telegram bot?
Persist the getUpdates offset somewhere durable, like a file or KV store, and send it on every poll. If the offset resets after a restart, Telegram replays old updates and every replay becomes a duplicate capture. For extra safety on retries, pass a stable client_id with each capture call so duplicates collapse server-side.
Related reads
- Telegram cron bot recipe in the API docs
- Claude Code task inbox: agent fallback captures
- Use case: the AI inbox
- AI task manager: the pillar guide
References
Keep reading
Product9 minTask Management API: The Developer's Field Guide
What a good task management API actually needs: low-friction capture, idempotent writes, search, bulk operations, and signed webhooks. A field guide that walks each requirement using the real quik.md API endpoints and rate limits.
Workflows5 minn8n Workflows for a Task Inbox: Build It Yourself or Use quik.md
n8n is great at moving signals from Telegram, Gmail, Slack, webhooks, and agents. It is not where unfinished work should live. This guide shows two paths: send automation output to quik.md, or build your own capture inbox.
Workflows9 minGTD Capture Tools: 6 Channels Ranked by Friction
Every GTD capture channel ranked by real-world friction: the physical tray, the pocket notebook, the phone widget, voice, email-to-inbox, and API automation. The ranking rule is simple, and most setups break it.