signalapp/Signal-Server · error · SubscriptionProcessorConflictException

existing processor does not match

Error message

existing processor does not match

What it means

addPaymentMethodToCustomer throws SubscriptionProcessorConflictException("existing processor does not match") when the subscription record's stored ProcessorCustomer was created with a different payment provider than the one handling the current request. A subscription is bound to exactly one processor (Stripe, Braintree, Google Play, App Store); you cannot add payment methods from a second processor to it.

Solutions

  1. Route the request to the processor that matches record.getProcessorCustomer().processor() instead of the configured one.
  2. If the user genuinely wants to switch processors, cancel the existing subscription first, then create a new subscription with the new processor.
  3. Verify server config/env so donation endpoints map to the same processor the client used at subscription creation.

Example fix

// before
PaymentProvider provider = PaymentProvider.BRAINTREE; // hardcoded
manager.addPaymentMethodToCustomer(creds, new BraintreeManager(...), setupFn);
// after
PaymentProvider provider = record.getProcessorCustomer().orElseThrow().processor();
manager.addPaymentMethodToCustomer(creds, getProcessor(provider), setupFn);
Defensive patterns

Strategy: validation

Validate before calling

Subscriptions.Record record = manager.getSubscriber(creds);
PaymentProvider existing = record.getProcessorCustomer().map(ProcessorCustomer::processor).orElse(null);
if (existing != null && existing != paymentProcessor.getProvider()) {
  throw new IllegalStateException("subscription already uses " + existing);
}

Type guard

boolean processorMatches(Subscriptions.Record r, PaymentProvider p) {
  return r.getProcessorCustomer().map(ProcessorCustomer::processor).map(p::equals).orElse(true);
}

Try / catch

try {
  manager.addPaymentMethodToCustomer(creds, processor, setupFn);
} catch (SubscriptionProcessorConflictException e) {
  // redirect user to the processor that owns the subscription or offer cancel-and-recreate
}

Prevention

When it happens

Trigger: Calling addPaymentMethodToCustomer with a subscriptionPaymentProcessor whose getProvider() differs from processorCustomer.processor() stored on the record (e.g. subscription created via Stripe, now adding a Braintree payment method).

Common situations: Client switches payment provider mid-subscription without first cancelling/migrating; wrong processor manager selected by configuration for an endpoint; user pays via app store then tries web card payment on the same subscription.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/4bea3c76c9ec2e1d. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java:274

      final SubscriberCredentials subscriberCredentials,
      final T subscriptionPaymentProcessor,
      final ClientPlatform clientPlatform,
      final ThrowingBiFunction<T, String, R, E> paymentSetupFunction)
      throws SubscriptionForbiddenException, SubscriptionNotFoundException, SubscriptionProcessorConflictException, E {

    Subscriptions.Record record = this.getSubscriber(subscriberCredentials);
    if (record.getProcessorCustomer().isEmpty()) {
      final ProcessorCustomer pc = subscriptionPaymentProcessor
          .createCustomer(subscriberCredentials.subscriberUser(), clientPlatform);
      record = subscriptions.setProcessorAndCustomerId(record,
          new ProcessorCustomer(pc.customerId(), subscriptionPaymentProcessor.getProvider()),
          Instant.now());
    }
    final ProcessorCustomer processorCustomer = record.getProcessorCustomer()
        .orElseThrow(() -> new UncheckedIOException(new IOException("processor must now exist")));

    if (processorCustomer.processor() != subscriptionPaymentProcessor.getProvider()) {
      throw new SubscriptionProcessorConflictException("existing processor does not match");
    }
    return paymentSetupFunction.apply(subscriptionPaymentProcessor, processorCustomer.customerId());
  }

  public interface LevelTransitionValidator {

    /**
     * Check is a level update is valid
     *
     * @param oldLevel The current level of the subscription
     * @param newLevel The proposed updated level of the subscription
     * @return true if the subscription can be changed from oldLevel to newLevel, otherwise false
     */
    boolean isTransitionValid(long oldLevel, long newLevel);
  }

  /**
   * Update the subscription level in the payment processor and update the table.

View on GitHub (pinned to 100ab61c82)