{"record":{"id":"f044eb97e95d84ab","repo":"signalapp/Signal-Server","slug":"cannot-use-play-billing-for-one-time-donations","errorCode":null,"errorMessage":"cannot use play billing for one-time donations","messagePattern":"cannot use play billing for one-time donations","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/OneTimeDonationController.java","lineNumber":355,"sourceCode":"\n  @POST\n  @Path(\"/receipt_credentials\")\n  @Consumes(MediaType.APPLICATION_JSON)\n  @Produces(MediaType.APPLICATION_JSON)\n  @ManagedAsync\n  public Response createBoostReceiptCredentials(\n      @Auth final Optional<AuthenticatedDevice> authenticatedAccount,\n      @NotNull @Valid final CreateBoostReceiptCredentialsRequest request,\n      @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) throws IOException {\n\n    if (authenticatedAccount.isPresent()) {\n      throw new ForbiddenException(\"must not use authenticated connection for one-time donation operations\");\n    }\n\n    final Optional<PaymentDetails> maybePaymentDetails = (switch (request.processor) {\n      case STRIPE -> stripeManager.claimOneTimePurchase(request.paymentIntentId);\n      case BRAINTREE -> braintreeManager.claimOneTimePurchase(request.paymentIntentId);\n      case GOOGLE_PLAY_BILLING -> throw new BadRequestException(\"cannot use play billing for one-time donations\");\n      case APPLE_APP_STORE -> throw new BadRequestException(\"cannot use app store purchases for one-time donations\");\n    });\n\n    if (maybePaymentDetails.isEmpty()) {\n      throw new WebApplicationException(Response.Status.NOT_FOUND);\n    }\n    final PaymentDetails paymentDetails = maybePaymentDetails.get();\n    if (paymentDetails.status() == PaymentStatus.PROCESSING) {\n      return Response.noContent().build();\n    }\n    if (paymentDetails.status() != PaymentStatus.SUCCEEDED) {\n      throw new WebApplicationException(Response.status(Response.Status.PAYMENT_REQUIRED)\n          .entity(new CreateBoostReceiptCredentialsErrorResponse(paymentDetails.chargeFailure())).build());\n    }\n\n    // The payment was successful, try to issue the receipt credential\n\n    final OneTimeDonationUtil.DonationLevelDetails levelDetails;","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/OneTimeDonationController.java#L337-L373","documentation":"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.","triggerScenarios":"Passing processor=GOOGLE_PLAY_BILLING (or APPLE_APP_STORE) in the CreateBoostReceiptCredentialsRequest to the boost receipt-credentials endpoint.","commonSituations":"Clients reusing the donation receipt-credential flow (meant for subscriptions) for in-app purchase receipts; wrong enum value selected when building the request.","solutions":["Use STRIPE or BRAINTREE as the processor for one-time donations","Submit Play Billing / App Store purchase tokens through the subscription in-app-purchase endpoints instead","Fix the client enum so boost flows map to card/PayPal processors only"],"exampleFix":"// before\n{ \"processor\": \"GOOGLE_PLAY_BILLING\", \"paymentIntentId\": \"...\" }\n// after\n{ \"processor\": \"STRIPE\", \"paymentIntentId\": \"...\" }","handlingStrategy":"validation","validationCode":"const ALLOWED = ['STRIPE', 'BRAINTREE'];\nif (!ALLOWED.includes(request.processor)) throw new Error(`boosts do not support processor ${request.processor}`);","typeGuard":"function supportsBoosts(p) { return p === 'STRIPE' || p === 'BRAINTREE'; }","tryCatchPattern":"try { await createBoostReceiptCredentials(req); } catch (e) { if (e.status === 400 && /play billing|app store/.test(e.message)) routeToSubscriptionIapFlow(req); else throw e; }","preventionTips":["Only send STRIPE or BRAINTREE for one-time donations","Route Play Billing / App Store receipts to subscription IAP endpoints","Constrain the processor field to the supported enum in the client type definitions"],"tags":["http-400","bad-request","donations","unsupported-operation"],"backgroundTag":"unsupported-operation","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}