Create Tip Jar Map Developers Articles

BitcoinTipJar

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.

URL Schema
// 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=dark
Style the card
amount={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}
Hand them out
// 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.
Get the payment data back
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.
Embed it in your own page
<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.

What will you build?
Lost your key?
Keyed endpoints
Check a Lightning address
// 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.
Hand your users tip pages
// 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.

Connect
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

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.

tip.settled
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
Check the signature
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

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.