Payments
Request a payment from a member, or send one to them with AnythingPay.
| Payment type | Our fee | How it is charged |
|---|---|---|
| Application payment | 0.75% | Added to the payer's total at checkout. |
| Donation | 0% | We take nothing from donations. |
| Card top-up or card donation | Processor cost | Added to the card charge, so the balance or donation arrives in full. The rate follows the fees Stripe actually charged. |
| Crypto cash-out | Network fee | Deducted from the payout and shown before you confirm. |
AnythingPay works in both directions. An application can send a member to a checkout page to collect a payment, or move USD from the application owner's balance to a member. Both flows settle in one transaction and leave a receipt.
Applications are created in the Applications tool. Payment requests are available to confidential applications only, because the status check uses the client secret.
Requirements
For a checkout:
- The application and its owner are active, and the application has a client secret.
- The
redirect_uriexactly matches one registered for the application. - The member is signed in, has two-factor authentication enabled, and is not restricted from sending payments.
- The member has enough balance, and the payment is inside the per-payment and daily limits.
A payout needs only an active, signed-in recipient to accept or decline it.
Guest payments
A checkout link can also be paid by card by someone who is not signed in and does not have a anything.solutions account. Guest checkouts are card-only: the visitor pays through Stripe, the application owner receives the full requested amount, and the platform fee and card processing cost are added to the card charge. Balance payments still require signing in with two-factor authentication.
Guest access is only available when card payments are configured. A request that names a specific payer still requires that member to sign in; a guest cannot pay on their behalf.
Create a checkout session
The application's server creates the request, authenticates with its client secret, and receives a checkout link. This proves the request came from the application, so another site cannot build a checkout under its name.
import json
import requests
response = requests.post(
"https://anything.solutions/pay/sessions",
auth=("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"),
data={
"redirect_uri": "https://example.com/paid",
"amount": "4.99",
"currency": "USD",
"description": "Premium role",
"items": json.dumps([{"name": "Premium role", "quantity": 1}]),
"state": "RANDOM_STATE",
"reference": "order-1042",
"payer": "482",
},
)
session = response.json()
const response = await fetch(
"https://anything.solutions/pay/sessions",
{
method: "POST",
headers: {
Authorization:
"Basic " + btoa("YOUR_CLIENT_ID:YOUR_CLIENT_SECRET"),
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
redirect_uri: "https://example.com/paid",
amount: "4.99",
currency: "USD",
description: "Premium role",
items: JSON.stringify([{ name: "Premium role", quantity: 1 }]),
state: "RANDOM_STATE",
reference: "order-1042",
payer: "482",
}),
},
);
const session = await response.json();
The response contains a short-lived checkout link and the reference:
{
"ok": true,
"reference": "order-1042",
"checkout_url": "https://anything.solutions/pay/checkout?request=...",
"expires_in": 900
}
Redirect the member's browser to checkout_url. The link expires with the configured checkout lifetime and can be opened by the intended member only when payer is set. The amount is fixed when the session is created.
Direct checkout link
For a simple payment link you can also build the checkout URL directly. The application name and registered return address still make the request identifiable, but a server-created session is the stronger option. Use an unguessable reference (or omit it and let the server generate one) when the link is shared; a reference is settled only once.
Redirect the member to the checkout endpoint with these query parameters:
| Parameter | Value |
|---|---|
client_id | The application's client ID. |
redirect_uri | One of the application's registered redirect URIs. |
amount | A positive USD amount with up to two decimal places. |
currency | USD (default). |
description | A short line describing the purchase. Optional. |
items | A URL-encoded JSON array of items. Optional. |
state | An unguessable value returned unchanged on the callback. Optional. |
reference | Your own reference, 1 to 64 letters, digits, dots, colons, underscores or dashes. Optional; a random one is generated when omitted. |
payer | The member's sub from AnythingAuth. Optional; when set, the checkout refuses to charge a different signed-in account. |
https://anything.solutions/pay/checkout?client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fexample.com%2Fpaid&amount=4.99¤cy=USD&description=Premium%20role&state=RANDOM_STATE&reference=order-1042
Items are names with an optional quantity. Keep the total amount in amount; item amounts are not used.
[
{"name": "Premium role", "quantity": 1},
{"name": "Server slot", "quantity": 2}
]
The checkout page shows the application, the amount in USD, the items, the member's balance, and whether the payment fits the daily allowance. The member enters an authenticator code or a backup code and chooses Pay or Cancel.
Callback
After the member decides, the browser returns to your registered redirect_uri with:
| Parameter | Value |
|---|---|
payment | The payment reference. |
status | paid or denied. |
state | The value you sent, unchanged, when one was provided. |
The browser callback is a hint, not proof. Confirm the result on your server before delivering anything, and compare the reference, status and amount from the status endpoint with the order you created.
Confirm a payment
Call the status endpoint from your server with the client ID and secret over HTTP Basic. Send the reference as a form field:
import requests
response = requests.post(
"https://anything.solutions/pay/status",
auth=("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"),
data={"reference": "order-1042"},
)
payment = response.json()
const response = await fetch(
"https://anything.solutions/pay/status",
{
method: "POST",
headers: {
Authorization:
"Basic " + btoa("YOUR_CLIENT_ID:YOUR_CLIENT_SECRET"),
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ reference: "order-1042" }),
},
);
const payment = await response.json();
A settled payment returns:
{
"ok": true,
"payment": {
"reference": "order-1042",
"kind": "charge",
"status": "paid",
"amount_cents": 499,
"fee_cents": 4,
"total_cents": 503,
"amount_usd": 4.99,
"requested_amount": 4.99,
"requested_currency": "USD",
"description": "Premium role",
"items": [{"name": "Premium role", "quantity": 1}],
"created_at": "2026-10-27T18:40:12Z",
"paid_at": "2026-10-27T18:40:19Z",
"payer": {"id": 482, "username": "example"},
"payee": {"id": 91, "username": "app-owner"},
"app": {"client_id": "YOUR_CLIENT_ID", "name": "Example App"}
}
}
Unknown references return 404. A reference that belongs to another application is treated as unknown.
Send a payment to a member
An application can pay a member from the balance of the account that owns it. This is the reverse of a checkout: the application authenticates the request with its client secret, and the member accepts or declines the pending payment instead of approving a checkout.
import requests
response = requests.post(
"https://anything.solutions/pay/send",
auth=("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"),
data={
"recipient": "482",
"amount": "10.00",
"currency": "USD",
"description": "Balance payout",
"reference": "payout-1042",
},
)
payment = response.json()
const response = await fetch(
"https://anything.solutions/pay/send",
{
method: "POST",
headers: {
Authorization:
"Basic " + btoa("YOUR_CLIENT_ID:YOUR_CLIENT_SECRET"),
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
recipient: "482",
amount: "10.00",
currency: "USD",
description: "Balance payout",
reference: "payout-1042",
}),
},
);
const payment = await response.json();
| Parameter | Value |
|---|---|
recipient | The member's sub from AnythingAuth. Required. |
amount | A positive USD amount with up to two decimal places. |
currency | USD (default). |
description | A short line describing the payment. Optional. |
items | A URL-encoded JSON array of items. Optional. |
reference | Your own reference for idempotency. A random one is generated when omitted. |
The response is the same payment object returned by the status endpoint, with kind set to payout. A newly created payout is pending and includes a claim_url for the recipient. Reusing a reference with the same payout details returns the original payment without creating another.
The recipient receives a notification and opens a payment page showing the application, amount, description, items, sender, reference and date. They can accept or decline there. No money moves until they accept, so poll the status endpoint before delivering anything. A payout ends as paid or denied.
The application owner's balance must cover the amount when the recipient accepts, and the daily outgoing limit applies at that point. The recipient sees the payment in their Payment history.
Monthly subscriptions
A confidential application can charge a member's balance automatically each month. The member first approves the application through AnythingAuth with the Payments and subscriptions scope, then the application creates a subscription with the member's current access token. The token must belong to the same application that authenticates with its client secret; a bare user id is never accepted.
The application calls these endpoints over HTTP Basic with its client ID and secret. Every call also includes the member's user_token:
| Endpoint | Purpose |
|---|---|
POST /pay/api/subscriptions | Create a subscription, or return the identical one for the same service and reference. |
POST /pay/api/subscriptions/status | Read the current status, next charge date and failure count. |
POST /pay/api/subscriptions/update | Change amount_cents, metadata or next_charge_at. |
POST /pay/api/subscriptions/cancel | Stop future charges. Repeating the call is harmless. |
import requests
response = requests.post(
"https://anything.solutions/pay/api/subscriptions",
auth=("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"),
json={
"user_token": "MEMBER_ACCESS_TOKEN",
"service": "pro-monthly",
"reference": "customer-482",
"amount_cents": 500,
"interval_months": 1,
"next_charge_at": "2026-04-01T00:00:00Z",
"metadata": {"plan": "pro"},
},
)
subscription = response.json()["subscription"]
| Field | Value |
|---|---|
service | Your product slug, up to 64 characters. Combined with reference, it uniquely identifies the subscription for your app. |
reference | Your own reference for this member's subscription, up to 64 characters. Reusing it with the same amount is idempotent. |
amount_cents | The recurring price in Points. The platform fee is added to the member's total; your app receives the full amount. |
interval_months | 1. Monthly subscriptions are supported today. |
next_charge_at | Optional ISO-8601 first charge time. Omit it to charge on the next scheduler pass. |
metadata | Optional JSON object returned with the subscription and its webhook events. |
A subscription is active, past_due or cancelled. A charge debits the member's balance in one transaction and writes the same receipt and Points ledger entries as a checkout. If the balance is too low, nothing is debited: the subscription becomes past_due, and the scheduler retries once a day until the member tops up or the application cancels. A successful charge advances the next date by one month from the period that was charged, so a late run does not move the billing anniversary. If the member revokes the application in Settings, the next scheduler pass cancels its subscriptions before charging.
The scheduler is flask --app wsgi charge-subscriptions. It is safe to run repeatedly and can be wired to cron or a systemd timer. The process-jobs --loop worker also calls it on the interval in ANYTHINGPAY_SUBSCRIPTION_INTERVAL_SECONDS.
Payment links
Members can create shareable payment links for donations without registering an application. Open the Payment links tab, add a title and optional description, and choose up to six suggested amounts in USD. Each link gets a short public address and a PNG QR code that can be downloaded from its management page.
Donors open the link, choose a suggested amount or enter their own, and approve the checkout with two-factor authentication. Funds move immediately to the link owner and the donation appears in both members' Payment history. Links can be paused or deleted at any time; past donations keep their receipts.
Donors can also pay by card through Stripe. The card charge settles to the link owner's balance and produces the same donation receipt; the donor does not need an account or a balance.
The Keys tab lists the client IDs and secret status of apps that request payments server-to-server. Payment links do not need an app or a key.
Payment-link API
For automation, a confidential app can manage its owner's payment links with its client credentials over HTTP Basic. Get the client ID and secret from the Keys tab or the Applications tool. The API returns the public link, QR endpoint, suggested amounts and donation totals.
import requests
response = requests.post(
"https://anything.solutions/pay/api/donation-links",
auth=("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"),
data={
"title": "Support my project",
"description": "Help keep the servers running",
"suggested_amounts": "3,5,10",
},
)
link = response.json()
const response = await fetch(
"https://anything.solutions/pay/api/donation-links",
{
method: "POST",
headers: {
Authorization:
"Basic " + btoa("YOUR_CLIENT_ID:YOUR_CLIENT_SECRET"),
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
title: "Support my project",
description: "Help keep the servers running",
suggested_amounts: "3,5,10",
}),
},
);
const link = await response.json();
The response includes url (the page to send donors to) and qr_url (a PNG QR code).
| Endpoint | Purpose |
|---|---|
GET /pay/api/donation-links | List the owner's links with totals. |
POST /pay/api/donation-links | Create a link. Fields: title, optional description, optional suggested_amounts. |
GET /pay/api/donation-links/<id> | Read one link and its totals. |
POST /pay/api/donation-links/<id> | Update title, description, suggested_amounts or active. |
GET /pay/api/donation-links/<id>/donations | List paid donations, newest first. Optional page and limit. |
GET /pay/api/donation-links/<id>/qr.png | Download the link's QR code. |
Webhooks
Members can register up to five HTTPS endpoints in the Webhooks tab. When a payment settles, AnythingPay posts one signed JSON event to every active endpoint of the payment recipient. Only the recipient's endpoints receive the event; the payer is never notified by webhook.
| Event | Sent when |
|---|---|
donation.paid | A donation link was paid, by balance checkout or by Stripe card checkout. |
payment.paid | An application checkout payment settled. |
payout.paid | The recipient accepted a pending application payout. |
subscription.charged | A monthly subscription charged successfully. The event includes the receipt. |
subscription.payment_failed | A monthly charge could not be collected and the subscription is past_due. |
subscription.cancelled | The application cancelled a subscription. |
Deliveries use POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | AnythingPay-Webhooks/1.0 |
X-AnythingPay-Event | The event type, such as donation.paid. |
X-AnythingPay-Signature | t=<unix>,v1=<hex digest>, an HMAC-SHA256 over <timestamp>.<raw body>. |
The endpoint must answer with a 2xx status within 5 seconds. Redirects are not followed. Transient failures are retried with backoff (30 seconds, 2 minutes, 10 minutes, then 1 hour) for up to five attempts, and the response status and the first 500 characters of the response body are recorded in the Webhooks tab.
{
"id": "pay_evt_1042_donation_paid",
"type": "donation.paid",
"created": 1761700000,
"data": {
"object": {
"id": 1042,
"reference": "donation-7a91",
"amount_usd": 5.0,
"currency": "USD",
"status": "paid",
"description": "Keep the servers running",
"payer": {"id": 482, "username": "example"},
"payee": {"id": 91, "username": "app-owner"},
"donation_link": {"id": 12, "title": "Support my project", "slug": "support-me"},
"client": null
}
}
}
Verify the signature against the exact raw request body before parsing it, and reject a timestamp that is more than a few minutes old:
import hashlib
import hmac
import time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
try:
parts = dict(item.split("=", 1) for item in header.split(","))
timestamp = int(parts.get("t", ""))
signature = parts.get("v1", "")
except (TypeError, ValueError):
return False
if abs(time.time() - timestamp) > 300:
return False
signed = f"{timestamp}.".encode("ascii") + raw_body
expected = hmac.new(
secret.encode("utf-8"), signed, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
async function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((part) => part.split("=")),
);
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const encoder = new TextEncoder();
const prefix = encoder.encode(`${timestamp}.`);
const body =
typeof rawBody === "string" ? encoder.encode(rawBody) : rawBody;
const signed = new Uint8Array(prefix.length + body.length);
signed.set(prefix);
signed.set(body, prefix.length);
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const digest = new Uint8Array(
await crypto.subtle.sign("HMAC", key, signed),
);
const expected = Array.from(digest, (byte) =>
byte.toString(16).padStart(2, "0"),
).join("");
const received = (parts.v1 || "").trim().toLowerCase();
if (received.length !== expected.length) return false;
let mismatch = 0;
for (let i = 0; i < expected.length; i += 1) {
mismatch |= expected.charCodeAt(i) ^ received.charCodeAt(i);
}
return mismatch === 0;
}
Store each event's id and ignore a replay. Delivery is at least once; the signature header is not covered by the timestamp check alone. Trust the signed body's type, not the unsigned X-AnythingPay-Event header.
Rotating an endpoint's secret takes effect immediately. The new secret is shown once, like the first one. Remove the old secret from your receiver at the same time.
Security
- Two-factor authentication is required to approve a checkout. It cannot be approved with only the account session.
- The member sees the application, amount, items and description before approving. The checkout data is bound to the request; it cannot be changed by editing the page.
- Checkout payments settle immediately. Payouts stay
pendinguntil the recipient accepts. Do not deliver goods or services until the status endpoint returnspaid. - Payouts debit the application owner's balance only when the recipient accepts. Keep the client secret server-side; anyone with it can create payments from that balance within the configured limits.
- Reuse the same
referencewhen retrying a checkout link. A settled reference never moves money twice. - The amount is settled in USD. The exact amount is shown before approval.
- Per-payment and daily limits apply. A request that exceeds them is refused before any balance changes.
- Members can review their sent and received payments under Payment history.
- Application names are self-registered and are not verified. The checkout shows the app owner; compare the status response before delivering anything.