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 when donations are not active for the show.
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:
{
"currency": "USD",
"suggested_amounts": [500, 1000, 2500],
"allow_custom_amount": true,
"minimum_custom_amount": 100,
"collect_donor_name": true,
"collect_donor_message": true,
"thank_you_message": "Thank you for supporting the show!",
"turnstile_site_key": ""
}
| Field | Description |
|---|---|
currency |
ISO currency code for this show. |
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_message |
Confirmation copy. |
turnstile_site_key |
The Cloudflare Turnstile site key, or an empty string. |
All amounts are integers in the smallest currency unit — 500 is $5.00 in USD and ¥500 in JPY. Minor units are not hundredths in every currency, so read currency rather than dividing by 100.
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. Upper-cased and truncated to three characters; falls back to the show’s configured 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. |
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.
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. |
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. |
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 means no typed number will succeed — usually a show that accepts only its suggested amounts. 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.