Skip to main content

Membership Subscribe REST API

The endpoint behind the subscribe button on [benecaster_subscribe]. It signs a visitor up to one tier at one billing cadence, creating their WordPress account along the way when they are not already logged in.


POST /benecaster/v1/shows/{id}/subscribe

Subscribes a visitor to a tier. The free path skips payment entirely; the paid path creates the Stripe subscription from a PaymentMethod the browser has already confirmed.

Auth: Public. There is no nonce and no login requirement — the endpoint identifies a logged-in caller from their session, and treats anyone else as a new signup who must supply email and password. See Why a public endpoint can accept a password below; the ordering that makes this safe is a contract, not an implementation detail.

Rate limit: 10 requests per 5 minutes per IP address, separate from the general limit on Benecaster’s routes. Past that the endpoint answers 429 benecaster_rate_limited with a Retry-After header. A real signup is one request, or two or three when a card is declined and the visitor tries another, so the limit leaves room for several people signing up from one office or mobile network. It raises the cost of card testing, where someone runs stolen card numbers through a public payment form, but it does not detect it: Stripe Radar, on your own Stripe account, is what recognises that. /subscribe/confirm stays on the general limit.

Path parameter:

Param Type Description
{id} int Show post ID

Body parameters:

Param Type Required Description
tier_slug string yes Slug of the tier being subscribed to
price_id int preferred Identifies which of the tier’s billing cadences was chosen. Optional on free tiers
billing_interval string legacy month or year. Accepted as a fallback when price_id is absent
email string logged-out only Address for the new account. Ignored for a logged-in caller
password string logged-out only The password the visitor chose. Required on the logged-out branch — there is no fallback to a generated one — and ignored entirely for a logged-in caller
payment_method_id string paid tiers A confirmed Stripe PaymentMethod. Omit on free tiers, where it is ignored
address object paid tiers on a show that collects tax The subscriber’s billing address: { line1, line2, city, state, postal_code, country }, with country as an ISO 3166-1 alpha-2 code (US, GB). It is the value of Stripe’s Address Element. Ignored on a show that does not collect tax and on a free tier

Password rules

At least 12 characters — WordPress’s own suggested minimum, and the figure the signup form displays — and at most 4096 bytes. Length is counted in characters, not bytes, so a short passphrase in a non-Latin script is not wrongly rejected.

Length is the only rule. There is no required digit, symbol, or mixed case, because WordPress core enforces none of its own and a subscriber facing two different password rules on the same site has no way to make sense of it.

The 4096-byte ceiling is not a statement about password strength. Password hashing is deliberately slow, so an unbounded value on a public endpoint is a cheap way to spend the site’s CPU.

Why a public endpoint can accept a password

On the logged-out branch the endpoint checks whether the email already belongs to a WordPress account before it reads the password at all, and answers benecaster_login_required if it does. The payment method and price are checked next, before any account is created. An account the request created is deleted again if the signup then fails without charging anything, so a retry is not answered benecaster_login_required.

That ordering is the whole safety property. Anyone can call this endpoint, so every submitted password is untrusted. If a password were ever applied to an address that already had an account, the endpoint would be account takeover by email address alone. Do not build a client that expects the password to be honoured for an existing address, and if you are reimplementing this flow, keep the collision check first.

Sales tax and the billing address

A show collects sales tax when the podcaster has turned on Calculate tax automatically, Stripe Tax is ready in the show’s Stripe account, and every active paid tier and buy-up on the show has a tax category. On such a show a paid signup must carry address:

  • No usable address (missing, or no two-letter country) answers 400 benecaster_tax_address_required before anything else happens: no WordPress account, no Stripe customer, no card attached. The message asks the visitor to reload the page, because the usual cause is a form rendered before the show started collecting tax, which had no address fields.
  • An address Stripe cannot place answers 422 benecaster_tax_location_invalid. For a logged-out visitor this is checked after their WordPress account is created, so a retry should be made logged in.

