Skip to main content

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:

RuleCustom code
Refuse American Expressamex_not_accepted
Refuse prepaid cardsprepaid_refused
Refuse cards issued outside the EUnon_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 webhook carries both names

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'tSuggest they contact their bank. No bank was involved; they will be told nothing is wrong.
Don'tOffer a retry with the same card. action is hard — it will be refused for as long as the rule stands.
Don'tBlame 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.
DoAsk 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_block is refused outright and comes back null, which looks exactly like never having named the rule. If you named a rule and still read null, check the leading character first.
  • Your provider does not report rule refusals separately. In that case you would be seeing generic_decline rather than blocked_by_rule at all — so do not treat the absence of blocked_by_rule as proof that no rule fired. See the availability note in the error reference.