Token. Firmware has to check in with the device token. The Arduino client does that with ota.begin() or ota.check(). The ESP-IDF client does that with simpleota_init() and simpleota_start(). You can also call the device API.
Rejected token. Arduino prints [SimpleOTAClient] WARNING: server rejected the token (HTTP 401/403). Check your project token. ESP-IDF prints server rejected the token (HTTP 401). Check your project token., with 401 or 403.
That message means the device token.
Wi-Fi. Join the network before the check (WiFi.begin on Arduino, esp_wifi_connect in the ESP-IDF example). A wrong name or password stays on "Never checked in". Both clients check as soon as they start, and after a failed check they wait the full interval (an hour by default) before the next one. On Arduino, pass a connectivity probe such as ota.begin(3600, nullptr, wifiUp) so each check waits for Wi-Fi. On ESP-IDF, call simpleota_start() once Wi-Fi has an IP, or set .is_connected in the config.
Server. The Arduino client always calls https://simpleota.com. The ESP-IDF client sends checks to .base_url, which defaults to that address.
OTA slots. A partition table with two OTA app slots is required for Arduino and for ESP-IDF. On ESP-IDF the first serial flash needs CONFIG_PARTITION_TABLE_TWO_OTA=y, CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y, and CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y. Each two-OTA slot is 1 MB, so build for size with CONFIG_COMPILER_OPTIMIZATION_SIZE=y if the build stops with "All app partitions are too small".
Check-in worked. ESP-IDF logs check: no update (...) with the server's status, for example no_active_deployment before you create a deployment. Arduino with SimpleOTAClient::setDebug(true) prints POST /api/v1/ota/check/ -> 200 and check: no update available.