signalapp/Signal-Server · error · NotAuthorizedException
Receipt is already expired
Error message
Receipt is already expired
What it means
The receipt credential presentation verified cryptographically, but its embedded receiptExpirationTime (epoch seconds) is in the past relative to the server clock. The controller throws a NotAuthorizedException because a login purchase whose receipt has lapsed cannot be used to create an account. This is a time-validity check applied after signature verification.
Solutions
- Purchase/issue a new receipt and retry registration with the fresh receipt credential presentation.
- Sync the client device clock (wrong local clocks can cause the client to believe the receipt is still valid).
- Check the receipt's receiptExpirationTime client-side before submitting and request renewal if it is expired or near expiry.
- If testing, update fixture receipts to have future expiration timestamps.
Example fix
// before: blindly submitting a stored presentation
submit(presentation);
// after: check expiry first
if (Instant.ofEpochSecond(presentation.getReceiptExpirationTime()).isBefore(Instant.now())) {
presentation = renewReceipt();
}
submit(presentation); Defensive patterns
Strategy: validation
Validate before calling
Instant expiry = Instant.ofEpochSecond(presentation.getReceiptExpirationTime());
if (Instant.now().isAfter(expiry)) {
throw new IllegalStateException("receipt expired at " + expiry + " — renew before registering");
} Type guard
boolean isReceiptUsable(ReceiptCredentialPresentation p) {
return Instant.ofEpochSecond(p.getReceiptExpirationTime()).isAfter(Instant.now());
} Try / catch
try {
register(request);
} catch (NotAuthorizedException e) {
if (e.getMessage().contains("already expired")) {
purchaseNewReceiptAndRegister();
} else throw e;
} Prevention
- Check receiptExpirationTime client-side before every redemption attempt.
- Keep device clocks synced (NTP); avoid submitting receipts after long offline periods.
- Proactively renew receipts nearing expiry.
When it happens
Trigger: Registering with a receipt credential whose expiration time has passed: the client waited too long between purchase and registration, the device clock skew delayed use, or a long-stored receipt is replayed after expiry.
Common situations: Cached receipts in app storage used after the subscription window ended; offline devices presenting old receipts once they reconnect; testing with fixtures containing hardcoded past timestamps.
Related errors
- Receipt credential presentation verification failed
- Receipt already redeemed
- Invalid receipt credential presentation
- Invalid receipt level
- number does not match session
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/4d3aa2b80b18d5a4.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java:356
.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 {
final Account account = accounts.create(
registrationRequest.accountAttributes(),
registrationRequest.aciIdentityKey(),
receiptCredentialPresentation,
new DeviceSpec(
registrationRequest.accountAttributes().getName(),
password,
signalAgent,
registrationRequest.accountAttributes().getCapabilities(),
new DeviceIdentityInfo(registrationRequest.accountAttributes().getRegistrationId(), registrationRequest.deviceActivationRequest()View on GitHub (pinned to 100ab61c82)