[BUG?] Billing attempt failures return PROCESSING_ERROR when the error message maps to a more specific code

We’re seeing a high volume of subscription billing attempt failures where errorCode (and processingError.code is PROCESSING_ERROR, but the accompanying errorMessage (or processingError.message) clearly corresponds to an existing, more specific value in the SubscriptionBillingAttemptErrorCode enum.

Over a two-day window on one shop (gateway: Authorize.net), ~83% of failed attempts were coded PROCESSING_ERROR. Examples, with the enum value we’d expect:

Error message Returned code Expected code Example attempt
“The credit card number is invalid.” PROCESSING_ERROR INVALID_NUMBER gid://shopify/SubscriptionBillingAttempt/112066429230
“This transaction has been declined.” PROCESSING_ERROR CARD_DECLINED gid://shopify/SubscriptionBillingAttempt/112150741294
“The credit card has expired.” PROCESSING_ERROR EXPIRED_CARD gid://shopify/SubscriptionBillingAttempt/112154542382
“The merchant does not accept this type of credit card.” PROCESSING_ERROR PAYMENT_METHOD_UNSUPPORTED gid://shopify/SubscriptionBillingAttempt/112192258350
“Customer Profile ID or Customer Payment Profile ID not found.” PROCESSING_ERROR INVALID_CUSTOMER_BILLING_AGREEMENT gid://shopify/SubscriptionBillingAttempt/112153264430

The docs say the error codes exist to “provide consistent results and help with dunning management” — which is exactly our use case. Our dunning logic branches on the code (e.g., expired card → prompt customer to update payment method; hard decline vs. soft decline → different retry strategies). When everything collapses into PROCESSING_ERROR, we’re forced to parse free-text error messages, which is fragile and gateway-specific.

Questions:

  1. Is PROCESSING_ERROR expected behavior for third-party gateways like Authorize.net (i.e., is fine-grained code mapping only implemented for Shopify Payments)?
  2. If so, is there a plan to map gateway response codes to the specific enum values?
  3. If not, is this a bug we should report with attempt IDs? (Happy to share more — we have 100s of examples.)

Note these all return as SubscriptionBillingAttemptPaymentError with a FAILED state, so the type structure is correct — it’s only the code that’s generic.

Hey @Brian_Singer! Thanks for reporting this and for including concrete examples. I traced the five attempts and found that the Authorize.net payment app sent reason.code = PROCESSING_ERROR in its paymentSessionReject requests, alongside the failure messages. Shopify accepted those requests and carried the submitted code through to the payment transaction and SubscriptionBillingAttempt.

The Payments Apps API rejection reasons support more specific values, including EXPIRED_CARD, CARD_DECLINED, INCORRECT_NUMBER, and INVALID_NUMBER. The raw Authorize.net response codes were not included in these callbacks, so we cannot yet confirm the intended enum for each message. I have raised this internally with the relevant team to review the provider mapping, whether this is currently expected and if it can be improved.

I’ll follow up here once I have something concrete to share, thanks again!

Quick update on this one @Brian_Singer. Since my original reply, we’ve compared behavior across other shops using the same Authorize.net integration on the same API version, and those shops are getting granular codes like INVALID_NUMBER for these same messages. That makes the all-PROCESSING_ERROR pattern you’re seeing look like unexpected behavior rather than a documented limitation, though we haven’t pinned down the root cause yet.

The payment app itself submits PROCESSING_ERROR in its paymentSessionReject call, and Shopify reports back exactly what the app sends. The rejection reason enum allows much more specific values, and the Admin API error codes surface those as-is, so the generic code is coming from the provider side of the integration rather than the subscription API layer.

This is with the team that owns that integration now. I’ll update the thread once I have more to share - thanks for your patience on this so far!

Hey @Donal-Shopify - something I’m noticing that’s related and may have been overlooked in recent deprecations. Since PROCESSING_ERROR is generic but the message provides details - its important to be able to get those details but they’ve been deprecated in the API. Here’s more detail on that:

With SubscriptionBillingAttempt.errorCode, errorMessage, and processingError deprecated in favor of the state union, there no longer appears to be any supported way to get a human-readable error message for a failed billing attempt.

The SubscriptionBillingAttemptFailedState.error union members only expose an error code:

  • SubscriptionBillingAttemptPaymentErrorcode only (this is the one related to the PROCESSING_ERROR we’re seeing)
  • SubscriptionBillingAttemptGeneralErrorcode only
  • SubscriptionBillingAttemptInventoryErrorcode + insufficientStockProductVariants

Only SubscriptionBillingAttemptUnexpectedError carries a message field.

Meanwhile, the deprecated field still returns useful detail the codes can’t provide. For example, on a declined card (API 2026-07):

{
  subscriptionBillingAttempt(id: "gid://shopify/SubscriptionBillingAttempt/...") {
    errorMessage   # "The credit card has expired."  (deprecated)
    state {
      ... on SubscriptionBillingAttemptFailedState {
        error {
          ... on SubscriptionBillingAttemptPaymentError {
            code   # PROCESSING_ERROR
          }
        }
      }
    }
  }
}

The gateway’s message (e.g. “The credit card has expired.”, or the underlying processor detail behind a generic PROCESSING_ERROR) matters a lot for merchants doing payment-failure triage — an enum like PROCESSING_ERROR alone often isn’t actionable. Right now the only way to surface it is to keep querying the deprecated errorMessage and hope it isn’t removed.

Questions:

  1. Is there a supported replacement for errorMessage on the state union that I’m missing?
  2. If not, is adding a message field to the failed-state error types on the roadmap?
  3. Can we rely on errorMessage continuing to work until an equivalent exists?

thanks in advance!

Hey @Brian_Singer, thanks for following up. I had a look into this and found that with the move to the state union, only SubscriptionBillingAttemptUnexpectedError carries a message, while PaymentError and GeneralError expose a code only.

errorCode, errorMessage, and processingError are deprecated but they still resolve (there is no stated sunset date) so dunning logic reading them isn’t at immediate risk.

Subscribing to the subscription_billing_attempts/failure webhook is another option worth testing as a place that text might surface. The public reference doesn’t publish a sample payload for that topic, so confirm on your own store’s failures that it delivers what you need before migrating off the deprecated fields.

The missing message on the typed errors is useful feedback for the team to consider so I’ve passed that on.

Your original report isn’t forgotten either. The all-PROCESSING_ERROR pattern is still with the team that owns that Authorize.net integration, and I’ll update this thread once there’s more to share on either front.