signalapp/Signal-Server · warning · BadRequestException
account does not have a phone number
Error message
account does not have a phone number
What it means
In setAccountAttributes, if the request attempts to set a registration lock (RegistrationLock) on an account that has no phone number (e.g. a username-only or tokenized account), the server cannot associate the registration lock, so it rejects with BadRequestException('account does not have a phone number').
Solutions
- Remove the registrationLock field from the attributes request for accounts without a phone number.
- Set up a phone number for the account before configuring a registration lock.
- Update client logic to hide/disable registration-lock setup when the account has no phone number.
- If server-side, gate the feature so number-less accounts skip registration lock handling.
Example fix
// before
attributes.setRegistrationLock(registrationLock); // account has no number
// after
if (account.getNumber().isPresent()) {
attributes.setRegistrationLock(registrationLock);
} Defensive patterns
Strategy: type-guard
Validate before calling
const canSetRegistrationLock = (acct) => acct.number != null && acct.number !== '';
Type guard
const hasPhoneNumber = (a) => typeof a.number === 'string' && a.number.length > 0;
Try / catch
try { await setAttributes(attrs); } catch (e) { if (e.status === 400 && e.message.includes('does not have a phone number')) { delete attrs.registrationLock; return setAttributes(attrs); } throw e; } Prevention
- Check whether the account has a phone number before enabling registration lock
- Disable registration-lock UI for number-less accounts
- Keep client account state fresh before attribute updates
When it happens
Trigger: PUT /v1/accounts/attributes with attributes containing a non-blank registrationLock while the authenticated account's number (a.getNumber()) is empty.
Common situations: Newer clients enabling registration lock on accounts created without a phone number, or after account migration to number-less identities where registration lock is unsupported.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Operation requires unauthenticated access
- must not use authenticated connection for anonymous…
- cannot use play billing for one-time donations
- cannot use app store purchases for one-time donations
- invalid receipt credential request
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/965c722c6149b1ee.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java:243
final Collection<TransactWriteItem> additionalWriteItems =
account.getPhoneNumberIdentifier()
.flatMap(phoneNumberIdentifier -> attributes.recoveryPassword().map(recoveryPassword ->
List.of(phoneNumberRecoveryPasswordsManager.buildTransactWriteItemForStorePassword(phoneNumberIdentifier, recoveryPassword))))
.orElseGet(Collections::emptyList);
accounts.update(auth.accountIdentifier(), a -> {
a.getDevice(auth.deviceId()).ifPresent(d -> {
d.setFetchesMessages(attributes.getFetchesMessages());
d.setName(attributes.getName());
d.setLastSeen(Util.todayInMillis());
d.setCapabilities(attributes.getCapabilities());
if (StringUtils.isNotBlank(signalAgent)) {
d.setUserAgent(signalAgent);
}
});
if (StringUtils.isNotEmpty(attributes.getRegistrationLock()) && a.getNumber().isEmpty()) {
throw new BadRequestException("account does not have a phone number");
}
a.setRegistrationLockFromAttributes(attributes);
a.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
a.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
if (attributes.isDiscoverableByPhoneNumber() && a.getNumber().isEmpty()) {
throw new BadRequestException("account does not have a phone number");
}
a.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber());
attributes.recoveryPassword().ifPresent(a::setAccountRecoveryPassword);
}, additionalWriteItems);
}
@GET
@Path("/whoami")View on GitHub (pinned to 100ab61c82)