An order comes in, and you find out two hours later. Whichever WooCommerce Telegram plugin you install, the WordPress log and the order note record the same error: cURL error 7: Failed to connect to api.telegram.org port 443: Connection refused — or its more patient version, cURL error 28: Connection timed out. This article gives three working ways around this error, with complete code and a list of the things that break along the way.
Why this happens
Three things work together:
1. Your server cannot connect to Telegram. When a plugin wants to send a message, PHP on your server makes an HTTPS request to api.telegram.org. api.telegram.org is filtered inside Iran.
Which error you get depends on how the blocking is done, and that is itself a troubleshooting clue:
cURL error 7: Connection refused— the firewall injected an RST packet. The connection is refused immediately.cURL error 28: Connection timed out— packets are silently dropped. The server waits until the timeout.
The second error is worse, because every order locks up the server for a few seconds. Neither case has anything to do with the bot, the token or the chat ID — the request never leaves the server.
2. Sanctions from the other side. Even without filtering, some intermediary services block Iranian IPs themselves. That is why the "find a free foreign service" fix usually dies after two weeks.
3. WordPress does not support SOCKS proxies natively. The WP_HTTP_Proxy class only understands HTTP proxies with BASIC authentication. Most proxies available to people in Iran are SOCKS5. So even when you have a proxy, WordPress will not use it without extra code.
The result: your problem is not "the right plugin". Your problem is the path your traffic takes out of the server. Until you fix that, no plugin will work.
The fix, step by step, with code
Three methods, from the most stable to the fastest.
Method 1: a relay on Cloudflare Workers (main recommendation)
The logic is simple: instead of Telegram, your server calls an address on Cloudflare. Cloudflare delivers the message to Telegram. It is free, needs no foreign server, and the free plan allows 100,000 requests a day, which is more than enough for any store.
Step 1 — Create the bot. In Telegram, message @BotFather, send /newbot and copy the token. Then add the bot to the managers' group or channel and make it an admin. To get the chat ID, message @getmyid_bot in that same group.
Step 2 — Create the Worker. In the Cloudflare dashboard, create a new Worker and put this code in it:
const SHARED_SECRET = "CHANGE_ME_TO_A_LONG_RANDOM_STRING";
export default {
async fetch(request) {
if (request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
if (request.headers.get("X-Relay-Secret") !== SHARED_SECRET) {
return new Response("Forbidden", { status: 403 });
}
let payload;
try {
payload = await request.json();
} catch (e) {
return new Response("Bad Request", { status: 400 });
}
const { bot_token, method, params } = payload;
if (!bot_token || !method) {
return new Response("Missing bot_token or method", { status: 400 });
}
const upstream = `https://api.telegram.org/bot${bot_token}/${method}`;
const tgResponse = await fetch(upstream, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params || {}),
});
return new Response(await tgResponse.text(), {
status: tgResponse.status,
headers: { "Content-Type": "application/json" },
});
},
};
Step 3 — Connect a custom domain. This is the most important step, and most tutorials skip it: the default *.workers.dev domain is filtered inside Iran. If you stay on the default address, you hit exactly the same dead end as before.
Connect a subdomain of your own domain (for example relay.yoursite.ir) to the Worker in Cloudflare:
Workers & Pages → your-worker → Settings → Domains & Routes → Add Custom Domain
Step 4 — Test from the server. SSH into your own server, not your laptop:
curl -s -X POST https://relay.yoursite.ir \
-H "Content-Type: application/json" \
-H "X-Relay-Secret: CHANGE_ME_TO_A_LONG_RANDOM_STRING" \
-d '{"bot_token":"123456:ABC-DEF","method":"sendMessage","params":{"chat_id":"-1001234567890","text":"relay ok"}}'
If you get {"ok":true,...}, the path is open. If you get Connection refused or a timeout, the domain or DNS is not set up correctly.
Step 5 — The WordPress code. Put this in a small plugin or in your child theme's functions.php:
<?php
defined( 'TG_RELAY_URL' ) || define( 'TG_RELAY_URL', 'https://relay.yoursite.ir' );
defined( 'TG_RELAY_SECRET' ) || define( 'TG_RELAY_SECRET', 'CHANGE_ME_TO_A_LONG_RANDOM_STRING' );
defined( 'TG_BOT_TOKEN' ) || define( 'TG_BOT_TOKEN', '123456:ABC-DEF' );
defined( 'TG_CHAT_ID' ) || define( 'TG_CHAT_ID', '-1001234567890' );
/**
* Queue the notification. Never send inside the checkout request.
* Args are positional on purpose: Action Scheduler spreads them with
* call_user_func_array, and on PHP 8 string keys become named arguments.
*/
add_action( 'woocommerce_order_status_processing', 'hami_queue_order_notice', 10, 1 );
function hami_queue_order_notice( $order_id ) {
if ( ! $order_id ) {
return;
}
// 4th arg = $unique: prevents a duplicate pending action for the same order.
as_enqueue_async_action( 'hami_send_order_notice', array( (int) $order_id ), 'telegram', true );
}
add_action( 'hami_send_order_notice', 'hami_send_order_notice_handler', 10, 1 );
function hami_send_order_notice_handler( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
// Idempotency guard: never notify twice for the same order.
if ( $order->get_meta( '_hami_tg_sent' ) ) {
return;
}
$lines = array();
$lines[] = '🛒 <b>سفارش جدید</b> #' . $order->get_order_number();
$lines[] = '👤 ' . esc_html( $order->get_formatted_billing_full_name() );
$lines[] = '📞 ' . esc_html( $order->get_billing_phone() );
$lines[] = '💰 ' . wp_strip_all_tags( $order->get_formatted_order_total() );
$lines[] = '💳 ' . esc_html( $order->get_payment_method_title() );
$lines[] = '';
// Trim the item list BEFORE building HTML, never the finished string.
$items = $order->get_items();
$item_count = count( $items );
$max_items = 12;
foreach ( array_slice( $items, 0, $max_items ) as $item ) {
$lines[] = '• ' . esc_html( $item->get_name() ) . ' × ' . $item->get_quantity();
}
if ( $item_count > $max_items ) {
$lines[] = '<i>و ' . ( $item_count - $max_items ) . ' قلم دیگر…</i>';
}
$lines[] = '';
$lines[] = '<a href="' . esc_url( $order->get_edit_order_url() ) . '">مشاهده در پیشخوان</a>';
$response = wp_remote_post(
TG_RELAY_URL,
array(
'timeout' => 15,
'headers' => array(
'Content-Type' => 'application/json',
'X-Relay-Secret' => TG_RELAY_SECRET,
),
'body' => wp_json_encode(
array(
'bot_token' => TG_BOT_TOKEN,
'method' => 'sendMessage',
'params' => array(
'chat_id' => TG_CHAT_ID,
'text' => implode( "\n", $lines ),
'parse_mode' => 'HTML',
'link_preview_options' => array( 'is_disabled' => true ),
),
)
),
)
);
// Network-level failure: relay unreachable, DNS, TLS.
if ( is_wp_error( $response ) ) {
$order->add_order_note( 'Telegram: relay unreachable — ' . $response->get_error_message() );
hami_schedule_telegram_retry( $order_id );
return;
}
$status = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
// Transient: Telegram rate limit or relay-side error. Worth retrying.
if ( 429 === $status || $status >= 500 ) {
$order->add_order_note( "Telegram: transient error {$status}, retrying in 5 min" );
hami_schedule_telegram_retry( $order_id );
return;
}
// Permanent: bad token, bad chat_id, broken entities. Retrying is pointless.
if ( empty( $body['ok'] ) ) {
$order->add_order_note( 'Telegram: permanent API error — ' . wp_remote_retrieve_body( $response ) );
return;
}
$order->update_meta_data( '_hami_tg_sent', current_time( 'mysql' ) );
$order->save();
}
/**
* Action Scheduler does NOT retry failed async actions — only recurring ones
* get rescheduled. Retries have to be scheduled explicitly.
*/
function hami_schedule_telegram_retry( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$retries = (int) $order->get_meta( '_hami_tg_retry_count' );
if ( $retries >= 3 ) {
$order->add_order_note( 'Telegram: giving up after 3 attempts' );
$order->save();
return;
}
$order->update_meta_data( '_hami_tg_retry_count', $retries + 1 );
$order->save();
as_schedule_single_action( time() + 300, 'hami_send_order_notice', array( (int) $order_id ), 'telegram' );
}
Four things in this code set it apart from the usual snippets:
- Sending goes through the Action Scheduler queue, so checkout does not slow down.
- Retries are scheduled manually. Many people assume that if they throw an
Exceptioninside the callback, Action Scheduler retries on its own. It does not. The action is simply markedfailedand that is the end of it; only recurring actions get rescheduled. If you rely on that assumption, messages are lost silently. - Transient errors are separated from permanent ones. 429 and 5xx errors are worth retrying; a wrong token or chat ID is not, and only clutters the queue.
- Trimming happens at the source, not on the final string. You will see why in the next section.
Method 2: Google Apps Script (plan B, without Cloudflare)
If you cannot get Cloudflare working, the same relay logic can be built on Google Apps Script. The Order and Stock Notifications via Telegram Bot for WooCommerce plugin (version 1.0.3) supports both Apps Script and Cloudflare Workers out of the box and publishes the script sources in its GitHub repository. For someone who does not write code, this is the fastest route.
Method 3: a proxy directly on the server
If you have your own proxy, you can make WordPress use it. For an HTTP proxy, in wp-config.php:
define( 'WP_PROXY_HOST', '10.0.0.5' );
define( 'WP_PROXY_PORT', '3128' );
define( 'WP_PROXY_USERNAME', 'user' );
define( 'WP_PROXY_PASSWORD', 'pass' );
// Critical: keep local and Iranian services off the proxy.
define( 'WP_PROXY_BYPASS_HOSTS', 'localhost, *.ir, api.zarinpal.com, *.shaparak.ir' );
For SOCKS5, which WordPress does not support natively, you have to work with cURL directly:
add_action( 'http_api_curl', function ( $handle, $args, $url ) {
if ( false === strpos( $url, 'api.telegram.org' ) ) {
return; // Only proxy Telegram traffic.
}
curl_setopt( $handle, CURLOPT_PROXY, '127.0.0.1' );
curl_setopt( $handle, CURLOPT_PROXYPORT, 1080 );
curl_setopt( $handle, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME );
}, 10, 3 );
That strpos is not optional. Without it, all of the site's outgoing traffic goes through the proxy and your payment gateway goes down.
What breaks
Read this section carefully. These are the things that catch up with you two weeks after launch.
Checkout slows down or fails. If you send the message directly inside the woocommerce_checkout_order_processed hook, the customer waits on a blank page until the Telegram request responds. If the relay is unreachable, that wait lasts as long as the timeout. In other words, an outage at Cloudflare hits your conversion rate directly. The fix is what the code above does: Action Scheduler or 'blocking' => false.
A global proxy kills the payment gateway. WP_PROXY_HOST applies to all of WordPress's outgoing requests: core updates, plugin licences, the Iran Post and Tipax APIs, and worst of all the payment gateway. If your Shaparak payment requests go out through a foreign IP, transactions fail. Take WP_PROXY_BYPASS_HOSTS seriously.
Customer data passes through a third-party server. Some ready-made plugins solve this same filtering problem with a shared intermediary server. One example is Notify Bot for WooCommerce (version 2.6.1), whose official documentation states openly, in its 3rd Party Services section, that when proxy mode is enabled, requests pass through the developer's own domain (tl.alijvhr.com).
The developer added this for convenience and did not hide it. But architecturally, it means your bot token and your customers' phone numbers and addresses pass through a server you do not control. That is fine for testing; for a store whose messages contain personal data, it is not. With a dedicated relay on your own domain, this problem does not arise at all.
A second point about the same plugin: in the repository's readme.txt, Tested up to is still WordPress 6.8.2 and WC tested up to is WooCommerce 10.1.2 — three major versions behind the current state. Test it on staging before installing it on a live store.
workers.dev is filtered. It is worth repeating. You create a Worker, test it from your laptop with a VPN and it works, then it does not respond from the server. Connect a custom domain.
Duplicate messages. An order can change status several times: pending, then processing, then processing again from the payment gateway. Without an idempotency lock, the managers' group fills up with duplicate messages, and after two days nobody looks at it anymore.
Telegram's rate limit. Telegram limits sending to a single group to about 20 messages per minute. On a sale day, the message queue comes back with 429 Too Many Requests and messages are lost. If you have high traffic, batch orders into one-minute windows and send them in a single message.
Markdown breaks. A product name that contains _, * or [ causes a can't parse entities error with parse_mode: Markdown, and the message never arrives. HTML is safer, as long as you run the output through esc_html.
The 4096-character limit and broken message structure. An order with 30 items goes over this limit, and Telegram discards the whole message rather than trimming it. The real trap is elsewhere, though: the intuitive fix is to shorten the text with mb_substr. If the message is HTML, that is dangerous. If the cut falls in the middle of a <b> or an <a href="...">, the tag stays open and Telegram rejects the whole message with 400 Bad Request: can't parse entities — exactly what you were trying to prevent, only worse.
The right approach is to limit the number of items at the source (for example the first 12 items, then "and X more items…") so that the dashboard link and the tags always stay intact. That is what the code above does with array_slice.
Outdated CA certificates. On older Iranian servers, cURL error 60 is common. It means the path is open but the server cannot verify the other side's certificate. Update ca-certificates; do not set sslverify => false.
WP-Cron is asleep. Action Scheduler runs on top of WP-Cron. If your site gets little traffic or DISABLE_WP_CRON is on, the queue does not run and messages arrive late. Set up a real cron job on the server.
The system dies silently. This is the worst case: the relay goes down, orders keep coming in, no message arrives, and you think you had no sales. Send a daily heartbeat — a "system is alive" message at a fixed time. The absence of that message is itself the alert.
Frequently asked questions
Can WooCommerce be connected to Telegram without a foreign server? Yes. The Cloudflare Workers or Apps Script method exists for exactly this. It needs no foreign VPS, just a domain whose DNS is on Cloudflare.
What does the Failed to connect to api.telegram.org port 443 error mean?
It means your server never reached Telegram. Do not change the token or the chat ID; it is a network problem. You need to set up a relay or a proxy.
Does a Telegram webhook work on a server in Iran? The incoming direction usually has fewer problems, because Telegram connects to your server rather than the other way round. The condition is that the domain has a valid SSL certificate and uses one of Telegram's allowed ports (443, 80, 88, 8443). If your host restricts incoming foreign traffic, it will not respond. The simplest approach is to set the same Worker as the webhook destination and forward from there to your site.
Can I change an order's status from Telegram? Yes, but you also need a return path. With an inline keyboard and a webhook, you can put "Approve" and "Cancel" buttons under each order. The Notify Bot plugin has this ready-made; for a dedicated version, the logic is the same relay with an endpoint on the WordPress side.
What about selling directly inside Telegram? There are two models. The simple one: the bot only sends the product's purchase link and payment happens on the site — the most stable option for Iran. The complex one: the whole cart is built inside the bot and submitted through the WooCommerce REST API. The second model needs more maintenance and involves more work with Iranian payment gateways.
Which versions? At the time of writing, according to the official WordPress repository API, the latest WooCommerce version is 11.1.0, and it requires at least WordPress 7.0 and PHP 7.4. The current WordPress core version is 7.1.
The code above is compatible with HPOS, because it uses wc_get_order and the $order object's methods throughout and never goes straight to the post_meta table.
Summary
Connecting WooCommerce to Telegram from a server in Iran is not a plugin problem; it is a problem of the path your traffic takes out of the server, and a simple relay solves it. The hard part is maintenance: the queue, preventing duplicate messages, a correct proxy bypass, and a heartbeat that tells you when the system has gone quiet.
If you would rather not maintain this chain yourself, at hami9.ir we set up this same path as a dedicated service for stores: a dedicated relay on your own domain, an automated n8n workflow for periodic reports, and connection health monitoring.