The address is set on the subscriber’s Stripe customer, and the subscription is created with Stripe’s automatic tax turned on. Benecaster does not store or log it. Show the visitor the taxed total first, from /subscribe/tax-preview, described below.

Choosing the cadence

A tier carries N billing cadences rather than a fixed monthly/annual pair, so the request has to say which one — a bare interval name cannot identify a price on a tier that sells quarterly, six-monthly, and annual.

price_id names one cadence in the tier’s price collection, and it is what the shortcode sends: the data-prices JSON on each tier card carries one entry per cadence, each with its identifier, and the selected radio button supplies it.

billing_interval also works, as a convenience for simple clients — custom checkout buttons, third-party integrations. The server resolves month to the tier’s month × 1 price and year to its year × 1 price.

It cannot address anything else. When the tier has no month × 1 or year × 1 price to match, the server falls back to the tier’s default price, so on a tier that sells only quarterly the visitor is billed at its default cadence, not what they asked for. Send price_id in anything you write.

When both are supplied, price_id wins and billing_interval is ignored.

Response — subscribed (201):

{
  "subscription_id": 101,
  "redirect_url": "https://yoursite.com/podcast-account/",
  "status": "active"
}

redirect_url is your site’s account page, from benecaster_account_page_url(), with /podcast-account/ only as the fallback.

On a paid tier, a 201 means Stripe has reported the first payment as paid. Nothing is written on this site — no membership, no feed token, no welcome email — until it has.

Response — the card needs 3D Secure (202):

{
  "requires_action": true,
  "client_secret": "pi_…_secret_…",
  "stripe_subscription_id": "sub_…",
  "status": "incomplete"
}

The subscriber’s bank wants to verify the payment. Nothing has been written. Pass client_secret to Stripe.js stripe.handleNextAction() to show the bank’s check, then call /subscribe/confirm (below) with stripe_subscription_id. If the browser closes after the check and never calls it, the signup still completes from Stripe’s webhook. A subscription nobody verifies is expired by Stripe after about 23 hours, uncharged; for a guest, that also deletes the account the signup created. Call /subscribe/confirm after a failed check too, not only a successful one, so a guest’s signup is cancelled straight away.

Response — payment still processing (202):

{
  "pending": true,
  "stripe_subscription_id": "sub_…",
  "status": "incomplete"
}

A payment method that settles later. The signup completes from the webhook once Stripe marks it paid, and the welcome email is how the subscriber hears. pending is not a failure — tell the visitor their membership is being set up.

A client that treats any 2xx as success must now check the body. Both 202 answers mean not subscribed yet: there is no subscription_id and no feed.

Errors:

Code HTTP Cause
rest_invalid_param 400 No tier_slug; a logged-out request with no valid email; or a paid tier with no payment_method_id
benecaster_rate_limited 429 More than 10 requests from this IP address in 5 minutes. Wait for the Retry-After header’s number of seconds
benecaster_tier_unavailable 404 No tier with that slug on this show, or the tier is inactive, or {id} is not a published show (draft, scheduled, private, trashed, or not a show). Refused before any account, Stripe Customer or charge is created. /subscribe/confirm and /subscribe/tax-preview do not make this check
benecaster_tax_address_required 400 The show collects sales tax and the request has no usable address. Refused before any account is created
benecaster_tax_location_invalid 422 The show collects sales tax and Stripe cannot work out where the address is
benecaster_price_mismatch 404 The price_id does not belong to this tier
already_subscribed 409 The user already has an active subscription to this show. A member on a free tier taking out a paid tier is the one exception
benecaster_login_required 409 The email supplied already has an account. Resolves before the password is read
benecaster_password_required 400 Logged-out request with no password
benecaster_password_too_weak 400 Under 12 characters
benecaster_password_invalid 400 Over the byte ceiling, or otherwise unusable
stripe_not_configured 503 No Stripe keys saved for the active mode; the tier has no prices; or the chosen cadence has no Stripe Price for the active mode yet (see below)
benecaster_stripe_error 502 Stripe refused the request for another reason. The message is Stripe’s
benecaster_billing_unavailable 409 This install is a copy of another site, so billing is refused. See below
benecaster_payment_declined 402 The first payment was declined. The incomplete Stripe subscription is cancelled at once, nothing is charged and nothing is written. The message is the card issuer’s reason when Stripe supplies one
subscription_persist_failed 500 The membership could not be saved on this site. On a paid sign-up the Stripe subscription has already been created, and Benecaster cancels it before answering; its first payment is not refunded, and the account is kept. No token or feed URL is issued and no welcome email is sent. Also returned for a free sign-up, where there is nothing at Stripe to cancel

