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

  1. Complete the registration session verification (submit the verification code / challenge) before calling registration
  2. Poll or await the session's verified state, then retry the registration call with the same sessionId
  3. 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

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


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)