Listener Support Donation Intent REST API
The two public endpoints behind the Stripe-mode donation form: one reads a show’s donation configuration, the other creates the Stripe PaymentIntent. Everything else in the Listener Support API is admin-only; these two are not.
Both routes are public, deliberately
Both carry permission_callback => '__return_true' — no authentication, no nonce, no cookie.
That is the feature, not an oversight. Anonymous tips are the entire point of Listener Support: a listener who has never visited before, holds no account and is not logged in must be able to donate. Requiring auth would mean requiring an account, which is the thing Listener Support exists to avoid.
What protects them instead is a set of controls that do not depend on knowing who the caller is — a dedicated rate limit, amount rules enforced server-side, and an optional bot challenge. See Protecting Against Card Testing, which is also clear about the limits of all three.
Both routes return 404 rest_not_found, Donations are not configured for this show., when donations are not active for the show, and also when {id} is not a live show: an id that belongs to no show, or to something other than a show, or a show in the Trash or never saved (an auto-draft). That check comes before anything else, so no PaymentIntent is created for an id that is not a show. A draft show still takes donations. A trashed show answers exactly as a missing one does, deliberately.
The link-mode route, POST /listener-support/donations, is separate and behaves differently: it refuses an id that is not a show (404 show_not_found) but still records a donation report for a trashed show, because that money was already paid off-site. See Listener Support Donations REST API.
GET /benecaster/v1/shows/{id}/listener-support/config
Returns the show’s donation configuration so the form can render and validate before submitting.
Auth: none.
Response:
{
"show_id": 42,
"public_key": "pk_live_…",
"currency": "USD",
"currency_decimals": 2,
"currency_exponent": 2,
"suggested_amounts": [500, 1000, 2500],
"allow_custom_amount": true,
"minimum_custom_amount": 100,
"turnstile_site_key": "",
"collect_donor_name": true,
"collect_donor_message": true,
"thank_you_mode": "message",
"thank_you_message": "Thank you for supporting the show!",
"thank_you_redirect_url": "",
"offer_enrollment": true,
"enrollment_label": "Add me to Dave’s Q&A's mailing list"
}
| Field | Description |
|---|---|
show_id |
The show this configuration belongs to. |
public_key |
The Stripe publishable key the form uses to confirm the payment. |
currency |
ISO currency code for this show. |
currency_decimals |
How many decimal places a person types and reads in this currency: 0 (JPY, UGX, ISK), 2 (USD and most others) or 3 (KWD, BHD, JOD, OMR, TND). Use it to format amounts for display. |
currency_exponent |
The power of ten Stripe counts this currency in: divide an amount by 10 ** currency_exponent to get whole units. The same as currency_decimals except for UGX and ISK, which are shown with 0 places but counted by Stripe with 2. |
suggested_amounts |
The preset buttons, in the smallest currency unit. |
allow_custom_amount |
Whether a supporter may type their own figure. |
minimum_custom_amount |
The floor a typed figure must clear. Lets the form validate locally instead of round-tripping into a 400. |
collect_donor_name / collect_donor_message |
Which optional donor fields this show collects. |
thank_you_mode |
What happens after a donation: message shows thank_you_message; redirect sends the supporter to thank_you_redirect_url. |
thank_you_message |
Confirmation copy. |
thank_you_redirect_url |
Where redirect mode sends the supporter. |
turnstile_site_key |
The Cloudflare Turnstile site key, or an empty string. |
offer_enrollment |
Whether the show offers donors a mailing-list opt-in (the show’s Offer mailing-list opt-in on donation form setting). |
enrollment_label |
The opt-in checkbox’s label, after the benecaster_donor_enrollment_label filter. Plain text, not HTML-encoded, so set it as text rather than HTML. An empty string when offer_enrollment is false. |
All amounts are integers in Stripe’s smallest currency unit — 500 is $5.00 in USD and ¥500 in JPY, 5000 is KWD 5.000, and 500000 is UGX 5,000. Minor units are not hundredths in every currency, so divide by 10 ** currency_exponent rather than by 100, and format with currency_decimals places.
turnstile_site_key is an empty string when Turnstile is off and also when it is half-configured — only one of the two keys saved. The form should render a challenge only when this is non-empty. A half-configured install returns empty precisely so that no widget appears whose verification would then refuse every donation.
This route is not separately rate limited. It reads already-public configuration, and throttling it would break a page legitimately rendering several donation forms.
POST /benecaster/v1/shows/{id}/listener-support/intent
Creates a Stripe PaymentIntent for a one-off donation and returns the client secret.
Auth: none. Rate limit: 10 requests per 5 minutes per IP.
Request body:
{
"amount": 500,
"currency": "USD",
"donor_email": "someone@example.com",
"donor_name": "Alex",
"donor_message": "Loved the last episode",
"enrollment_opt_in": true,
"turnstile_token": "0.abc..."
}
| Field | Description |
|---|---|
amount |
Required, smallest currency unit. See the amount rules below. |
currency |
Optional. The intent is always charged in the show’s own currency. If you send one, it must be the show’s code, in any letter case, compared whole; anything else is refused with benecaster_invalid_currency. |
donor_email |
Optional. Dropped silently when it is not a valid address. |
donor_name / donor_message |
Optional, capped at 255 and 1000 characters. Each is discarded unless the show has the matching collection toggle on — a client cannot opt itself in by sending the field. |
enrollment_opt_in |
Optional mailing-list opt-in. Gated on both the client’s intent and the show’s server-side toggle, so a tampered form cannot create a follower for a show whose podcaster never enabled enrollment. A donation to a show that is not published (a draft, for example) is still taken, but the opt-in enrols nobody and benecaster_donor_enrolled_as_follower does not fire. |
turnstile_token |
Optional challenge response. Needed only when the show’s site has Turnstile fully configured. |
Response: 200 OK
{ "client_secret": "pi_..._secret_...", "intent_id": "pi_...", "amount": 500, "currency": "USD" }
Amount rules
An amount is accepted when it is either one of the show’s suggested_amounts, or — when the show allows custom amounts — a figure that clears minimum_custom_amount. Zero and negative values are always rejected.
Whatever the show’s settings, the amount must also be one Stripe can charge, checked on the amount after the benecaster_donation_amount filter:
- At least Stripe’s minimum charge in that currency — USD 0.50, GBP 0.30, JPY 50, HKD 4.00, MXN 10, and 0.50 of the currency for one Stripe lists no minimum for (KWD 0.500, which is
500). - On the step Stripe charges the currency in — a multiple of
100for UGX and ISK, of10for KWD, BHD, JOD, OMR and TND. The donation form rounds onto the step itself; a direct caller or a filter may not, and is refused with the nearest amount that can be charged: That amount cannot be charged in UGX. The nearest amount that can is UGX 5,001.
The minimum never applies to the show’s own suggested amounts. The suggested list is matched first, deliberately, so a show offering a $3 button under a $25 floor keeps taking $3 through that button. A floor that could veto a button the podcaster can see on their own page would fail with the explanation nowhere.
Errors
| Code | HTTP | Meaning |
|---|---|---|
benecaster_invalid_show |
400 | Show ID missing or not a positive integer. |
benecaster_amount_below_minimum |
400 | A custom amount below the show’s floor. The error data carries minimum_custom_amount so the form can say what would work. |
benecaster_invalid_amount |
400 | Zero, negative, or an amount the show’s settings do not permit at all; also an amount below Stripe’s minimum charge in the currency, or off the step Stripe charges it in (the message names the nearest amount that can be charged). |
benecaster_invalid_currency |
400 | currency named a currency other than the show’s (including one that only starts with it, such as USDX). Message: This show does not accept donations in that currency. Checked after Turnstile and before the amount, so nothing reaches Stripe. |
benecaster_turnstile_failed |
403 | Turnstile is enabled and the challenge did not verify. |
benecaster_intent_blocked |
403 | A benecaster_should_create_donation_intent filter returned false. |
rest_not_found |
404 | Donations are not configured for this show, or {id} is not a live show (missing, not a show, trashed or an auto-draft). |
benecaster_billing_unavailable |
409 | The install is a copy of another site; the charge is refused. |
benecaster_stripe_unconfigured |
500 | Stripe credentials unusable for this show. |
benecaster_stripe_error |
502 | Stripe rejected the request. |
Do not treat the two 400-level amount refusals as one. benecaster_amount_below_minimum means a bigger number would succeed, and the floor is in the error data. benecaster_invalid_amount usually means no typed number will succeed — a show that accepts only its suggested amounts. Its two Stripe cases are the exception, and the message says so: show it to the supporter as it is. Telling a supporter to try more on a show that takes no custom amounts sends them round a loop with no exit.
benecaster_turnstile_failed is a 403 rather than a 400: the submission is well-formed and is being refused, not misunderstood. It covers a missing or malformed token, a success: false from Cloudflare, and Cloudflare being unreachable — once Turnstile is genuinely enabled, verification fails closed.
Verification runs before amount validation
When Turnstile is enabled, the challenge is verified before any amount is checked, and the ordering is load-bearing.
Checked the other way round, the route’s own refusals would answer an unverified caller: benecaster_amount_below_minimum returns the floor in its error data, and the accept/reject pattern of benecaster_invalid_amount enumerates the suggested list. A bot could read the show’s entire donation configuration out of the errors it was being handed. Verifying first means an unverified caller learns nothing.
Hooks
benecaster_should_create_donation_intent( bool $create, int $show_id, int $amount, string $currency ) and benecaster_donation_amount( int $amount, int $show_id ) run in that order — the block decision is made on the submitted amount, before any filter rewrites it.
Both take amounts as integers in minor units. See Listener Support Hooks, which explains why that catches people out.
A note on the 409
benecaster_billing_unavailable carries a deliberately generic message and never the underlying detail, which names the recorded site URL, the current one, and the wp-config.php constant that disarms the guard. This route takes anonymous payments, so a logged-out visitor reaches it — the operator’s copy of those facts goes to the debug log instead.
See Also
Need this built rather than just documented? See our services →