Skip to main content

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 unit500 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.

See Also