"{$f}", $fields)) : '' . implode(', ', $fields) . ''; return '' . '
1Export' . 'CollectionAWCompanyList
' . '' . '$$SysName:XML' . '' . 'Company' . $body . '' . '
'; } // ── One curl POST, with full transfer diagnostics ────────────────── function bsr_probe(string $host, int $port, string $xml, int $timeout = 15): array { $out = [ 'ok'=>false,'http'=>0,'err'=>'','errno'=>0,'primary_ip'=>'','local_ip'=>'', 'connect_ms'=>0.0,'total_ms'=>0.0,'body'=>'','size'=>0,'verbose'=>'', ]; if (!function_exists('curl_init')) { $out['err'] = 'PHP curl extension is NOT loaded. Enable extension=curl in php.ini — BSRunner cannot reach Tally without it.'; $out['errno'] = -1; return $out; } $vh = fopen('php://temp', 'w+'); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => "http://{$host}:{$port}/", CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $xml, CURLOPT_HTTPHEADER => ['Content-Type: application/xml'], CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => $timeout, CURLOPT_VERBOSE => true, CURLOPT_STDERR => $vh, // Force IPv4 — the #1 reason a browser works but curl doesn't: // 'localhost' can resolve to ::1 (IPv6) while Tally listens only on // 127.0.0.1. Comment this line out to test the difference. CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, // Never route a loopback call through a proxy env var (http_proxy). CURLOPT_PROXY => '', ]); $body = curl_exec($ch); $out['errno'] = curl_errno($ch); $out['err'] = curl_error($ch); $out['http'] = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $out['primary_ip'] = (string)curl_getinfo($ch, CURLINFO_PRIMARY_IP); $out['local_ip'] = (string)curl_getinfo($ch, CURLINFO_LOCAL_IP); $out['connect_ms'] = round(curl_getinfo($ch, CURLINFO_CONNECT_TIME) * 1000, 1); $out['total_ms'] = round(curl_getinfo($ch, CURLINFO_TOTAL_TIME) * 1000, 1); curl_close($ch); rewind($vh); $out['verbose'] = stream_get_contents($vh); fclose($vh); if ($body !== false) { $out['ok'] = true; $out['body'] = $body; $out['size'] = strlen($body); } return $out; } // ── Pull company names out of a Tally XML reply (tolerant) ───────── function bsr_companies(string $xml): array { if (trim($xml) === '') return []; $prev = libxml_use_internal_errors(true); $x = simplexml_load_string($xml); libxml_use_internal_errors($prev); $names = []; if ($x !== false) { foreach ($x->xpath('//COMPANY') ?: [] as $c) { $n = trim((string)($c->NAME ?? $c['NAME'] ?? '')); if ($n !== '') $names[] = $n; } } if (!$names && preg_match_all('/(.*?)<\/NAME>/is', $xml, $m)) { foreach ($m[1] as $n) { $n = trim(html_entity_decode($n)); if ($n!=='') $names[] = $n; } } return array_values(array_unique($names)); } // ── Raw TCP connect, curl-independent (isolates port vs curl) ────── function bsr_tcp(string $host, int $port, float $to = 5): array { $t0 = microtime(true); $fp = @fsockopen($host, $port, $errno, $errstr, $to); $ms = round((microtime(true) - $t0) * 1000, 1); if ($fp) { fclose($fp); return ['ok'=>true,'ms'=>$ms,'err'=>'']; } return ['ok'=>false,'ms'=>$ms,'err'=>"[$errno] $errstr"]; } // ── Plain GET — exactly what a browser does (the "TallyServer is Running" banner) ── function bsr_get(string $host, int $port, int $to = 8): array { if (!function_exists('curl_init')) return ['ok'=>false,'http'=>0,'body'=>'','err'=>'curl missing']; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL=>"http://{$host}:{$port}/", CURLOPT_RETURNTRANSFER=>true, CURLOPT_CONNECTTIMEOUT=>8, CURLOPT_TIMEOUT=>$to, CURLOPT_IPRESOLVE=>CURL_IPRESOLVE_V4, CURLOPT_PROXY=>'', ]); $b = curl_exec($ch); $e = curl_error($ch); $h = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return ['ok'=>$b!==false,'http'=>$h,'body'=>$b!==false?$b:'','err'=>$e]; } function esc($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } // ── Run the probes ───────────────────────────────────────────────── $results = []; $tcp = $get = null; $hostcmp = []; if ($run) { foreach (['fetch','native'] as $mode) { $results[$mode] = bsr_probe($host, $port, bsr_company_xml($mode)); } $tcp = bsr_tcp($host, $port); // curl-independent TCP reachability $get = bsr_get($host, $port); // what the browser does foreach (['127.0.0.1','localhost','::1'] as $h) { // IPv4 vs name vs IPv6 $hostcmp[$h] = bsr_tcp($h, $port, 3); } } // Best result to summarise $best = null; foreach ($results as $r) { if ($r['ok'] && $r['http']===200) { $best = $r; break; } if (!$best) $best = $r; } $companies = ($best && $best['ok']) ? bsr_companies($best['body']) : []; ?> Tally Connection Test — Balance Sheet Runner

