signalapp/Signal-Server · error · BadRequestException

receipt credential request failed verification

Error message

receipt credential request failed verification

What it means

Receipt credential requests carry a zero-knowledge proof that is verified server-side before a receipt is issued. If issueReceiptCredential throws VerificationFailedException, the proof did not verify against the server's secret keys, so the request is rejected with a 400 BadRequest. This means the request bytes parse but are cryptographically invalid (tampered, replayed, or for a different server).

Solutions

  1. Generate the ReceiptCredentialRequest fresh using the current production server's public parameters
  2. Do not modify any fields of the credential request after generation
  3. Ensure the client is compiled against the same serverSecretParams/public keys the server actually uses
Defensive patterns

Strategy: validation

Validate before calling

// client-side: generate and locally verify the proof before sending
ReceiptCredentialRequest req = zkReceiptOperations.createReceiptCredentialRequest(publicParams, receiptSerial, amount);
// ensure it was created against the CURRENT server public parameters

Try / catch

try { /* API call */ } catch (BadRequestException e) { if (e.getMessage().contains("failed verification")) { rebuildWithCurrentServerParams(); } }

Prevention

When it happens

Trigger: POST to the boost donation endpoint with a ReceiptCredentialRequest whose ZK proof fails verification — e.g. modified expiration/level fields, credentials generated against different server parameters, or a replayed request.

Common situations: Client built against a staging server's public parameters but pointed at production; tampered or reconstructed credential requests; protocol version drift between client and server key material.

Related errors


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

Appendix: source

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

      throw new BadRequestException("invalid receipt credential request", e);
    }
    final Instant paidAt = oneTimeDonationsManager.getPaidAt(request.processor, paymentDetails.id(), paymentDetails.created());
    final Instant expiration = paidAt
        .plus(levelDetails.levelExpiration())
        .truncatedTo(ChronoUnit.DAYS)
        .plus(1, ChronoUnit.DAYS);
    try {
      issuedReceiptsManager.recordOneTimeIssuance(paymentDetails.id(), request.processor,
          receiptCredentialRequest, expiration);
    } catch (WriteConflictException _) {
      throw new WebApplicationException(Response.Status.CONFLICT);
    }
    final ReceiptCredentialResponse receiptCredentialResponse;
    try {
      receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
          receiptCredentialRequest, expiration.getEpochSecond(), levelDetails.level().getValue());
    } catch (final VerificationFailedException e) {
      throw new BadRequestException("receipt credential request failed verification", e);
    }
    Metrics.counter(SubscriptionController.RECEIPT_ISSUED_COUNTER_NAME,
            Tags.of(
                Tag.of(SubscriptionController.PROCESSOR_TAG_NAME, request.processor.toString()),
                Tag.of(SubscriptionController.TYPE_TAG_NAME, "boost"),
                UserAgentTagUtil.getPlatformTag(userAgent)))
        .increment();
    return Response.ok(
            new CreateBoostReceiptCredentialsSuccessResponse(receiptCredentialResponse.serialize()))
        .build();
  }
}

View on GitHub (pinned to 100ab61c82)