signalapp/Signal-Server · error · NotAuthorizedException
registration session is unverified
Error message
registration session is unverified
What it means
Number-based registration can be tied to a registration session (e.g. verified via SMS/session tokens). If the supplied session exists but has not been verified (UnverifiedRegistrationSessionException), the controller converts it to a 401 NotAuthorizedException 'registration session is unverified'. The caller must complete the verification flow before attempting to create the account.
Solutions
- Complete the registration session verification (submit the verification code / challenge) before calling registration
- Poll or await the session's verified state, then retry the registration call with the same sessionId
- If verification isn't applicable, omit sessionId and use an alternative verification method (e.g. recovery password)
Example fix
// before
POST /v1/registration { "sessionId": "abc", ... } // session not yet verified
// after
POST /v1/registration/verify { "sessionId": "abc", "code": "123456" } then POST /v1/registration { "sessionId": "abc", ... } Defensive patterns
Strategy: try-catch
Validate before calling
if (sessionId != null && !registrationSessionIsVerified(sessionId)) {
awaitVerificationCompletion(sessionId); // poll / wait before registering
} Try / catch
try { /* registration */ } catch (NotAuthorizedException e) { if ("registration session is unverified".equals(e.getMessage())) { completeVerificationFlow(sessionId); } } Prevention
- Always finish the verification step before account creation
- Never reuse a sessionId from an abandoned flow
- Handle 401 by resuming (not restarting) the registration session
When it happens
Trigger: POST /v1/registration with a sessionId whose registration session has not completed verification — verifyAccess via phoneVerificationTokenManager raises UnverifiedRegistrationSessionException when the session's verification state is still pending.
Common situations: Client skips the verification step and jumps straight to account creation; user abandons SMS/challenge flow midway; sessionId reused before verification finished.
Related errors
- registration session is unverified
- recovery password could not be verified
- ?…
- number does not match session
- Missing required device capability
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/4b59eddd6d421275.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java:239
final String signalAgent)
throws RateLimitExceededException, InterruptedException, RegistrationLockFailureException {
if (registrationRequest.pniIdentityKey() == null) {
// RegistrationRequest checks that either all phone number-associated information is present or all is absent
throw new WebApplicationException("PNI keys and registration ID must be provided", 422);
}
final PhoneVerificationRequest.VerificationType verificationType;
try {
verificationType = phoneVerificationTokenManager.verify(
number,
requestContext.getHeaderString(HttpHeaders.USER_AGENT),
requestContext.getHeaderString(HttpHeaders.ACCEPT_LANGUAGE),
(String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME),
StringUtils.isNotBlank(registrationRequest.sessionId()) ? registrationRequest.decodeSessionId() : null,
registrationRequest.recoveryPassword());
} catch (final UnverifiedRegistrationSessionException e) {
throw new NotAuthorizedException("registration session is unverified");
} catch (final InvalidRegistrationSessionException e) {
throw new BadRequestException(e.getMessage());
} catch (final IOException e) {
throw new ServiceUnavailableException(e.getMessage());
} catch (final RecoveryPasswordVerificationFailedException e) {
throw new ForbiddenException("recovery password could not be verified");
}
rateLimiters.getRegistrationLimiter().validate(number);
// There can be at most one existing account for a set of numbers in the same equivalence class, so it's sufficient
// to find the first one.
final Optional<Account> existingAccount = Util.getAlternateForms(number)
.stream()
.map(accounts::getByE164)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();View on GitHub (pinned to 100ab61c82)