No set-password email is sent from this endpoint

It used to send one, because the account it created had a generated password nobody had seen. That is no longer true: the visitor chooses their own password here, so they can log in the moment the request succeeds.

The guest_password_set email still exists, but only for accounts created for somebody — bulk enrollment, a subscriber added by hand, the donation-completion flow. A client that waits for that email after calling this endpoint will wait forever.

benecaster_billing_unavailable means the site is a clone, not that Stripe is broken

Benecaster records the site address it first ran at, and refuses to move money once it is running somewhere else. That is what a staging copy is: a database clone carrying production’s real Stripe subscription IDs. The refusal happens whatever mode the keys are in — pasting test keys onto a clone does not make it safe, because the subscription IDs it inherited are still the live ones.

The response message deliberately explains almost nothing. It reads “Billing changes are temporarily unavailable on this site. Please try again later or contact the podcast owner.” — the same sentence every time. This route is public, so anyone can reach it, and the detail that would explain the refusal (both site addresses and the wp-config.php constant that disarms the guard) belongs nowhere near an anonymous caller. The operator’s copy of that detail is in the debug log, written once per operation per hour.

Until recently this path answered stripe_not_configured (503) — and put the explanation in the message. If you have support notes, monitoring rules, or client-side handling that treat a 503 here as “the podcaster’s Stripe keys are wrong”, they are now wrong for the clone case. The status is 409 precisely because retrying cannot change the answer: a copy is a settled state, and a 5xx invites a retry loop.

If the site tripping this really is production — a genuine domain change, for instance — the operator adds define( 'BENECASTER_IS_PRODUCTION', true ); to wp-config.php. See Using a Staging Site.

“Not yet available for purchase” is a transient state, not a bug

Stripe Prices are immutable, so changing an amount cannot edit the existing Price — Benecaster nulls the stored Stripe IDs and mints a replacement. Between those two moments the cadence exists, is visible in the admin, and cannot be checked out: the request answers 503 stripe_not_configured with the message “This tier is not yet available for purchase. Please try again shortly.”

The window is short. A client hitting this should retry rather than surface a hard failure, and an operator seeing it persist should check that the Stripe keys for the active mode are valid — a failing mint leaves the cadence unprovisioned indefinitely.


POST /benecaster/v1/shows/{id}/subscribe/confirm

Finishes a signup that answered requires_action, once the subscriber has passed their bank’s check.

Auth: Public. A new subscriber is not logged in yet, so there is nobody to authenticate. It is safe because it grants nothing Stripe has not been paid for: it re-reads the subscription from Stripe and completes only one the signup form created for this show, and only once it is paid — exactly what the webhook would do moments later.

Body parameters:

Param Type Required Description
subscription_id string yes The stripe_subscription_id from the 202 answer — a Stripe sub_… id

Responses:

HTTP Body Meaning
201 Same as a completed signup Paid, and the membership is written
202 { pending, stripe_subscription_id, status } Stripe has not caught up yet — its status moves a moment after the check. The webhook finishes it; do not treat this as a failure
402 benecaster_payment_declined The bank’s check failed. Nothing is charged or written. For a guest whose account the signup created, the incomplete Stripe subscription is cancelled and that account is deleted, so they can try again at once. A signed-in member’s subscription stays incomplete until Stripe expires it
400 rest_invalid_param No subscription_id, or not a sub_… id
404 benecaster_signup_not_found Stripe does not recognise the subscription

