· 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:

  1. A user sends a proposed quote.
  2. The bot forwards a formatted preview to an approver group or specific reviewer.
  3. The reviewer taps Approve or Reject.
  4. 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:

  • sendMessage to submit and route quotes
  • inline_keyboard to expose approval actions
  • callback_query updates to capture button taps
  • editMessageText or editMessageReplyMarkup to 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_id
  • requester_chat_id
  • requester_user_id
  • approver_chat_id or approver list
  • quote_text
  • status (pending, approved, rejected)
  • reviewed_by
  • reviewed_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_revision status

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.

← All posts