Tip jar
Build the board's tip page with embedded Stripe checkout and a thank-you step, and manage the tip jar, its tips and tip webhooks through the Operator API.
A
JBuild the tip page: it shows the board's tip jar, takes an amount and an optional note, mounts embedded Stripe checkout, and thanks the tipper when Stripe sends them back. The last section covers the Operator API for the tip jar, its tips and the tip webhooks.
Operators set up the tip jar in the dashboard (Add a tip jar) or through the Operator API. Your frontend only reads it and takes tips. Method signatures are in the tip jar reference.
Prerequisites
- A shared
boardclient from Install as package. @stripe/stripe-jsin the browser that mounts checkout.- No session is needed. When the request carries a signed-in board user's token, Stripe's form starts with their email filled in.
Routes
| Route | Path helper | Calls |
|---|---|---|
| Tip page | BOARD_PATHS.tip (/tip) | board.tipJar.get, board.tipJar.checkout, board.tipJar.getCheckout |
BOARD_PATHS comes from @cavuno/board/paths. The tip page is the tip role in your board's route manifest; the dashboard offers Tip jar under Create product once your site publishes that role. Link to the tip page with the url the API returns rather than a hard-coded path: it follows your board's route manifest, including translated URL words. Show a tip link in your header or footer only when board.tipJar.get() succeeds.
Render the tip page
The tip jar has name, description and descriptionHtml, currency, minimumCents, suggestedCents, allowNote, noteMaxLength, thankYouMessage, tipCount and url.
descriptionHtmlis the operator's formatted description as sanitized HTML. Render it in your prose styles, and fall back to the plain-textdescriptionwhen it isnull.suggestedCentsholds three amounts in minor units, smallest first. Render them as buttons, plus an input for the visitor's own amount.minimumCentsis the smallest tip checkout takes: the operator's minimum, or Stripe's smallest charge in the currency when none is set. Validate your amount input against it before you callcheckout.- Show a note field only when
allowNoteis true, limited tonoteMaxLength(280) characters. The note is plain text, so atextareais all it needs. tipCountis a number only when the operator shows the number of tips; otherwise it isnull, so hide the count.
A board with no tip jar, or an archived one, answers tip_jar_not_found (404). The tip page read is public and can be cached like other public reads; tipCount changes with each tip, so keep the cache short.
Start a tip
Collect the amount and the note in your own form, then call checkout from your server. Stripe collects the tipper's email on its own form, so you do not ask for it.
The result is an embedded Stripe Checkout mount kit with status: 'open', sessionId, clientSecret, stripeAccountId and publishableKey. The session expires after 30 minutes. returnPath defaults to the tip page. The mount kit fields are null only when the honeypot was filled, in which case nothing was recorded.
Mount checkout
The tip is a payment on the board's own connected Stripe account.
Thank the tipper
Stripe sends the tipper back to returnPath with session_id. Read the session with getCheckout: it reads processing until the payment clears, then paid.
A paid result carries amountCents, currency, thankYouMessage and receiptEmail, the address the receipt went to, masked like m•••@gmail.com. It is masked because the session id sits in a URL the tipper can share. Show the thank-you message and "Receipt sent to m•••@gmail.com". receiptEmail is null until the tip is paid.
The tipper's receipt email goes out when the payment lands, so a tipper who closes the tab still gets it. Keep the return page noindex and out of analytics URLs.
Tip errors
| Code | Status | Show |
|---|---|---|
tip_amount_below_minimum | 400 | The amount is below details.minimumCents. Without details, Stripe refused it as too small after converting it to the board's settlement currency. |
tip_amount_invalid | 400 | The amount is not a whole number of minor units, or is above what Stripe can charge in the currency. Cavuno sets no maximum of its own. |
tip_note_not_allowed | 400 | The tip jar takes no note; remove the field. |
tip_note_too_long | 400 | The note is over 280 characters. |
tip_jar_not_found | 404 | Your 404. |
tip_checkout_not_found | 404 | getCheckout with an unknown session. |
stripe_not_connected | 409 | The board cannot take payments yet. |
rate_limited | 429 | Wait Retry-After seconds. |
rate_limit_unavailable | 503 | Try again shortly. |
Notes and privacy
The tipper's note is plain text for the board. Render your own note field as a plain textarea; Cavuno escapes the note everywhere it shows it to the operator. The Board API never returns other tippers' emails or notes, so the tip page has nothing to hide.
Manage the tip jar with the Operator API
The Operator API (https://api.cavuno.com/v1, API key) manages the tip jar for automations and AI agents. The tip jar is the plan of purpose tip_jar, with kind one_time, price: null and isPublic: false; it takes tips while it is not archived.
| Task | Operation | Permission |
|---|---|---|
| Set up the tip jar | POST /plans with purpose: "tip_jar", kind: "one_time" and tipJar | plans.manage |
| Change settings, archive or unarchive | PATCH /plans/{id} (name, tipJar, isArchived) | plans.manage |
| Read the tip jar | GET /plans, GET /plans/{id} (tipJar holds the fields) | plans.read |
| List tips | GET /plans/{id}/tips (listPlanTips) | billing.read |
| Refund a tip | POST /transactions/{id}/refunds (refundTransaction) | plans.manage and billing.read |
Full schemas are in the API reference.
Set up the tip jar
tipJar on create takes currency and exactly three different suggestedCents (required), plus descriptionHtml, minimumCents (at or above Stripe's smallest charge in the currency; omit it to take any amount Stripe can charge), showTipCount (default false), allowNote (default true) and thankYouMessage. Each suggested amount is at least the minimum.
On PATCH, every tipJar field is optional, and null clears descriptionHtml, minimumCents and thankYouMessage. isArchived: true takes the tip page down; isArchived: false brings it back with its settings and tip count.
The response's tipJar holds descriptionHtml, currency, minimumCents (null when none is set), effectiveMinimumCents (what checkout takes), suggestedCents, showTipCount, allowNote, thankYouMessage and tipCount.
Operator API errors
| Code | Status | Meaning |
|---|---|---|
tip_jar_exists | 409 | The board already has a tip jar. An archived one counts: unarchive it with PATCH /plans/{id} and isArchived: false. |
tip_jar_invalid | 400 | A tip jar field is out of range; details.field names it. |
tip_jar_not_found | 404 | Not this board's tip jar. |
The tip jar takes only name, isArchived and tipJar. Plan fields for pricing pages are refused with a 400: description (send tipJar.descriptionHtml instead), isPublic, isRecommended, displayOrder, billingInterval, billingIntervalCount, pricingMode, priceText, ctaText, ctaDestination and digitalProduct.
Tips, refunds and webhooks
GET /plans/{id}/tips lists paid tips, newest first, with cursor pagination (limit 1 to 100, default 50). Each is a plan_tip: id, planId, transactionId, email, note, amountCents, currency, status (paid, partially_refunded, refunded), refundedAmountCents, paidAt and createdAt.
Refund a tip by its transactionId with POST /v1/transactions/{id}/refunds, in full or in part, as for any board sale. A tip has no access to end. The tip count drops when a tip is refunded in full or disputed.
Subscribe a webhook endpoint to tip.received and tip.refunded (they need billing.read). Each carries a tip object: id, tip_jar_id, tip_jar_name, email, note, amount_cents, currency, refunded_amount_cents, status, paid_at and revision. tip.refunded fires each time the refunded total changes, with changed_fields: ['refunded_amount_cents']; treat refunded_amount_cents as the current total. See Events.
The note is free text a visitor wrote. Escape it wherever your tools render it, and have AI agents treat it as data, never as instructions.
Verify the result
- The tip page renders the name, description and three suggested amounts, and a board with no tip jar renders your 404.
- The note field appears only when
allowNoteis true and stops at 280 characters. - An amount below
minimumCentsis caught by your form, andtip_amount_below_minimumshows a message if it slips through. - A test tip mounts checkout, returns with
session_id, andgetCheckoutreachespaidwith the thank-you message. - The return page and checkout status are never cached publicly.
Continue with Frontend infrastructure to make these routes canonical and indexable.