From Website Form to Telegram Manager
This tutorial demonstrates how to build a complete pipeline that takes a user-submitted form on your site and routes it to a Telegram manager. The flow is entirely client‑server: the browser posts the form data to a PHP endpoint, your server creates a persistent lead_id (a 14‑character hexadecimal string derived from seven random bytes), persists the record in a relational database, and later handles Telegram’s callback_query responses. By the end you’ll have a self‑contained system that works over HTTPS webhooks without requiring sessions or complex state management.
### Architecture Overview The system consists of three distinct phases. Phase 1 is the frontend form – a standard HTML <form> with a hidden field named take whose value encodes the lead_id. When the user submits, the browser sends both the form fields and this ID to your Telegram webhook. Phase 2 is the backend handler. It receives the POST, extracts the take parameter, generates a fresh lead_id if missing, inserts the row into the leads table, and returns a 200 OK response immediately. Phase 3 is the Telegram side. After the form is processed, Telegram fires an update event containing the update_id and optionally a callback_query_id. Your controller reads these fields, performs any side effects (e.g., marking the lead as processed), and answers the callback query to keep the conversation alive.
### Database Schema First, create a leads table that stores the generated lead_id along with the original form data:
CREATE TABLE leads (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
lead_id VARCHAR(32) UNIQUE NOT NULL, -- 14‑char hex from random_bytes(7)
name VARCHAR(255),
email VARCHAR(255),
phone VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
The lead_id column is defined as VARCHAR(32) to accommodate the maximum 14‑character hex string. A unique constraint prevents accidental collisions. Indexing this column would speed up lookups if you frequently retrieve leads by their ID.
### Webhook Handler Your webhook must listen for POST /webhook. Below is a minimal yet robust handler written in PHP 8.x:
<?php
// webhook.php
header('Content-Type: application/json');
$json = json_decode(file_get_contents('php://input'), true);
// Basic validation – reject malformed requests early
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
exit('Invalid JSON');
}
$take = $json['take'] ?? null;
$formData = $json['form'] ?? [];
// If no take_id was supplied, generate a brand‑new one
if ($take === null || empty($take)) {
$take = bin2hex(random_bytes(7));
}
// Persist the lead record atomically
$stmt = $pdo->prepare(
"INSERT INTO leads (lead_id, name, email, phone) VALUES (?, ?, ?, ?)"
);
$stmt->execute([$take, $formData['name'], $formData['email'], $formData['phone']]);
$leadId = $pdo->lastInsertId();
// Telegram expects a fast acknowledgment
http_response_code(200);
echo json_encode(['status' => 'ok']);
Key points: - Input validation – reject any request that isn’t valid JSON. - Fallback ID generation – if the caller omits take, we create a new lead_id using bin2hex(random_bytes(7)). This guarantees uniqueness across millions of submissions. - Prepared statement – protects against SQL injection. - Immediate response – returning 200 right away keeps Telegram happy; heavy processing happens inside the next webhook cycle.
### Inline Button Creation On the frontend, render an inline keyboard that points to your webhook with the lead_id encoded in the query string:
<script>
function createTakeButton(leadId) {
return [
{ type: 'button', text: 'Send to Manager', url: `?take=${encodeURIComponent(leadId)}` }
];
}
</script>
When the user taps the button, the browser navigates to https://yourdomain.com/webhook?take=abc123def456. Your PHP handler will receive the identical take value, ensuring that the subsequent processing sees the same identifier used during the initial form post.
### Updating Leads via Webhook Events After the form is successfully stored, Telegram will deliver an update event. The payload typically looks like:
{
"update": {
"id": 12345,
"type": "message",
"message": {
"from": "<chat_id>",
"text": "Form submitted"
},
"callback_query_id": 67890
}
}
Your controller should extract the update_id and, if desired, trigger additional actions (e.g., sending a confirmation message). Because each lead_id maps to a stable row, you can safely replay the same update_id without fear of double‑processing.
// Inside the webhook handler, after receiving the update
$update = $json['update'];
$lead = $pdo->query("SELECT * FROM leads WHERE lead_id = ?")->fetch();
if ($lead) {
// Mark the lead as processed – this is just an example
$lead->update([
'status' => 'processed',
'updated_at' => date('Y-m-d H:i:s')
]);
}
Using a dedicated status column makes it trivial to track which leads have been handled and which are still pending.
### Callback Handling Telegram may reply to your bot with a callback_query (for inline button clicks, inline keyboard presses, etc.). You must answer with an answerCallbackQuery to maintain the conversation thread:
if (isset($json['message']['callback_query'])) {
$cb = $json['message']['callback_query'];
$text = $json['message']['text'];
$response = [
'message' => [
'type' => 'text',
'text' => '✅ Lead received. We\'ll follow up shortly.'
]
];
http_response_code(200);
echo json_encode($response);
}
Only the id of the callback query is guaranteed to exist. Always pair it with the corresponding update_id if you need to correlate events across different parts of your workflow.
### Idempotency and Edge Cases One subtle but important consideration is duplicate submissions. If a user double‑clicks the form or experiences network latency, Telegram might deliver the same update_id twice. Without idempotency checks, you could accidentally create two lead records. The solution is to store the update_id alongside the lead_id (or simply check the status before acting):
-- Pseudocode for idempotent processing
SELECT status FROM lead_updates WHERE update_id = ?
If the status is already 'processed', skip the work. Otherwise, proceed and then mark it as done.
Another edge case involves the ok field returned by Telegram. Occasionally, a submission fails silently and Telegram sets ok: false. Your handler should still treat the take parameter as valid—if the submission succeeded, ok will be true; if it failed, you should log the discrepancy for investigation rather than discarding the lead outright.
### Security and Best Practices - HTTPS – Always serve your webhook over TLS; Telegram requires it. - Rate limiting – Implement throttling (e.g., via a Redis-based token bucket) to protect against abuse. - CSRF protection – If your form is also served from the same domain, consider adding a CSRF token that matches the take value. - Timeouts – Set a reasonable timeout on the webhook handler (e.g., 30 seconds) to avoid hanging connections. - Logging – Record every webhook arrival with timestamps and the update_id for debugging.
### Summary By combining a unique lead_id generated with bin2hex(random_bytes(7)), a reliable PHP webhook, and proper idempotent processing, you can reliably bridge a website form to a Telegram manager. The pattern scales horizontally, survives network retries, and keeps your data consistent across restarts. Follow the steps above, test thoroughly with tools like ngrok for local development, and you’ll have a production‑ready integration in place.
BotCreator — studio that ships Telegram bots / Mini Apps.
Further reading: Telegram Bot API.