For developers & AI agents
Every tip jar on BitcoinTipJar is just a URL. Tip links need no key, no account and no sign-up: build the link and it works. Tips go straight from the payer's Lightning wallet to the recipient's — we are never in the middle of the money, and we take no fee.
The three things you can do: generate a tip page from parameters, embed the live card in your own page, and be told when a tip actually settles.
For the wallet check, and tip pages for all your users in one call, get a free API key.
// Generate a tip page via URL params — no API key, no account bitcointipjar.com/tip?ln={address}&name={name}&amounts={csv}
bitcointipjar.com/tip?ln=satoshi@coinos.io&name=Satoshi%27s+Coffee&amounts=1000,2500,5000,10000&accent=ff9b00&theme=darkamount={sats} pre-select that amount, land straight on the QR accent={hex} your brand color — 6 hex chars, no # theme=dark dark card for dark sites (the QR stays scannable) custom=0 drop the “Other” tile — your amounts only tagline={text} the line under the name (default “Tip with Bitcoin”) // Swap /tip for /poster with the same params → print-ready A4 poster bitcointipjar.com/poster?ln={address}&name={name}&amounts={csv}
// Give every user on your platform a jar: one link each, built from their // Lightning address and name. No account, no API key, and no money passes // through you or us. Each tip goes straight to their own wallet. src=partner tag the links your platform hands out batch={your-name} your platform: a-z, 0-9, - and _, up to 32 characters
bitcointipjar.com/tip?ln=satoshi@coinos.io&name=Satoshi&src=partner&batch=yourplatform// Tagged, your users’ jars are counted apart, so we can tell you how // they’re doing. A jar a user claims keeps the tag.
ref={string} opaque ref echoed back to you (song, order, table…) tag={text} your own data, passed through — on any tip link: a jar, ln=, an embed, a poster. Like ref, but the card stays a tip card, and a jar’s own records keep it. Up to 200 characters. notify={url} HTTPS webhook — fires only after the payment settles // We confirm settlement with the wallet (LUD-21 verify), then POST: { ref, tag, site, amount_sats, payer, pr, verify_url, settled_at } // site: the site an embed sat on (its hostname, nothing more), recorded // on its own: no parameter. Null for a link opened anywhere else. // Re-check verify_url yourself before trusting the event. // payer is the name the tipper typed (LUD-12 comment) — self-reported, // so escape it. Verify-proof covers the payment, not the name.
<iframe src="https://bitcointipjar.com/embed?jar={name}" style="border:0" width="360" height="480"></iframe>
jar={name} a claimed jar — its branding, records and extras come with it, and the owner can change wallets without you re-pasting ln={address} or an address directly, for a jar nobody has claimed chrome=none bare card — no border, background or header block bg={hex} paint the frame to your panel color (beats transparency) // Same params as /tip. When the tip settles, the frame postMessages // {type:'tipjar:settled', ref, tag, amount_sats, payer, pr, verify_url}. // Treat it as a UI hint only — anything you gate on payment should // trust the notify= webhook (server-verified) and re-check verify_url. // The frame also reports its content height on every state change, so // the iframe can grow with the card instead of scrolling:
addEventListener('message', e => {
if (e.source !== frame.contentWindow || !e.data) return // only OUR frame
if (e.data.type === 'tipjar:height')
frame.style.height = Math.min(Math.max(e.data.px|0, 180), 1200) + 'px'
})// QR-encode the full URL for physical placement. // AI agents: construct URL → return link. // Optional: claim a vanity URL later bitcointipjar.com/mikespizza
Free API key
For the wallet check, and tip pages for all your users in one call. Tip links still need no key.
Keys are on their way. Check back soon.
Make a new key? Your old one stops working.
// The probe behind /wallets/: LUD-16, 06, 12, 09 and 21, and whether a browser // can reach the wallet. The address goes in the body, never the URL.
curl https://bitcointipjar.com/api/v1/check \ -H "Authorization: Bearer btj_…" \ -H "Content-Type: application/json" \ -d '{"address":"alice@coinos.io"}'
{ "address": "alice@coinos.io", "ok": true, "checked_at": "…", "cached": false,
"lud16": { "ok": true, "url": "https://coinos.io/.well-known/lnurlp/alice", "status": 200 },
"lud06": { "ok": true, "min_sats": 1, "max_sats": 100000000, "metadata_ok": true, "identifier_match": true,
"invoice": { "ok": true, "amount_matches": true, "description_hash_matches": true } },
"lud12": { "ok": true, "max_chars": 512 },
"lud09": { "seen": false, "tag": null, "ok": true },
"lud21": { "ok": true, "verify_host_matches": true, "answered_unpaid": true },
"browser": { "discovery": true, "callback": true, "verify": true },
"tip_page": { "works": true, "shows_paid": true, "carries_names": true },
"problems": [] }
// null means not checked. A result from the last 10 minutes comes back as it is;
// add "fresh": true to ask the wallet again. A key asks wallets afresh up to 20
// times a day until your email is confirmed, then 300, and a wallet is asked at
// most 20 times an hour by everyone together. Past those you get the address's
// last result with "cached": true, or a 429 with Retry-After when there is none.// Up to 100 at a time, and 10 count as one call. Only the address's shape is // checked; a bad one fails its own row and the rest come back.
curl https://bitcointipjar.com/api/v1/links \ -H "Authorization: Bearer btj_…" \ -H "Content-Type: application/json" \ -d '{"links":[{"ln":"alice@coinos.io","name":"Alice","amounts":[1000,2500,5000]}]}'
// Each link also takes accent, theme and tagline, as /tip does. Back comes // url, poster_url and embed_html, tagged src=partner&batch={your tag}, so the // jars your users claim are counted as yours. GET /api/v1/app your partner tag, your limits, your calls today and this week, and your share // Call it from your server: a key is a secret. Send it only in the // Authorization header; a key that arrives in a URL is switched off at once. // 100 calls a day and 20 a minute until your email is confirmed, then 5,000 // a day and 60 a minute. // Every answer carries X-RateLimit-Limit, -Remaining and -Reset (Unix seconds, // midnight UTC); a 429 adds Retry-After.
Connect a user’s jar
To see a user’s tips, or hear about each one as it lands, the jar’s owner says yes once. Send them to our page; we email the jar’s owner a link to approve, or they paste their edit link. You never hold a secret of theirs, and they can disconnect your app from their edit page at any time.
https://bitcointipjar.com/developers/connect/?app={your app_id}&jar={their jar}&state={your value}// After the yes, their browser comes back to the first return URL you // registered (or the one you name with return_url=, which must match one exactly): {return_url}?jar={name}&state={your value}&connected=1 // Register up to 5 return URLs, your webhook and your payout address:
curl -X PATCH https://bitcointipjar.com/api/v1/app \ -H "Authorization: Bearer btj_…" \ -H "Content-Type: application/json" \ -d '{"return_urls":["https://yourapp.com/tipjar/back"],"webhook_url":"https://yourapp.com/hooks/tipjar","payout_ln":"you@wallet.com"}'
Your users’ tips
GET /api/v1/jars/{name} { "jar", "url", "exists", "confirms_payments" } for any key // With the owner’s yes, also: { "tips_count", "sats_total", "last_tip_at", "tips_30d", "sats_30d" } GET /api/v1/jars/{name}/tips?since={iso time}&limit=100 [{ "id", "amount_sats", "payer", "tag", "site", "settled_at" }] // Oldest first: the last settled_at is your next since. 403 without the // owner’s yes, or once they disconnect. id is the first 24 characters of the // payment’s hash. payer is what the tipper typed: untrusted, so escape it. // The owner’s own test tips and the first tip we send a new jar are left out.
Webhooks
For every tip a connected jar receives, we POST to your webhook_url. Every tip was already proved by our server with the receiving wallet, and pr and verify_url let you prove it again. Nothing in it moves money.
X-TipJar-Event: tip.settled X-TipJar-Delivery: evt_… the event’s id, the same on every retry X-TipJar-Signature: t={unix seconds},v1={hex} HMAC-SHA256 of "t.body" with your webhook_secret { "id": "evt_…", "type": "tip.settled", "created": 1791100000, "data": { "jar", "amount_sats", "payer", "tag", "site", "settled_at", "pr", "verify_url" } } // PATCH webhook_url answers webhook_secret once; each PATCH of it makes a new one. // Answer with a 2xx. Anything else, a redirect included, is tried again after // 1 minute, 5, 30, 2 hours and 12 hours. One payment is one event, so drop an id // you have seen. 50 failures in a row switch the webhook off, and we email you. POST /api/v1/app/webhook/test a signed ping, to check your end
const crypto = require('crypto')
// rawBody: the request body exactly as it arrived, before any JSON parsing
function verifyTipJar(rawBody, header, secret) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '')
if (!m) return false
if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false // older than 5 minutes
const want = crypto.createHmac('sha256', secret).update(m[1] + '.' + rawBody).digest()
const got = Buffer.from(m[2], 'hex')
return got.length === want.length && crypto.timingSafeEqual(got, want)
}Partner share
When your users buy stands, tap tags or extras for jars made from your links, you get 20% of what they pay, paid monthly in sats. Payouts start once we’ve checked your app. Set payout_ln with PATCH /v1/app; GET /v1/app shows your share. A payment counts once it has cleared (60 days for cards, 30 for bitcoin), and under US$5 carries to the next month.
Notes worth reading once
- Settlement is verified server-side. Before we fire your webhook
we re-fetch the invoice's LUD-21 verify URL, pinned to the receiving wallet's own
host, and read the amount out of the bolt11 rather than out of any request body.
Re-check
verify_urlyourself before you ship anything of value. payeris untrusted. It is the LUD-12 comment the tipper typed. Escape it. The verify proof covers the payment, not the name.- Not every wallet can confirm. Confirmation needs LUD-21 verify.
coinos, Alby, Blink, Speed and Stacker News have it; Wallet of Satoshi, Strike,
ZBD and npub.cash do not. A jar on those still receives tips perfectly well — you
just never hear about them, so no webhook fires. The live list, as our own probe
finds it, is /wallets/, and as JSON
/api/wallets. - Prefer
jar=overln=. A claimed jar carries its owner's branding and records, and the owner can change wallets without anything you pasted breaking.
Machine-readable summary
A plain-text description of this site and these endpoints lives at /llms.txt, for agents that would rather not parse HTML.