Trial Fraud Protection
Throttle ships a card-fingerprint check that detects the same physical card being reused across "fresh" customer accounts. The check runs automatically inside the auto-create flow and is also exposed as a pre-flight API endpoint for explicit gating.
How it works
- Every payment vault writes the processor's stable
payment_method.fingerprintonto thecustomer_payment_methodsrow. - When a subscription is created with
trialEndset (auto-create mode), the engine looks up all payment methods on this merchant with the same fingerprint, then checks whether any of those customers have a prior subscription that hadtrialEndset. - If a match exists, the new subscription is created without the trial — it starts in
activeimmediately, the buyer is charged at signup as expected, and asubscription.trial_blockedoutbound event fires so you can react.
reason: 'no_fingerprint_available' so you can layer your own checks on top. Failing-closed would be catastrophic during the rollout — many legitimate cards in your existing customer base have no fingerprint yet.Scope is per-merchant
A fingerprint that's been on Merchant A's trial does not block the same card from getting a trial on Merchant B. Different businesses, different fraud surfaces. Throttle deliberately scopes the check to (workspaceId, fingerprint) so we don't leak fraud signals across workspaces.
Pre-flight API
POST /api/v1/subscriptions/eligibility-check
Server-to-server endpoint, secured with your API key. Useful when you want to render different UI before subscribing (e.g., hide the "Start free trial" button when the buyer is ineligible).
POST /api/v1/subscriptions/eligibility-check
X-API-Key: sk_production_...
Content-Type: application/json
{
"paymentMethodId": "01HF8...uuid"
}Response:
{
"data": {
"eligible": true | false,
"reason": "card_already_used_for_trial"
| "payment_method_not_found"
| "no_fingerprint_available"
| undefined // when eligible:true with no caveat
}
}Using it from your backend
// MERCHANT BACKEND
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_SECRET_KEY!,
});
// Pre-flight check before stamping a trialEnd on a new subscription.
// You'd typically call this after the buyer's card has been vaulted
// (you have the customerPaymentMethods row id) and before showing
// the "Start your 14-day trial" confirmation screen.
const result = await subscriptions.checkTrialEligibility({
paymentMethodId: pmRowId,
});
if (!result.eligible) {
// Surfaces a reason: 'card_already_used_for_trial' is the abuse signal.
// You can fall back to a no-trial subscribe flow ("subscribe now, no
// trial") or refuse outright. Up to your product policy.
return showNoTrialOption({ reason: result.reason });
}
return showTrialOption();The trial_blocked webhook
When the auto-create flow gates a trial silently (without you calling the eligibility endpoint), Throttle still emits a webhook so you have a hook point to react. This is fired in addition to the normal subscription.created — you get both events.
// MERCHANT WEBHOOK
// Fires when the auto-create flow downgrades a trial-requested subscription
// to a no-trial subscription because the eligibility check failed. The
// subscription IS created — just without trialEnd. Use this hook to send
// a "we couldn't grant you a trial" email or to log the decision.
case 'subscription.trial_blocked':
// event.data.subscriptionId — the (active, no-trial) sub we created
// event.data.customerId
// event.data.paymentMethodId
// event.data.reason — 'card_already_used_for_trial' (or another)
// event.data.requestedTrialDays — what was originally asked for
await notifyTrialDeclined(event.data);
break;What this does NOT protect against
- Different cards from the same buyer. The fingerprint changes when the card changes. A buyer with three different cards can claim three trials.
- Disposable email signups. Block disposable email domains in your signup flow if your business cares; Throttle doesn't.
- IP / device velocity. Throttle doesn't see the buyer's IP. Add rate-limiting or a CAPTCHA at your edge if needed.
- Stolen cards. That's a payment-processor concern (provider auth-decline rules) — not a subscription concern.
Disabling the check
The check runs automatically when customerPaymentMethods schema is wired into the subscription service (default in production). There's no per-merchant toggle in v1 — if your business model wants unlimited trials per card, contact support and we'll discuss the right primitive.
Next
- Lifecycle and States — trial timing semantics + dunning curve.
- Subscription Webhooks — full event reference (now includes
subscription.trial_blocked).