Tally Connection Test

This runs the exact XML request BSRunner sends when it lists companies — a curl POST to http://:/. Reaching that address in a browser only opens a GET and proves the port is listening; it does not prove BSRunner's POST gets through. Run it below.

Connection settings

0; $wentIPv6 = $best && strpos($best['primary_ip'], ':') !== false; ?>
✓
Tally is reachable and BSRunner can read it.Found open . If BSRunner still isn't picking up, the problem is in the app config (host/port/company name), not the connection.
!
Connected to Tally, but no company came back.The gateway answered (HTTP 200) but the company collection was empty. Open a company in Tally so it is loaded, then re-test. Raw reply is shown below.
!
Something answered on that port, but it wasn't Tally's gateway (HTTP ).Another program may be using port , or Tally's gateway isn't set to Server mode. Check the raw reply below.
✕
BSRunner cannot reach Tally on :. — curl connected over IPv6 (); Tally usually listens on IPv4 only. See the checklist below.

What happened, step by step

curl extensionloaded' : 'MISSING' ?>
Target URLhttp://:/
Connected to IP  IPv6
HTTP status'.esc($best['http']).'') : 'no response' ?>
Connect time ms
Total time ms
Reply size bytes
curl error

Companies open in Tally

POST vs GET vs raw TCP — where does it break?

Raw TCP connect
curl-independent socket
open' : 'blocked' ?>   ms
GET (what the browser does) HTTP '.esc($get['http']).'') : 'failed' ?>  
POST XML (what BSRunner does) HTTP 200' : ''.esc($best['http']?:'no response').'' ?>   bytes

If TCP is open and GET works but POST fails, the port and Tally are fine — the fault is curl/proxy on the POST. If TCP is blocked here but the browser reaches the port, Apache's service account is being firewalled or proxied differently than your user.

127.0.0.1 vs localhost vs ::1

$r): ?>
reachable' : 'no' ?>   ms

If 127.0.0.1 is reachable but localhost is not (or is slow), a localhost anywhere in your Tally path resolves to ::1 and misses Tally. Keep the import host as 127.0.0.1.

PHP environment (the Apache service, not your shell)

PHP version ·
curl version
Loaded php.ini
Runs as user
 set — may hijack loopback
Proxy env varsnone set

Raw exchange

Request sent (FETCH mode)
Reply from Tally
curl verbose log

If the browser works but BSRunner doesn't — check these

  • IPv6 vs IPv4. If your config uses localhost, PHP may resolve it to ::1 while Tally listens only on 127.0.0.1. Fix: set the host to 127.0.0.1 everywhere. (This test forces IPv4, so if it works here but the app fails, this is almost certainly it.)
  • A proxy env var. If http_proxy / HTTP_PROXY is set for the web-server process, curl routes even loopback calls through it. This page blanks the proxy; make sure the app does too, or exclude 127.0.0.1 via no_proxy.
  • Tally gateway not in Server mode. Gateway of Tally → F1 (Help) → Settings → Connectivity → Client/Server configuration → Server, port . A GET in the browser can succeed even when POST behaviour is off, so confirm this explicitly.
  • Company not loaded. The gateway only returns companies currently open in Tally. If none are open you connect fine but get zero companies.
  • Firewall on the PHP process. Windows Firewall can allow your browser but block php.exe / the web server from opening the loopback socket. Allow it, or test with server and Tally on the same machine.
  • curl extension off. If the row above says MISSING, enable extension=curl in php.ini and restart the web server — BSRunner can't reach Tally at all without it.
Balance Sheet Runner · Auditwave — standalone diagnostic, safe to delete when done.