Idempotent. Calling it twice, or racing the webhook, writes the membership once.

A free member who starts a paid signup and abandons it — at the bank’s check or with a declined card — keeps their free membership.

Finishing a 3D Secure signup from your own form

If you build your own signup form instead of using [benecaster_subscribe], it has to handle the bank’s check itself. Post to /subscribe; on requires_action, run Stripe.js handleNextAction() and then post the subscription ID to /subscribe/confirm. Only a 201 means the subscriber is in.

const res  = await fetch( `${ rest }/shows/${ showId }/subscribe`, { method: 'POST', headers, body: JSON.stringify( payload ) } );
let   body = await res.json();
if ( ! res.ok ) throw new Error( body.message );          // 402 benecaster_payment_declined, 409, …

if ( body.requires_action ) {
    const { error } = await stripe.handleNextAction( { clientSecret: body.client_secret } );
    if ( error ) throw new Error( error.message );          // failed or abandoned — nothing was written
    body = await ( await fetch( `${ rest }/shows/${ showId }/subscribe/confirm`, {
        method: 'POST', headers,
        body: JSON.stringify( { subscription_id: body.stripe_subscription_id } ),
    } ) ).json();
}

if ( body.pending ) {
    showMessage( 'Your payment is being processed — we will email you when your membership is ready.' );
} else {
    window.location.href = body.redirect_url;               // 201: membership written, feed created
}

Treating any 2xx from /subscribe as success is the mistake to avoid. A 202 is a payment still waiting for the bank.

pending is not a failure. Stripe marks the subscription paid a moment after the check, the site finishes it from the webhook, and the welcome email reaches the subscriber. Don’t retry the signup — that would start a second subscription.


POST /benecaster/v1/shows/{id}/subscribe/tax-preview

Works out what a visitor will be charged, with tax, for the address they have entered, so a signup form can show the total before they pay. With tax added on top, the charge is not the price on the tier card. It creates nothing and charges nothing.

Auth: Public, for the same reason as /subscribe: the visitor is usually not logged in.

Rate limit: 60 requests per 5 minutes per IP address, separate from the general limit on Benecaster’s routes. Call it once the address is complete, and debounce it, rather than on every keystroke. Call it again when the visitor changes billing period, since the total depends on the price.

Body parameters:

Param Type Required Description
tier_slug string yes Slug of a paid tier on this show
price_id int preferred The chosen cadence, as on /subscribe. Without it the tier’s month × 1 price is used, falling back to its default
address object yes { line1, line2, city, state, postal_code, country }, country an ISO 3166-1 alpha-2 code. The value of Stripe’s Address Element

Response (200):

{
  "currency": "usd",
  "subtotal": 500,
  "tax": 44,
  "total": 544,
  "subtotal_display": "$5.00",
  "tax_display": "$0.44",
  "total_display": "$5.44"
}

subtotal, tax and total are in minor units of currency. The *_display strings are ready to show as they are. [benecaster_subscribe] shows them as Subtotal, Tax and Total.

Errors:

Code HTTP Cause
benecaster_tier_unavailable 404 No tier with that slug on this show, or the tier is inactive or free
benecaster_tax_not_applicable 409 This show does not collect sales tax. The normal answer on most shows: no address is needed, and the price on the tier card is the charge
benecaster_tax_location_invalid 422 No usable address, or Stripe cannot work out where it is. Ask the visitor to check it, and never show the untaxed price as the total instead
benecaster_price_mismatch 404 The price_id does not belong to this tier
stripe_not_configured 503 No Stripe keys, or the cadence has no Stripe Price for the active mode yet
benecaster_stripe_error 502 Stripe could not work out the tax just then. Try again

The address is passed to Stripe and dropped. Benecaster does not store or log it, and a custom form should not store it either.

See Show the taxed total in your own signup form for a worked example.


See Also

See Also

Need this built rather than just documented? See our services →