diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java
index bcaa2a5a6053215bcb250c87076ae3aa1146d52d..617eced01f3c5938c817f96c112da5b7ca042577 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java
@@ -435,16 +435,7 @@ public class SubscriptionController {
 
     final SubscriptionProcessorManager manager = getManagerForProcessor(processor);
 
-    return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
-        .thenApply(this::requireRecordFromGetResult)
-        .thenCompose(record -> record.getProcessorCustomer()
-            .map(processorCustomer -> manager.setDefaultPaymentMethodForCustomer(processorCustomer.customerId(),
-                paymentMethodToken, record.subscriptionId))
-            .orElseThrow(() ->
-                // a missing customer ID indicates the client made requests out of order,
-                // and needs to call create_payment_method to create a customer for the given payment method
-                new ClientErrorException(Status.CONFLICT)))
-        .thenApply(customer -> Response.ok().build());
+    return setDefaultPaymentMethod(manager, paymentMethodToken, requestData);
   }
 
   public record SetSubscriptionLevelSuccessResponse(long level) {
@@ -987,6 +978,33 @@ public class SubscriptionController {
         });
   }
 
+  @POST
+  @Path("/{subscriberId}/default_payment_method_for_ideal/{setupIntentId}")
+  @Produces(MediaType.APPLICATION_JSON)
+  public CompletableFuture<Response> setDefaultPaymentMethodForIdeal(
+      @Auth Optional<AuthenticatedAccount> authenticatedAccount,
+      @PathParam("subscriberId") String subscriberId,
+      @PathParam("setupIntentId") @NotEmpty String setupIntentId) {
+    RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
+
+    return stripeManager.getGeneratedSepaIdFromSetupIntent(setupIntentId)
+        .thenCompose(generatedSepaId -> setDefaultPaymentMethod(stripeManager, generatedSepaId, requestData));
+  }
+
+  private CompletableFuture<Response> setDefaultPaymentMethod(final SubscriptionProcessorManager manager,
+      final String paymentMethodId,
+      final RequestData requestData) {
+    return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
+        .thenApply(this::requireRecordFromGetResult)
+        .thenCompose(record -> record.getProcessorCustomer()
+            .map(processorCustomer -> manager.setDefaultPaymentMethodForCustomer(processorCustomer.customerId(),
+                paymentMethodId, record.subscriptionId))
+            .orElseThrow(() ->
+                // a missing customer ID indicates the client made requests out of order,
+                // and needs to call create_payment_method to create a customer for the given payment method
+                new ClientErrorException(Status.CONFLICT)))
+        .thenApply(customer -> Response.ok().build());
+  }
     private Instant receiptExpirationWithGracePeriod(Instant itemExpiration) {
         return itemExpiration.plus(subscriptionConfiguration.getBadgeGracePeriod())
                 .truncatedTo(ChronoUnit.DAYS)
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java
index a988a460152edc25cda8bec475cdae680c399747..6e24b13f8d052b6d6ecd00e54e93e9d0e76e76bd 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java
@@ -31,6 +31,7 @@ import com.stripe.param.PaymentIntentCreateParams;
 import com.stripe.param.PaymentIntentRetrieveParams;
 import com.stripe.param.PriceRetrieveParams;
 import com.stripe.param.SetupIntentCreateParams;
+import com.stripe.param.SetupIntentRetrieveParams;
 import com.stripe.param.SubscriptionCancelParams;
 import com.stripe.param.SubscriptionCreateParams;
 import com.stripe.param.SubscriptionItemListParams;
@@ -63,7 +64,9 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
+import javax.ws.rs.ClientErrorException;
 import javax.ws.rs.InternalServerErrorException;
+import javax.ws.rs.NotFoundException;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
@@ -632,6 +635,33 @@ public class StripeManager implements SubscriptionProcessorManager {
         }, executor);
   }
 
+  public CompletableFuture<String> getGeneratedSepaIdFromSetupIntent(String setupIntentId) {
+    return CompletableFuture.supplyAsync(() -> {
+      SetupIntentRetrieveParams params = SetupIntentRetrieveParams.builder()
+          .addExpand("latest_attempt")
+          .build();
+      try {
+        final SetupIntent setupIntent = stripeClient.setupIntents().retrieve(setupIntentId, params, commonOptions());
+        if (setupIntent.getLatestAttemptObject() == null
+            || setupIntent.getLatestAttemptObject().getPaymentMethodDetails() == null
+            || setupIntent.getLatestAttemptObject().getPaymentMethodDetails().getIdeal() == null
+            || setupIntent.getLatestAttemptObject().getPaymentMethodDetails().getIdeal().getGeneratedSepaDebit() == null) {
+          // This usually indicates that the client has made requests out of order, either by not confirming
+          // the SetupIntent or not having the user authorize the transaction.
+          logger.debug("setupIntent {} missing expected fields", setupIntentId);
+          throw new ClientErrorException(Status.CONFLICT);
+        }
+        return setupIntent.getLatestAttemptObject().getPaymentMethodDetails().getIdeal().getGeneratedSepaDebit();
+      } catch (StripeException e) {
+        if (e.getStatusCode() == 404) {
+          throw new NotFoundException();
+        }
+        logger.error("unexpected error from Stripe when retrieving setupIntent {}", setupIntentId, e);
+        throw new CompletionException(e);
+      }
+    }, executor);
+  }
+
   /**
    * We use a client generated idempotency key for subscription updates due to not being able to distinguish between a
    * call to update to level 2, then back to level 1, then back to level 2. If this all happens within Stripe's