signalapp/Signal-Server · error · BadRequestException

Invalid receipt credential presentation

Error message

Invalid receipt credential presentation

What it means

registerAccountWithoutNumber parses the receiptCredentialPresentation bytes supplied in the RegistrationRequest through ReceiptCredentialPresentationFactory. If parsing fails (InvalidInputException) the client supplied malformed, truncated, or wrong-type data, so the controller throws a BadRequestException (HTTP 400). This happens before any cryptographic verification — the presentation could not even be deserialized.

Solutions

  1. Regenerate the ReceiptCredentialPresentation client-side from the original receipt credential with a current, matching zk-credentials library version.
  2. Verify the presentation bytes are passed through unmodified (no base64 double-encoding, truncation, or re-serialization) between client and server.
  3. Check client and server are using the same ZK receipt system (same ReceiptCredentialPresentation schema/generation).
  4. Log/inspect the raw presentation length before sending to catch empty or truncated payloads.

Example fix

// before: sending a stale/corrupted blob
request.setReceiptCredentialPresentation(oldBytes);
// after: rebuild from the verified receipt credential
ReceiptCredentialPresentation p = clientZkReceiptOperations
    .createReceiptCredentialPresentation(receiptSecretParams, receiptCredential);
request.setReceiptCredentialPresentation(p.serialize());
Defensive patterns

Strategy: validation

Validate before calling

byte[] presentation = request.receiptCredentialPresentation();
if (presentation == null || presentation.length == 0) {
  throw new IllegalArgumentException("receiptCredentialPresentation missing/empty");
}
// dry-run parse locally with the same factory/version before sending
new ReceiptCredentialPresentation(presentation);

Type guard

boolean isUsablePresentation(byte[] b) {
  try { new ReceiptCredentialPresentation(b); return true; }
  catch (InvalidInputException e) { return false; }
}

Try / catch

try {
  register(request);
} catch (BadRequestException e) {
  if (e.getMessage().contains("Invalid receipt credential presentation")) {
    regeneratePresentationAndRetryOnce();
  } else throw e;
}

Prevention

When it happens

Trigger: POST to the registration endpoint with receiptCredentialPresentation bytes that are not a valid ZK receipt credential presentation: corrupted base64/byte encoding, a presentation from a different ZK system/version, or random/garbage bytes in the field.

Common situations: Client built the presentation with a mismatched libsignal/zkcredential version; receipt data truncated in transit or in local storage; developer testing the login-purchase endpoint with hand-crafted or placeholder receipt payloads.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java:346

      final String password,
      final RegistrationRequest registrationRequest,
      final String userAgent,
      final String signalAgent) {

    if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {
      throw new BadRequestException("login purchases are not enabled");
    }

    registrationRequest.accountAttributes().recoveryPassword()
        .filter(ArrayUtils::isNotEmpty)
        .orElseThrow(() -> new WebApplicationException("Account recovery password is required", 422));

    final ReceiptCredentialPresentation receiptCredentialPresentation;
    try {
      receiptCredentialPresentation = receiptCredentialPresentationFactory
          .build(registrationRequest.receiptCredentialPresentation());
    } catch (InvalidInputException _) {
      throw new BadRequestException("Invalid receipt credential presentation");
    }
    try {
      serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);
    } catch (VerificationFailedException _) {
      throw new NotAuthorizedException("Receipt credential presentation verification failed");
    }

    final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());
    if (clock.instant().isAfter(receiptExpiration)) {
      throw new NotAuthorizedException("Receipt is already expired");
    }

    final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
    if (receiptLevel != ReceiptLevel.LOGIN.getValue()) {
      throw new BadRequestException("Invalid receipt level");
    }

    try {

View on GitHub (pinned to 100ab61c82)