Handling Rule Declines
You configured a rule in your payment provider dashboard — refuse American Express, refuse a card country, refuse prepaid cards. When it fires, the charge comes back refused, but no bank was ever asked. This guide covers what you get, how to tell one of your rules from another, and what to say to the shopper.
For the field-level contract, see blocked_by_rule in the error reference.
1. Give every rule its own code
Do this before anything else. When you create the rule in your provider dashboard, fill in its custom code field with a short label:
| Rule | Custom code |
|---|---|
| Refuse American Express | amex_not_accepted |
| Refuse prepaid cards | prepaid_refused |
| Refuse cards issued outside the EU | non_eu_card |
Leave that field blank and your provider emits one identical value for every rule on the account. There is then nothing to tell them apart with, rule_code reads null, and no amount of handling on your side can recover which rule fired.
2. Branch on it in your server code
if (intent.decline_code === "blocked_by_rule") {
switch (intent.rule_code) {
case "amex_not_accepted":
return "We don't accept American Express — please try Visa or Mastercard.";
case "prepaid_refused":
return "We can't accept prepaid cards for this order.";
default:
// A rule with no custom code, or one you have not handled yet.
return "That card was declined — please use a different one.";
}
}
Keep the fallback. A rule you add next month arrives here before you have written a case for it, and so does every rule you never named.
rule_code is also on the charge.failed, payment_intent.failed and session.failed webhooks. It is null whenever the decline block is — see when the decline block is absent, so reconcile your rule reporting against the create/confirm response rather than assuming every failure webhook can name the rule.
The normalized value arrives as failure_code and decline_code on every failed-family webhook payload — the same value under two names, so the switch above works unchanged in a webhook handler. rule_code keeps its name on both surfaces.
action is on the webhook too, from the same mapping the API response uses. It can still be null for a code we hold no classification for, so never read a missing action as permission to retry.
3. Put the check on your server, not in the page
On any response a buyer's browser can receive, a rule refusal is reported as the generic card_declined — it is never named. Branch on blocked_by_rule in front-end JavaScript and it will never match.
That is deliberate: your publishable key is readable from your page source, so naming the rule there would let anyone run cards in a loop and map out exactly what your rules refuse. The error reference explains the full boundary.
Read the decline on your server — from the payment intent or the webhook — and send your own message to the page.
4. What to tell the shopper
| Don't | Suggest they contact their bank. No bank was involved; they will be told nothing is wrong. |
| Don't | Offer a retry with the same card. action is hard — it will be refused for as long as the rule stands. |
| Don't | Blame the card type unless your rule is actually about the type. For a country or prepaid rule, another card of the same brand would have worked, and a shopper told otherwise will try one and fail again. |
| Do | Ask for a different payment method, and say why when your rule makes the reason clear. |
Testing your handling
blocked_by_rule cannot be produced by a test card — it depends on a rule configured on your account, not on a property of the card. To exercise your handling, configure a real rule in your provider dashboard against your sandbox account and run a matching card through it.
When rule_code is empty
- The rule has no custom code. Go back to step 1.
- The code you set was rejected by the format rule. Letters, digits,
_and-, up to 64 characters — and the first character must be a letter or a digit. A code like_amex_blockis refused outright and comes backnull, which looks exactly like never having named the rule. If you named a rule and still readnull, check the leading character first. - Your provider does not report rule refusals separately. In that case you would be seeing
generic_declinerather thanblocked_by_ruleat all — so do not treat the absence ofblocked_by_ruleas proof that no rule fired. See the availability note in the error reference.