signalapp/Signal-Server · error · WebApplicationException

Missing required device capability

Error message

Missing required device capability

What it means

DeviceController.linkDevice rejects linking a new device that does not advertise the OPTIONAL_PHONE_NUMBER capability when the account has no PhoneNumberIdentifier (PNI). The capability is required because the new device must support accounts without a PNI; without it the server returns 409 with this message before validating PNI keys.

Solutions

  1. Update the linking client to declare DeviceCapability.OPTIONAL_PHONE_NUMBER in its capabilities set
  2. Upgrade the client app to a version that supports phone-number identifiers
  3. If you control the account state, ensure the account has a PNI or use a client matching the account's requirements
  4. Verify the capabilities payload sent during device activation includes optional-phone-number

Example fix

// before
Set<DeviceCapability> caps = EnumSet.of(DeviceCapability.STORAGE); // missing OPTIONAL_PHONE_NUMBER
linkDevice(account, new LinkDeviceRequest(attrs(caps), ...)); // 409 Missing required device capability
// after
Set<DeviceCapability> caps = EnumSet.of(DeviceCapability.STORAGE, DeviceCapability.OPTIONAL_PHONE_NUMBER);
linkDevice(account, new LinkDeviceRequest(attrs(caps), ...));
Defensive patterns

Strategy: validation

Validate before calling

if (account.getPhoneNumberIdentifier().isEmpty()
    && !deviceAttributes.capabilities().contains(DeviceCapability.OPTIONAL_PHONE_NUMBER)) {
  // upgrade client or add capability before attempting to link
}

Try / catch

try {
  client.linkDevice(account, request);
} catch (WebApplicationException e) {
  if (e.getResponse().getStatus() == 409) {
    // add DeviceCapability.OPTIONAL_PHONE_NUMBER and retry
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the link-device (PUT /v1/devices/link) flow for an account whose getPhoneNumberIdentifier() is empty, while the submitted linkDeviceRequest's deviceAttributes.capabilities() does not contain DeviceCapability.OPTIONAL_PHONE_NUMBER.

Common situations: Older or third-party clients that predate PNI support linking to a newer account created without a PNI; stale client builds that omit the optional-phone-number capability flag; accounts migrated to PNI-less state.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java:248

      description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
  public LinkDeviceResponse linkDevice(@HeaderParam(HttpHeaders.AUTHORIZATION) @NotNull BasicAuthorizationHeader authorizationHeader,
      @HeaderParam(HttpHeaders.USER_AGENT) @Nullable String userAgent,
      @NotNull @Valid LinkDeviceRequest linkDeviceRequest)
      throws RateLimitExceededException, DeviceLimitExceededException {
    final Account account = accounts.checkDeviceLinkingToken(linkDeviceRequest.verificationCode())
        .flatMap(accounts::getByAccountIdentifier)
        .orElseThrow(ForbiddenException::new);

    final DeviceActivationRequest deviceActivationRequest = linkDeviceRequest.deviceActivationRequest();
    final DeviceAttributes deviceAttributes = linkDeviceRequest.deviceAttributes();

    rateLimiters.getVerifyDeviceLimiter().validate(account.getAccountIdentifier());

    // Check the optional-phone-number capability before checking PNI keys, so we can give a better error code (since an
    // older device will improperly supply PNI keys for a PNI-less account)
    if (account.getPhoneNumberIdentifier().isEmpty() &&
        !linkDeviceRequest.deviceAttributes().capabilities().contains(DeviceCapability.OPTIONAL_PHONE_NUMBER)) {
      throw new WebApplicationException("Missing required device capability", 409);
    }

    final boolean allKeysValid =
      PreKeySignatureValidator
          .validatePreKeySignatures(account.getAccountIdentityKey(),
              List.of(deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey()),
              userAgent,
              "link-device")
          && account.getPhoneNumberIdentityKey()
                .map(pniIdentityKey ->
                  deviceActivationRequest.pniSignedPreKey().isPresent()
                      && deviceActivationRequest.pniPqLastResortPreKey().isPresent()
                      && PreKeySignatureValidator.validatePreKeySignatures(
                          pniIdentityKey,
                          List.of(deviceActivationRequest.pniSignedPreKey().get(), deviceActivationRequest.pniPqLastResortPreKey().get()),
                          userAgent,
                          "link-device"))
              .orElse(

View on GitHub (pinned to 100ab61c82)