· 5 min read
Building a Quote Approval Bot with the Telegram Bot API
A practical tutorial for Telegram developers who want to use the Bot API for a new workflow: collecting, reviewing, and approving quotes inside chat. We’ll combine inline keyboards, callback queries, message editing, and persistent state to build a compact approval flow that feels native to Telegram.
Telegram bots are often used for alerts, reminders, or simple command responders. A more interesting use case is a quote approval bot: sales reps, freelancers, or internal teams can submit a drafted quote, route it to an approver, and record the decision directly in Telegram.
This tutorial shows how to build that workflow with the Telegram Bot API. The emphasis is on the parts that matter in production: structured state, callback handling, safe message edits, and a clean approval loop.
What this bot does
The bot will support a simple pipeline:
- A user sends a proposed quote.
- The bot forwards a formatted preview to an approver group or specific reviewer.
- The reviewer taps Approve or Reject.
- The bot updates the original message and notifies the requester.
This is a useful pattern whenever chat itself is the workspace and you want to avoid switching to a separate admin panel.
Core Bot API features used
You do not need anything exotic, but you do need a few Bot API features working together:
sendMessageto submit and route quotesinline_keyboardto expose approval actionscallback_queryupdates to capture button tapseditMessageTextoreditMessageReplyMarkupto reflect the final decision- persistent storage to track which quote is awaiting which decision
If you already know how to reply to messages and parse updates, the main challenge here is designing the state flow cleanly.
Data model
Keep your storage simple. A quote approval record should include:
quote_idrequester_chat_idrequester_user_idapprover_chat_idor approver listquote_textstatus(pending,approved,rejected)reviewed_byreviewed_at- optional metadata such as customer name, currency, and amount
Even if you are using a lightweight database like SQLite or a key-value store, make sure every callback can be resolved back to a single quote record.
Suggested flow
A reliable approval flow has three distinct steps.
1. Submit the quote
You can accept a message like:
/quote ACME Corp — 12 seats — $4,800 annual
Or you can support a richer form:
/quote
Customer: ACME Corp
Plan: 12 seats annual
Amount: $4,800
Notes: Includes onboarding support
When the bot receives the submission, it should:
- validate the content
- create a
quote_id - save the record as pending
- send a preview to the approver
2. Present approval buttons
Use an inline keyboard with at least two buttons:
- Approve
- Reject
Optionally add:
- Request revision
- View history
The callback data should encode the action and the quote ID. For example:
approve:12345
reject:12345
Keep callback payloads short. Telegram callback data has size limits, so do not stuff the whole quote into the button payload.
3. Finalize the decision
When the approver taps a button:
- verify the user is allowed to review that quote
- check that the quote is still pending
- update the database
- edit the review message to show the final status
- notify the requester
This last step matters because it closes the loop in the chat itself. An unresolved button row looks unfinished and can confuse future reviewers.
Example implementation outline
Below is a Python-style outline using a typical Bot API wrapper. The exact library may differ, but the architecture stays the same.
from datetime import datetime
# Pseudocode: replace with your storage layer
quotes = {}
def handle_quote_submission(message, bot):
requester_chat_id = message.chat.id
requester_user_id = message.from_user.id
quote_text = message.text.removeprefix('/quote').strip()
if not quote_text:
bot.send_message(requester_chat_id, 'Please include a quote after /quote.')
return
quote_id = str(len(quotes) + 1)
quotes[quote_id] = {
'quote_id': quote_id,
'requester_chat_id': requester_chat_id,
'requester_user_id': requester_user_id,
'quote_text': quote_text,
'status': 'pending',
'reviewed_by': None,
'reviewed_at': None,
}
keyboard = {
'inline_keyboard': [[
{'text': 'Approve', 'callback_data': f'approve:{quote_id}'},
{'text': 'Reject', 'callback_data': f'reject:{quote_id}'}
]]
}
bot.send_message(
chat_id=APPROVER_CHAT_ID,
text=f"New quote pending review\n\nID: {quote_id}\n{quote_text}",
reply_markup=keyboard
)
bot.send_message(requester_chat_id, f'Quote {quote_id} submitted for approval.')
def handle_callback(callback_query, bot):
action, quote_id = callback_query.data.split(':', 1)
user_id = callback_query.from_user.id
quote = quotes.get(quote_id)
if not quote:
bot.answer_callback_query(callback_query.id, 'Quote not found.')
return
if quote['status'] != 'pending':
bot.answer_callback_query(callback_query.id, 'This quote has already been reviewed.')
return
if user_id not in APPROVER_USER_IDS:
bot.answer_callback_query(callback_query.id, 'You are not allowed to review this quote.')
return
quote['status'] = 'approved' if action == 'approve' else 'rejected'
quote['reviewed_by'] = user_id
quote['reviewed_at'] = datetime.utcnow().isoformat()
status_text = 'APPROVED' if action == 'approve' else 'REJECTED'
bot.edit_message_text(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id,
text=f"Quote {quote_id} {status_text}\n\n{quote['quote_text']}"
)
bot.send_message(
quote['requester_chat_id'],
f"Your quote {quote_id} was {quote['status']} by {user_id}."
)
bot.answer_callback_query(callback_query.id, f'Quote {quote_id} {quote["status"]}.')
This example is intentionally simple, but it demonstrates the essential mechanics:
- the quote content is stored outside callback data
- the callback identifies the record by ID
- the final state is persisted before the UI is updated
Important production details
1. Always verify authorization
Do not trust that a button tap came from the intended reviewer. Anyone who can see the message may be able to interact with it, depending on where you posted it. Check the user ID against an allowlist, a role table, or the source chat membership before taking action.
2. Make callback handling idempotent
Callback queries can be retried, messages can be double-tapped, and webhooks can be delivered more than once. The handler should accept that a quote may already be finalized and respond gracefully.
A good rule: if the quote is no longer pending, do not change it again.
3. Edit the message instead of sending a new one
When a quote is approved or rejected, editing the original review message keeps the chat readable. If you need an audit trail, you can still send a separate confirmation message to a log channel.
A common pattern is:
- edit the review message to show the final outcome
- post a compact audit note to an admin log chat
- notify the requester privately or in a team chat
4. Keep state in your own storage
Telegram is the transport, not your database. If the bot restarts and you have not persisted pending reviews, you may lose the ability to process callbacks correctly.
At minimum, store:
- pending quotes
- approver mapping
- review outcomes
- the message ID you need for later edits
5. Handle long quotes safely
If quotes can be verbose, consider truncating the preview in the review message and storing the full text in your database. Telegram messages have length limits, and you do not want approval to fail because someone pasted an entire proposal into the command.
A better UX: revision requests
In many teams, approval is not binary. A reviewer may want changes before approval. Add a Request revision button to make the bot more useful:
- reviewer taps Request revision
- bot asks for a short note
- requester receives the note and a link back to resubmit
- quote remains in
needs_revisionstatus
This is where Telegram bots become more than simple command processors. They can model an actual workflow.
Security and audit suggestions
If this bot is used for real business decisions, treat it like a workflow system:
- log every state transition
- record the acting user and timestamp
- separate requester and reviewer permissions
- consider restricting approvals to a private group
- include a unique quote ID in every message for traceability
If the bot will approve monetary quotes, you should also think about replay protection and duplicate approvals. A simple optimistic lock on the record is often enough: only one transition from pending to a final state should succeed.
Testing the workflow
Before deploying, test these cases:
- submitting an empty quote
- approving a non-existent quote
- double-clicking Approve
- rejecting after approval
- unauthorized user tapping a button
- bot restart with pending callbacks in storage
The hardest bugs in bot workflows are usually not message parsing problems. They are state problems.
Extending the pattern
Once the approval bot works, you can adapt the same structure to other Telegram workflows:
- purchase request approvals
- content moderation queues
- leave requests
- support escalation approvals
- deployment confirmations
The Telegram Bot API is especially effective when a task can be reduced to a small number of state transitions and a few clearly labeled actions.
Conclusion
A quote approval bot is a strong example of what the Telegram Bot API does well: fast interaction, clear decision buttons, and lightweight workflow automation inside chat. The key to making it reliable is not the buttons themselves, but the surrounding state management, authorization checks, and message updates.
If you design the bot so every action maps to a durable record, you get a workflow that is easy to use for reviewers and easy to maintain for developers.