03 · Revenue
Record payments from any provider
Selling through something Tally doesn't connect to? Your server posts each payment, and it's linked to its visit like any provider's.
01Make a key that can record payments
On the site's Setup page, under API keys, name a key and tick Record payments. The key is shown once; the list marks it Records payments. A key without it can only read, and is refused here with 403.
02Post each payment
When your server takes a payment, from whatever provider, post it to POST https://tallybyoutpost.com/api/v1/payments with the key as Authorization: Bearer … and a JSON body. Send the buyer's visitor id with it, read from the _tally_vid cookie on their request:
// Express with cookie-parser, where your server records a payment it took.
app.post('/paid', async (req, res) => {
await fetch('https://tallybyoutpost.com/api/v1/payments', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.TALLY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 29.99,
currency: 'USD',
transaction_id: 'order_1042',
tally_visitor_id: req.cookies['_tally_vid'],
email: 'buyer@example.com',
}),
});
res.sendStatus(204);
});
curl https://tallybyoutpost.com/api/v1/payments \
-H "Authorization: Bearer $TALLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 29.99, "currency": "USD", "transaction_id": "order_1042", "tally_visitor_id": "the _tally_vid cookie"}'
# Where your server records a payment it took (requests, Flask).
import os
import requests
requests.post(
"https://tallybyoutpost.com/api/v1/payments",
headers={"Authorization": "Bearer " + os.environ["TALLY_API_KEY"]},
json={
"amount": 29.99,
"currency": "USD",
"transaction_id": "order_1042",
"tally_visitor_id": request.cookies.get("_tally_vid"),
"email": "buyer@example.com",
},
timeout=10,
)
03The fields
amountnumberrequired- What was paid, in the currency's main unit (
29.99), tax included. A decimal string works too. More than 0: anamountof0is a free trial, which records nothing. currencystringrequired- Its three-letter ISO 4217 code:
USD,EUR,JPY. transaction_idstringrequired- The payment's id in your system, at most 200 characters (a number works too). It's unique within the site: posting it again is a duplicate, not a second payment.
tally_visitor_idstring- The
_tally_vidcookie's value, which ties the payment to its visit.datafast_visitor_idandvisitor_idare read the same way. emailstring- The buyer's email, for an email match. Hashed when it arrives; only the hash is kept.
customer_emailworks too. namestring- Accepted, so DataFast's body works as it is, and never stored.
customer_nametoo. customer_idstring- Your id for the buyer. A renewal without a
subscription_idfollows the customer's first payment. subscription_idstring- Groups a subscription's payments, so each renewal follows its first payment.
renewalboolean- This payment renews a subscription (
subscription_id, elsecustomer_id, says which). It's credited as the first payment was. refundedboolean- This is a refund of
amountagainst the paymenttransaction_idnames. Partial refunds are fine. refund_idstring- Your id for the refund, when one payment has several partial refunds. Without it, a second refund of the same payment is a duplicate.
is_free_trialboolean- A trial with nothing paid: nothing is recorded, and the answer is
200. taxnumberdefault 0- The part of
amountthat is tax, so revenue is counted without it. timestampstringdefault now- When it was paid: ISO 8601 with its zone, like
2026-11-10T12:00:00Z. At most a few minutes in the future.
Anything else in the body is ignored.
04What it answers
200 with JSON, whenever the payment is recorded or was already:
{"message": "Payment recorded. It's linked to its visit within a few seconds.", "transaction_id": "order_1042", "duplicate": false}| When | message | duplicate |
|---|---|---|
| A payment | Payment recorded. It's linked to its visit within a few seconds. | false |
| A refund | Refund recorded. | false |
The same transaction_id again | This transaction_id was recorded before; nothing new was recorded. | true |
| The same refund again | This refund was recorded before; nothing new was recorded. | true |
| A free trial | Free trials aren't revenue in Tally, so nothing was recorded. | false |
Anything else is an error, as everywhere in the API: {"error": {"code", "message"}}, where the message is a sentence you can show as it is.
| Status | code | When |
|---|---|---|
| 400 | invalid_parameter | The body isn't a payment: the message says which field and why. |
| 401 | unauthorized | No key, not a key, or a revoked one. |
| 402 | payment_required | The site's account has no plan: its trial ended, or its plan did. |
| 403 | forbidden | The key can only read: make one with Record payments ticked. |
| 404 | not_found | A refund names a transaction_id this site never recorded. |
| 429 | rate_limited | The key's rate limit, with Retry-After in seconds. |
05Renewals and refunds
A renewal names its subscription (or its customer) and sets renewal; it's credited as the subscription's first payment was:
curl https://tallybyoutpost.com/api/v1/payments \
-H "Authorization: Bearer $TALLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 29.99, "currency": "USD", "transaction_id": "inv_2207", "subscription_id": "sub_88", "renewal": true}'
A refund sets refunded, names the payment it reverses by its transaction_id, and gives the amount refunded. Give each partial refund of one payment its own refund_id:
curl https://tallybyoutpost.com/api/v1/payments \
-H "Authorization: Bearer $TALLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 10, "currency": "USD", "transaction_id": "order_1042", "refunded": true, "refund_id": "rf_1"}'
06How each payment is linked
By the same rules as every provider, after the answer: a visitor id links with high confidence (as checkout metadata), an email with medium (when the buyer's browser called identify with it), and renewals and refunds follow the payment they belong to. How linking works.
07Check it worked
Something missing, or not what you see? Write to support@tallybyoutpost.com and say which page.