signalapp/Signal-Server · error · BadRequestException

cannot use play billing for one-time donations

Error message

cannot use play billing for one-time donations

What it means

App-store and Play Billing purchases cannot be redeemed as one-time boost donations; the receipt-credentials endpoint rejects those processors with HTTP 400. Only STRIPE and BRAINTREE processors are accepted for boosts.

Solutions

  1. Use STRIPE or BRAINTREE as the processor for one-time donations
  2. Submit Play Billing / App Store purchase tokens through the subscription in-app-purchase endpoints instead
  3. Fix the client enum so boost flows map to card/PayPal processors only

Example fix

// before
{ "processor": "GOOGLE_PLAY_BILLING", "paymentIntentId": "..." }
// after
{ "processor": "STRIPE", "paymentIntentId": "..." }
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED = ['STRIPE', 'BRAINTREE'];
if (!ALLOWED.includes(request.processor)) throw new Error(`boosts do not support processor ${request.processor}`);

Type guard

function supportsBoosts(p) { return p === 'STRIPE' || p === 'BRAINTREE'; }

Try / catch

try { await createBoostReceiptCredentials(req); } catch (e) { if (e.status === 400 && /play billing|app store/.test(e.message)) routeToSubscriptionIapFlow(req); else throw e; }

Prevention

When it happens

Trigger: Passing processor=GOOGLE_PLAY_BILLING (or APPLE_APP_STORE) in the CreateBoostReceiptCredentialsRequest to the boost receipt-credentials endpoint.

Common situations: Clients reusing the donation receipt-credential flow (meant for subscriptions) for in-app purchase receipts; wrong enum value selected when building the request.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/OneTimeDonationController.java:355

  @POST
  @Path("/receipt_credentials")
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  @ManagedAsync
  public Response createBoostReceiptCredentials(
      @Auth final Optional<AuthenticatedDevice> authenticatedAccount,
      @NotNull @Valid final CreateBoostReceiptCredentialsRequest request,
      @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) throws IOException {

    if (authenticatedAccount.isPresent()) {
      throw new ForbiddenException("must not use authenticated connection for one-time donation operations");
    }

    final Optional<PaymentDetails> maybePaymentDetails = (switch (request.processor) {
      case STRIPE -> stripeManager.claimOneTimePurchase(request.paymentIntentId);
      case BRAINTREE -> braintreeManager.claimOneTimePurchase(request.paymentIntentId);
      case GOOGLE_PLAY_BILLING -> throw new BadRequestException("cannot use play billing for one-time donations");
      case APPLE_APP_STORE -> throw new BadRequestException("cannot use app store purchases for one-time donations");
    });

    if (maybePaymentDetails.isEmpty()) {
      throw new WebApplicationException(Response.Status.NOT_FOUND);
    }
    final PaymentDetails paymentDetails = maybePaymentDetails.get();
    if (paymentDetails.status() == PaymentStatus.PROCESSING) {
      return Response.noContent().build();
    }
    if (paymentDetails.status() != PaymentStatus.SUCCEEDED) {
      throw new WebApplicationException(Response.status(Response.Status.PAYMENT_REQUIRED)
          .entity(new CreateBoostReceiptCredentialsErrorResponse(paymentDetails.chargeFailure())).build());
    }

    // The payment was successful, try to issue the receipt credential

    final OneTimeDonationUtil.DonationLevelDetails levelDetails;

View on GitHub (pinned to 100ab61c82)