Tally · Beta
Auto-sync Tally with FundReap — no export needed
A small program that reads straight from TallyPrime's own connection on your PC, every few minutes, and uploads only when something actually changed. No Alt+E export, no upload button, nothing to remember to do after entering a voucher.
1. In Tally: F1 (Help) → Settings → Connectivity → "TallyPrime acts as" → Server, port 9000, restart Tally when prompted.
2. Download the script, right-click it, Run with PowerShell.
3. It opens your browser to sign in — no token to copy. If Chrome asks to access your local network, click Allow. Type your exact Tally company name when it asks. Leave the window open.
What this actually is — and isn't
This talks to Tally's own XML/HTTP gateway (a feature built into TallyPrime itself) on
localhost:9000, asks it for your outstanding bills and recent receipts, and
uploads that to the same place a browser upload or the folder-watcher connector would —
same limits, same history, same result.
Prefer to keep exporting by hand, or Tally's gateway isn't something you want to turn on? The original folder-watcher connector still works exactly as before — this is an additional option, not a replacement.
Step 1 — Turn on Tally's Gateway Server
- Open TallyPrime with the company you want to sync loaded
- Press F1 (Help) → Settings → Connectivity → Client/Server configuration
- Set TallyPrime acts as to Server. Leave Enable ODBC as Yes and Port as 9000 unless you have a reason to change it.
- Accept, then restart TallyPrime when it asks — this only takes effect after a restart.
This has to be done once per machine Tally runs on. It doesn't open
anything to the internet — the gateway only ever listens on that machine itself
(localhost), which is exactly why this script has to run on the same PC as
Tally, not on a server somewhere else.
Verify it before you run it
This one is a bigger ask than the folder-watcher version — it talks to Tally's own gateway automatically, with no export step for you to see happening. That's exactly why it's worth reading before you run it, not just trusting the description. It's a plain, unsigned PowerShell script; Windows will likely show a security warning the first time (normal, see "Common problems" below), and the standard practice for any unsigned script is to actually read it — so here's everything that takes.
- Only ever talks to Tally on
localhost:9000— the same machine it runs on. It cannot reach a Tally instance anywhere else, and Tally's gateway itself never listens outside that machine. - Only asks Tally for two specific read-only reports (Bills Receivable, Receipt vouchers) — it cannot post, edit, or delete anything in Tally. There is no write path in this script at all.
- Sends data only to
fundreap.com/api/connector/upload— the exact same endpoint every other upload path uses. Nothing else, no analytics, no telemetry. - Never asks for administrator rights, and never needs them.
Read the entire script before downloading it (300 lines)
# FundReap live Tally connector.
#
# Unlike fundreap-connector.ps1 (which watches a folder for a file you export
# from Tally by hand), this talks to TallyPrime's own XML/HTTP gateway
# directly, on the same machine, and needs no manual export step at all.
#
# What it does, every poll:
# 1. Ask Tally (http://localhost:9000) for the "Bills Receivable" report
# and for Receipt vouchers, as XML.
# 2. If nothing changed since the last poll, do nothing.
# 3. If it changed, combine both responses and upload them to the exact
# same endpoint the folder-watcher connector uses.
#
# This is still not "real time" — Tally has no push/webhook mechanism, only
# request/response (see connect-tally-live.html for why). It is "a few
# minutes stale at worst", which is the same ceiling every real product that
# talks to on-prem Tally has settled on (Biz Analyst polls every 5 minutes by
# default; this defaults to 3).
#
# Requires: TallyPrime running locally with the Gateway Server enabled
# (F1 Help -> Settings -> Connectivity -> "TallyPrime acts as: Server",
# port 9000) and the company you want to sync already open.
#
# Run it: right-click this file -> "Run with PowerShell". First run opens your
# browser to sign in (or, if you're already signed in to fundreap.com, pairs
# instantly with no prompt at all) — no token to copy-paste. Then it asks for
# your Tally company name, the one thing FundReap has no way to know, and
# remembers both in a config file next to this script.
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$configPath = Join-Path $scriptDir 'fundreap-connector-live.config.json'
$appUrl = 'https://fundreap.com'
$apiUrl = "$appUrl/api/connector/upload"
$gatewayUrl = 'http://localhost:9000'
$pollSeconds = 180
$clearedMarker = "`n<!--FUNDREAP-CLEARED-BILLS-->`n"
function Write-Status($msg, $color = 'Gray') {
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] $msg" -ForegroundColor $color
}
# Writes a minimal raw HTTP response onto a NetworkStream. Hand-rolled rather
# than System.Net.HttpListener: HttpListener goes through Windows' HTTP.sys,
# which silently swallowed every request in testing here — connections
# completed at the TCP level but no response ever came back, with no
# exception raised anywhere to explain why (a known class of issue tied to
# HTTP.sys URL-ACL reservations, which normally need an explicit
# `netsh http add urlacl` a script like this can't assume it's allowed to
# run). A bare TcpListener on a loopback port needs no such registration.
function Send-PairResponse($stream, [int]$statusCode, [string]$statusText, [string]$bodyText) {
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($bodyText)
$header = @(
"HTTP/1.1 $statusCode $statusText"
'Content-Type: application/json'
"Content-Length: $($bodyBytes.Length)"
"Access-Control-Allow-Origin: $appUrl"
'Access-Control-Allow-Methods: POST, OPTIONS'
'Access-Control-Allow-Headers: content-type'
# Chrome's Private Network Access policy blocks a public HTTPS origin
# (fundreap.com) from fetching a loopback address at all unless the
# response explicitly opts in with this header — ordinary CORS headers
# alone aren't enough. Confirmed missing this the hard way: it worked
# in every localhost-to-localhost test (PNA doesn't apply there) and
# only failed once tested against the real https://fundreap.com origin,
# which is exactly the real-world case.
'Access-Control-Allow-Private-Network: true'
'Connection: close'
''
''
) -join "`r`n"
$headerBytes = [System.Text.Encoding]::ASCII.GetBytes($header)
$stream.Write($headerBytes, 0, $headerBytes.Length)
if ($bodyBytes.Length -gt 0) { $stream.Write($bodyBytes, 0, $bodyBytes.Length) }
$stream.Flush()
}
# Opens the browser to appUrl/connector/pair with a fresh one-time code, then
# blocks (with a timeout) on a tiny local listener until that page hands a
# connector token back to it. See public/connector/pair.html for the other
# side of this — it's what actually calls POST /api/connector/token once the
# user is signed in, then POSTs the result here. This machine-local listener
# is the only thing that can ever act on that POST, and only because it
# knows $pairCode, which never leaves this process except embedded in the
# URL it opens in the user's own browser.
function Get-TokenViaPairing {
$pairCode = [System.Guid]::NewGuid().ToString('N')
$listener = $null
$port = 17865
for ($i = 0; $i -lt 20; $i++) {
try {
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $port)
$listener.Start()
break
} catch {
$listener = $null
$port++
}
}
if (-not $listener) { throw 'Could not open a local port to receive the pairing token.' }
try {
$pairUrl = "$appUrl/connector/pair?code=$pairCode&port=$port"
Write-Status 'Opening your browser to sign in and pair this connector...' 'Cyan'
Start-Process $pairUrl
$deadline = (Get-Date).AddMinutes(5)
while ((Get-Date) -lt $deadline) {
if (-not $listener.Pending()) { Start-Sleep -Milliseconds 300; continue }
$client = $listener.AcceptTcpClient()
try {
# Browsers open and drop loopback connections for reasons that have
# nothing to do with our request — preconnect, speculative fetches,
# a CORS preflight the browser abandons after reading the response
# headers. Any of those can throw partway through a raw socket read.
# A single one of those must not take the whole listener down with
# it — confirmed the hard way against a real browser, not just
# curl, which never triggers this. Only a genuinely successful
# /pair POST is allowed to return out of this loop.
try {
$stream = $client.GetStream()
$buffer = New-Object byte[] 8192
$ms = New-Object System.IO.MemoryStream
$headerEnd = -1
while ($headerEnd -lt 0 -and $ms.Length -lt 65536) {
$read = $stream.Read($buffer, 0, $buffer.Length)
if ($read -le 0) { break }
$ms.Write($buffer, 0, $read)
$headerEnd = ([System.Text.Encoding]::ASCII.GetString($ms.ToArray())).IndexOf("`r`n`r`n")
}
if ($headerEnd -lt 0) { continue }
$allBytes = $ms.ToArray()
$headerText = [System.Text.Encoding]::ASCII.GetString($allBytes, 0, $headerEnd)
$lines = $headerText -split "`r`n"
$reqParts = $lines[0] -split ' '
$method = $reqParts[0]
$reqPath = $reqParts[1]
$contentLength = 0
foreach ($line in $lines) {
if ($line -match '^Content-Length:\s*(\d+)') { $contentLength = [int]$Matches[1] }
}
$bodyStart = $headerEnd + 4
while (($allBytes.Length - $bodyStart) -lt $contentLength) {
$read = $stream.Read($buffer, 0, $buffer.Length)
if ($read -le 0) { break }
$ms.Write($buffer, 0, $read)
$allBytes = $ms.ToArray()
}
if ($method -eq 'OPTIONS') { Send-PairResponse $stream 204 'No Content' ''; continue }
if ($method -ne 'POST' -or $reqPath -ne '/pair') { Send-PairResponse $stream 404 'Not Found' '{}'; continue }
$bodyBytes = $allBytes[$bodyStart..($bodyStart + $contentLength - 1)]
$payload = ([System.Text.Encoding]::UTF8.GetString($bodyBytes)) | ConvertFrom-Json
if ($payload.code -ne $pairCode) { Send-PairResponse $stream 403 'Forbidden' '{}'; continue }
Send-PairResponse $stream 200 'OK' '{"ok":true}'
return $payload.token
} catch {
continue
}
} finally {
$client.Close()
}
}
throw 'Timed out waiting for sign-in. Run the script again when you are ready.'
} finally {
$listener.Stop()
}
}
if (Test-Path $configPath) {
$config = Get-Content $configPath -Raw | ConvertFrom-Json
} else {
Write-Host ''
Write-Host 'FundReap Live Connector - first-time setup' -ForegroundColor Cyan
Write-Host ''
$token = Get-TokenViaPairing
Write-Status 'Paired.' 'Green'
$company = Read-Host 'Exact Tally company name (as shown at the top of Tally)'
if ([string]::IsNullOrWhiteSpace($company)) { Write-Host 'No company entered - exiting.' -ForegroundColor Red; exit 1 }
$config = [pscustomobject]@{ token = $token.Trim(); company = $company.Trim() }
$config | ConvertTo-Json | Set-Content $configPath
Write-Host "Saved. Editing $configPath directly changes these later." -ForegroundColor DarkGray
}
function Build-Request([string]$reportXml) {
return $reportXml -replace '\{\{COMPANY\}\}', [System.Security.SecurityElement]::Escape($config.company)
}
$outstandingReq = Build-Request @'
<ENVELOPE>
<HEADER>
<VERSION>1</VERSION>
<TALLYREQUEST>EXPORT</TALLYREQUEST>
<TYPE>DATA</TYPE>
<ID>Bills Receivable</ID>
</HEADER>
<BODY>
<DESC>
<STATICVARIABLES>
<SVCURRENTCOMPANY>{{COMPANY}}</SVCURRENTCOMPANY>
<SVEXPORTFORMAT>$SysName:XML</SVEXPORTFORMAT>
</STATICVARIABLES>
</DESC>
</BODY>
</ENVELOPE>
'@
$receiptsReq = Build-Request @'
<ENVELOPE>
<HEADER>
<VERSION>1</VERSION>
<TALLYREQUEST>EXPORT</TALLYREQUEST>
<TYPE>COLLECTION</TYPE>
<ID>ReceiptVouchers</ID>
</HEADER>
<BODY>
<DESC>
<STATICVARIABLES>
<SVCURRENTCOMPANY>{{COMPANY}}</SVCURRENTCOMPANY>
</STATICVARIABLES>
<TDL>
<TDLMESSAGE>
<COLLECTION NAME="ReceiptVouchers" ISMODIFY="No">
<TYPE>Voucher</TYPE>
<FILTER>FilterReceipts</FILTER>
<FETCH>DATE, VOUCHERNUMBER, PARTYLEDGERNAME, VOUCHERTYPENAME</FETCH>
<FETCH>LEDGERNAME, AMOUNT, BILLALLOCATIONS.NAME, BILLALLOCATIONS.AMOUNT</FETCH>
</COLLECTION>
<SYSTEM TYPE="Formulae" NAME="FilterReceipts">$VoucherTypeName = "Receipt"</SYSTEM>
</TDLMESSAGE>
</TDL>
</DESC>
</BODY>
</ENVELOPE>
'@
function Get-TallyXml([string]$requestXml) {
try {
return Invoke-RestMethod -Uri $gatewayUrl -Method Post -Body $requestXml -ContentType 'text/xml' -TimeoutSec 30
} catch {
throw "Could not reach Tally at $gatewayUrl - $($_.Exception.Message). Is TallyPrime open, with the gateway server enabled (F1 > Settings > Connectivity) and '$($config.company)' the loaded company?"
}
}
Write-Host ''
Write-Status "Polling Tally every $pollSeconds seconds for '$($config.company)'." 'Green'
Write-Status 'Leave this window open. Ctrl+C to stop.' 'DarkGray'
Write-Host ''
$lastHash = $null
while ($true) {
try {
$outstandingXml = Get-TallyXml $outstandingReq
$receiptsXml = Get-TallyXml $receiptsReq
# Both Invoke-RestMethod calls above return the response auto-parsed as
# XML objects when content-type allows it; re-serialize to raw text so
# the combined payload matches exactly what server-side parse-tally-xml.js
# expects (it parses text, not a .NET XML DOM).
$outstandingText = if ($outstandingXml -is [System.Xml.XmlDocument]) { $outstandingXml.OuterXml } else { [string]$outstandingXml }
$receiptsText = if ($receiptsXml -is [System.Xml.XmlDocument]) { $receiptsXml.OuterXml } else { [string]$receiptsXml }
$combined = $outstandingText + $clearedMarker + $receiptsText
$hash = [System.BitConverter]::ToString([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($combined)))
if ($hash -eq $lastHash) {
Write-Status 'No change since last poll.' 'DarkGray'
} else {
Write-Status 'Change detected - uploading ...'
$bytes = [System.Text.Encoding]::UTF8.GetBytes($combined)
$base64 = [System.Convert]::ToBase64String($bytes)
$body = @{ fileBase64 = $base64; filename = "tally-live-$(Get-Date -Format 'yyyyMMdd-HHmmss').xml" } | ConvertTo-Json
$r = Invoke-RestMethod -Uri $apiUrl -Method Post -Body $body `
-ContentType 'application/json' `
-Headers @{ Authorization = "Bearer $($config.token)" }
if ($r.error) {
Write-Status " Rejected: $($r.error)" 'Yellow'
} else {
$lastHash = $hash
Write-Status ' Synced.' 'Green'
}
}
} catch {
Write-Status " $($_.Exception.Message)" 'Red'
Write-Status ' Will retry next poll.' 'DarkGray'
}
Start-Sleep -Seconds $pollSeconds
}
Verify the file you downloaded matches this source exactly. Its SHA-256 checksum is published at fundreap-connector-live.ps1.sha256 — computed live from the file actually being served. After downloading, check it yourself in PowerShell:
Get-FileHash .\fundreap-connector-live.ps1 -Algorithm SHA256
Compare the Hash value it prints to what's at the link above — a
match means the file on your computer is byte-for-byte what's shown here.
Step 2 — Download and run the script
- Download it from the Integrations page (Integrations → Auto-sync — no export needed → Set up), or directly: fundreap-connector-live.ps1
- Right-click the downloaded file → Run with PowerShell
- It opens a browser tab and asks you to sign in with your FundReap account — the same email-link sign-in as the main app. Already signed in to fundreap.com in that browser? It pairs instantly with no form at all.
- On Chrome, a permission prompt appears — "fundreap.com wants to access devices on your local network". Click Allow. This is Chrome itself asking, the same way it would for a camera or microphone — it's how the page hands the token to the script running on your PC, and it only asks once per browser.
- Once paired, the script itself asks for your exact Tally company name — copy it
character-for-character from the top of the Tally window. This is the one thing FundReap
has no way to know on its own. Both the pairing and the company name get saved next to the
script, in
fundreap-connector-live.config.json.
Nothing to copy-paste, no token to lose — if you ever need to re-pair
(new PC, lost the config file), just delete
fundreap-connector-live.config.json and run the script again.
Step 3 — Leave it running
That's it. Every 3 minutes it asks Tally for your current outstanding bills and recent receipts; if nothing changed it does nothing; if something changed it uploads automatically. Enter a sale or record a payment in Tally, and within a few minutes your plan on fundreap.com reflects it — no export, no login, no click.
Common problems
"Could not reach the connector script on this PC" during pairing
Almost always means the Chrome permission prompt ("fundreap.com wants to access devices on your local network") got dismissed or Blocked instead of Allowed — reload the pairing page and choose Allow this time. Otherwise, make sure the PowerShell window from Step 2 is still open; closing it before pairing finishes stops the listener it's waiting on.
"Could not reach Tally at http://localhost:9000"
TallyPrime either isn't running, doesn't have the right company loaded, or the Gateway Server setting from Step 1 didn't take (remember: it needs a restart to apply). Open Tally, load the company, check F1 → Settings → Connectivity shows "Server", and try again.
"Running scripts is disabled on this system" / SmartScreen warning
Same fix as the folder-watcher connector — see the common problems section of that guide. Both scripts are plain, readable PowerShell with nothing compiled or hidden.
It says "No change since last poll" every time, even after I added a voucher
Double-check the company name in fundreap-connector-live.config.json matches
the company currently open in Tally exactly — a mismatch means it's quietly asking Tally
about the wrong (or no) company, which reads back as "nothing outstanding" rather than an
error.
Turning it off
Close the PowerShell window — nothing runs when it isn't open, there's no background service or scheduled task installed. To fully disconnect (invalidate the token so this script can no longer upload anything even if it's run again), go to Integrations → Connect Tally → Reconnect — the token is shared with the folder-watcher connector, so rotating it there revokes both.
Sign in, then Integrations → Auto-sync to download the script.
Go to IntegrationsAlso see: The folder-watcher connector (simpler, no Tally configuration needed) · How to reduce DSO