Revenue attribution
Connect Telegram payments
Forward verified payment updates from your Telegram bot server to Revfaro. Track sales, renewals and Stars refunds without sharing your bot token.
In short
Keep Telegram's webhook on your bot server. Save a separate forwarding secret in Revfaro, then send verified payment updates to your website's endpoint. Link visitors using your server's order records. Stars remain XTR, with no currency conversion.
On this page
You need an existing Telegram bot that handles payments and a Revfaro website with revenue attribution enabled. A website manager can open Website settings › Setup › Connect payments › Telegram.
Keep your bot's existing webhook
Telegram continues sending updates to your bot server. Verify each incoming request before queuing anything for Revfaro. Telegram's setWebhook secret token arrives in X-Telegram-Bot-Api-Secret-Token; check it on your server using a constant-time comparison. Reject missing or incorrect authentication.
Do not point Telegram's setWebhook at Revfaro. Your bot still answers pre-checkout queries within Telegram's 10-second deadline and handles fulfillment. Only a successful-payment update confirms a completed sale. See Telegram's payment flow.
Save a separate forwarding secret
In the Telegram tab, choose Generate a secret, copy it into your bot server's secret storage, and click Save. You can also supply 32–256 letters, numbers, hyphens or underscores.
This secret is for the connection from your server to Revfaro. It is separate from both your bot token and the secret on Telegram's incoming webhook. Never put it in browser code, query strings or logs.
The tab shows Configured after saving. That confirms the local configuration, not delivery of a real payment.
Forward verified updates
Send a JSON POST to the address shown in the Telegram tab:
https://revfaro.com/api/webhooks/telegram/YOUR_SITE_IDSet Content-Type: application/json and X-Revfaro-Telegram-Secret to your saved forwarding secret. Revfaro reads these native update fields:
| Field | Use |
|---|---|
update_id |
Original nonnegative Telegram update ID |
message.date |
Original message time, in Unix seconds |
message.successful_payment |
A sale or recurring charge |
message.refunded_payment |
A Stars refund |
message.from.id |
Optional Telegram customer ID, scoped to Telegram |
revfaro.visitor_id and revfaro.session_id |
Optional attribution from your server's order record; provide both together |
Keep the payment's original currency, integer total_amount and telegram_payment_charge_id. Preserve is_recurring and is_first_recurring for subscriptions. Do not replace the charge ID when retrying. See Telegram's SuccessfulPayment and RefundedPayment definitions.
Revfaro accepts amounts from 1 to 1,000,000,000,000 minor units. Charge IDs must be 1–255 printable ASCII characters without spaces. Message dates must be between January 1, 2000 and 24 hours ahead of the server clock. Forward the original values rather than rounding or changing them.
Only send payment-related updates. Revfaro ignores other valid update types. It does not store invoice payloads, message text, buyer names, email addresses, phone numbers or delivery addresses from these updates. Avoid forwarding those fields when your server can omit them.
Node.js forwarding job
Run this helper from a durable job on your own server, after authenticating Telegram's request. Configure REVFARO_TELEGRAM_URL with the exact HTTPS endpoint above and REVFARO_TELEGRAM_SECRET with your saved secret. These are merchant-server settings, not Revfaro deployment variables.
update must come from your verified Telegram handler. attribution must come from an order record your server created and associated with that purchase; it must not come from untrusted URL parameters or a client-supplied revfaro object. The helper forwards only the fields Revfaro uses.
export async function forwardTelegramPayment(update, attribution, attempt = 1) {
let endpoint;
try {
endpoint = new URL(process.env.REVFARO_TELEGRAM_URL);
} catch {
throw new Error('Invalid Revfaro forwarding configuration');
}
const secret = process.env.REVFARO_TELEGRAM_SECRET;
if (
endpoint.protocol !== 'https:' ||
endpoint.username ||
endpoint.password ||
endpoint.search ||
endpoint.hash ||
!/^[A-Za-z0-9_-]{32,256}$/.test(secret ?? '')
) {
throw new Error('Invalid Revfaro forwarding configuration');
}
if (!Number.isInteger(attempt) || attempt < 1 || attempt > 5) {
throw new Error('Invalid forwarding attempt');
}
const message = update.message;
const kind = message?.successful_payment ? 'successful_payment' : 'refunded_payment';
const payment = message?.[kind];
if (!payment) return { state: 'ignored' };
const payload = {
update_id: update.update_id,
message: {
date: message.date,
...(message.from?.id ? { from: { id: message.from.id } } : {}),
[kind]: {
currency: payment.currency,
total_amount: payment.total_amount,
telegram_payment_charge_id: payment.telegram_payment_charge_id,
...(kind === 'successful_payment'
? {
is_recurring: !!payment.is_recurring,
is_first_recurring: !!payment.is_first_recurring,
}
: {}),
},
},
...(attribution?.visitorId && attribution?.sessionId
? {
revfaro: {
visitor_id: attribution.visitorId,
session_id: attribution.sessionId,
},
}
: {}),
};
const retry = (delayMs) => (attempt < 5 && delayMs <= 86_400_000 ? { state: 'retry', delayMs } : { state: 'stopped' });
const backoffMs = 10_000 * 2 ** (attempt - 1);
let response;
try {
response = await fetch(endpoint, {
method: 'POST',
redirect: 'error',
signal: AbortSignal.timeout(10_000),
headers: {
'Content-Type': 'application/json',
'X-Revfaro-Telegram-Secret': secret,
},
body: JSON.stringify(payload),
});
} catch {
return retry(backoffMs);
}
await response.body?.cancel().catch(() => {});
if (response.ok) return { state: 'handled' };
if (response.status !== 429 && response.status < 500) {
return { state: 'stopped', status: response.status };
}
const header = response.headers.get('Retry-After') ?? '';
const delayMs = /^\d+$/.test(header) ? Number(header) * 1000 : Math.max(0, Date.parse(header) - Date.now());
if (delayMs === Infinity) return { state: 'stopped' };
return retry(Math.max(backoffMs, Number.isFinite(delayMs) ? delayMs : 0));
}Your queue must persist the verified, minimized payload and attribution, schedule state: 'retry' after delayMs, and increment attempt. Retain stopped jobs for operator review; do not silently discard them or retry them forever. An unusually long Retry-After stops this example for manual scheduling instead of retrying earlier than requested. A handled response can also mean an ignored update; it is not a guarantee that revenue was recorded. Keep request bodies and secrets out of logs.
Queue the job before acknowledging the incoming Telegram update. Do not delay your bot's pre-checkout response while waiting for Revfaro. A network timeout can happen after Revfaro records a payment: retry the same charge rather than creating another ID.
Attribution and recurring payments
Store the visitor and session with your order when your website hands a buyer over to the bot. Resolve that association on your server after the payment is verified, then add both IDs in the top-level revfaro object. Revfaro checks that the session belongs to the visitor on this website. It does not read invoice_payload for attribution automatically.
If Telegram provides the same customer ID on later payments, Revfaro can reuse that customer's earlier attribution on this website. Without a valid association, revenue is still recorded but can remain unattributed. Preserve recurring-payment flags so renewals are classified correctly; each charge has its own charge ID.
Stars, refunds and duplicate delivery
- Stars use
XTRwith zero decimal places:total_amount: 250means 250 Stars. Fiat currencies use their usual minor units. - Set your website currency to Telegram Stars (XTR) to include Stars in its revenue totals. Other currencies are retained but excluded from totals in a different website currency. Revfaro does not convert Stars to USD or other currencies.
message.refunded_paymentcurrently supports Stars refunds. Revfaro does not initiate a refund or call Telegram.- Repeating the same Telegram charge for the same website does not add another payment. Refunds use that charge ID and a cumulative refunded amount, so a duplicate or an older, smaller total does not increase or reverse the refund.
- A refund received before its payment is held for reconciliation when the payment arrives.
- Use one integration per payment. Telegram deduplication does not merge a second copy sent through Stripe, another provider or the Payments API, and does not merge copies sent to different Revfaro websites.
Responses and disconnecting
| Response | Action |
|---|---|
200 |
Update handled or ignored; do not retry automatically |
400 |
Check the payload or missing site configuration before replaying |
401 |
Correct the forwarding secret before replaying |
404 |
Check the website ID and whether the website still exists |
413 |
Reduce the payload; the request limit is 1 MiB |
429, 5xx or a network failure |
Retry with the same charge ID, bounded attempts and any Retry-After delay |
Unsupported updates and updates for a plan without revenue attribution return 200 with an ignored reason. Check the website's plan and forwarding configuration before replaying an intentionally ignored payment.
A refund that arrives before its payment returns 200 with pending: "payment not recorded yet". It is retained for reconciliation; do not keep retrying the refund while waiting for the original payment.
Choose Disconnect Telegram in website settings to remove the forwarding secret. Existing analytics remain. Stop or update your server's forwarding job as well; otherwise it will keep trying a disabled connection. To rotate the secret, update both Revfaro and your server, then replay any failed deliveries with their original charge IDs.
Frequently asked questions
Does Revfaro need my Telegram bot token?
No. Revfaro accepts payment updates forwarded by your server with a separate secret. Your bot keeps its webhook, checkout handling and fulfillment.
Does Configured mean a payment arrived?
No. It means your forwarding secret is saved. A payment appears only after your server forwards a valid successful payment update.
Can the same payment also be sent through Stripe or the Payments API?
Choose one integration for each payment. Duplicate delivery is handled within the Telegram integration, but sending the same sale through another provider can count it twice.