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.

Build 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 board client from Install as package.
  • @stripe/stripe-js in 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

RoutePath helperCalls
Tip pageBOARD_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.

  • descriptionHtml is the operator's formatted description as sanitized HTML. Render it in your prose styles, and fall back to the plain-text description when it is null.
  • suggestedCents holds three amounts in minor units, smallest first. Render them as buttons, plus an input for the visitor's own amount.
  • minimumCents is 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 call checkout.
  • Show a note field only when allowNote is true, limited to noteMaxLength (280) characters. The note is plain text, so a textarea is all it needs.
  • tipCount is a number only when the operator shows the number of tips; otherwise it is null, 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

CodeStatusShow
tip_amount_below_minimum400The amount is below details.minimumCents. Without details, Stripe refused it as too small after converting it to the board's settlement currency.
tip_amount_invalid400The 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_allowed400The tip jar takes no note; remove the field.
tip_note_too_long400The note is over 280 characters.
tip_jar_not_found404Your 404.
tip_checkout_not_found404getCheckout with an unknown session.
stripe_not_connected409The board cannot take payments yet.
rate_limited429Wait Retry-After seconds.
rate_limit_unavailable503Try 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.

TaskOperationPermission
Set up the tip jarPOST /plans with purpose: "tip_jar", kind: "one_time" and tipJarplans.manage
Change settings, archive or unarchivePATCH /plans/{id} (name, tipJar, isArchived)plans.manage
Read the tip jarGET /plans, GET /plans/{id} (tipJar holds the fields)plans.read
List tipsGET /plans/{id}/tips (listPlanTips)billing.read
Refund a tipPOST /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

CodeStatusMeaning
tip_jar_exists409The board already has a tip jar. An archived one counts: unarchive it with PATCH /plans/{id} and isArchived: false.
tip_jar_invalid400A tip jar field is out of range; details.field names it.
tip_jar_not_found404Not 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 allowNote is true and stops at 280 characters.
  • An amount below minimumCents is caught by your form, and tip_amount_below_minimum shows a message if it slips through.
  • A test tip mounts checkout, returns with session_id, and getCheckout reaches paid with 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.