03 · Revenue
Connect Paddle and link its sales to visits
Tally reads Paddle Billing's transactions, refunds and chargebacks with a key that can only read.
01Connect Paddle
Create an API key in Paddle
Tally only reads your transactions; it never needs to move money. An API key can do only what you allow, and you can revoke it at any time. In Paddle, open Developer tools, Authentication, choose New API key, and give it these permissions:
Permission Access Why Transaction Read Every payment: its amount, tax, origin and custom data. Adjustment Read Refunds and chargebacks. Customer Read The customer's email to match tally('identify'): hashed at once, never stored. Notification setting Write Tally adds its own notification destination, so payments arrive within seconds. Leave every other permission off. To try it first, use a sandbox key from sandbox-vendors.paddle.com.
Paste the API key
On your site's Integrations page (or the Payments step when you add a site), open Paddle and fill in:
- API key: It starts with pdl_live_apikey_ (or pdl_sdbx_apikey_ in the sandbox). Tally stores it encrypted and never shows it again.
Then press Connect Paddle.
Tally checks it and imports the past year
It names any permission the key is missing, adds its own notification destination so new payments arrive as they happen, and imports the past 365 days, refunds and chargebacks included.
02Pass the visitor's id in customData
Where you open the checkout with Paddle.js, read the _tally_vid cookie and pass it as tally_visitor_id in customData. Paddle keeps it on the transaction and copies it to the subscription, so renewals follow. A transaction your server creates takes it in custom_data.
// Where you open the checkout (Paddle.js, after Paddle.Initialize).
const visitorId = (document.cookie.match(/(?:^|; )_tally_vid=([\w-]+)/) || [])[1];
Paddle.Checkout.open({
items: [{ priceId: 'pri_123', quantity: 1 }],
// customData needs at least one key, so it is left out without a visitor id.
...(visitorId ? { customData: { 'tally_visitor_id': visitorId } } : {}),
});
// Express with cookie-parser: a transaction your server creates for the checkout.
app.post('/checkout', async (req, res) => {
const visitorId = req.cookies['_tally_vid'];
const response = await fetch('https://api.paddle.com/transactions', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PADDLE_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [{ price_id: 'pri_123', quantity: 1 }],
custom_data: visitorId ? { 'tally_visitor_id': visitorId } : null,
}),
});
const { data } = await response.json();
// On the page: Paddle.Checkout.open({ transactionId: data.id }).
res.json({ transactionId: data.id });
});
04Check it worked
05What isn't read
Something missing, or not what you see? Write to support@tallybyoutpost.com and say which page.