Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

if challenge_respons['status']; key error 'status' #537

Open
pielshawn opened this issue Jan 18, 2025 · 89 comments
Open

if challenge_respons['status']; key error 'status' #537

pielshawn opened this issue Jan 18, 2025 · 89 comments

Comments

@pielshawn
Copy link

Login is not operating as before. The RH app prompts me to confirm that I'm attempting to log in but the program does not authenticate.

@zabidin901
Copy link

@clarin-ebtio800090 is this meant to be a working solution? _validate_sherrif_id is not defined and the time module is not imported in your solution.

Thank you for digging into the issue however.

@alex-l-zhou
Copy link

@clarin-ebtio800090 is this meant to be a working solution? _validate_sherrif_id is not defined and the time module is not imported in your solution.

Thank you for digging into the issue however.

i tried it and it didn't work

@ravi-bharadwaj
Copy link
Contributor

Looks like only sms codes are used for authentication even though mfa is setup as authentication method. Not sure if its a new feature or bugged-release on RH

@alex-l-zhou
Copy link

Looks like only sms codes are used for authentication even though mfa is setup as authentication method. Not sure if its a new feature or bugged-release on RH

any idea how to handle this?

@HMSS013
Copy link

HMSS013 commented Jan 18, 2025

It seems to be that Robinhood insists on using the Trusted Device authentication method even after selecting and setting up the Authenticator App method and using the QR affiliated code.

@Kr1msonReaper
Copy link

Does anyone know if it's the SMS code in addition to the authentication app? Or is it now just SMS?

@HMSS013
Copy link

HMSS013 commented Jan 18, 2025

Does anyone know if it's the SMS code in addition to the authentication app? Or is it now just SMS?

from what i can tell, it's both.

when Authenticator App is selected in RobinHood it will set up with a QR & Code, but at login RobinHood will attempt to use Trusted Device instead.

and with SMS Authentication set Stocks no longer prompts for input of the SMS code.

@cpasean
Copy link

cpasean commented Jan 18, 2025

Same issue here. Would anyone be able to help me out?
//
print(challenge_response)
=> {'detail': 'Authentication credentials were not provided.'}

@ravi-bharadwaj
Copy link
Contributor

I have been trying to check with robinhood support as they seem to have abandoned multi factor authentication option and using only sms, photo, gov-id, bank as valid form of authentication methods

@Adelantado
Copy link

I am experiencing issues too. Bot will not log in and at the same time App will ask me to verify new device, which I do, but it dose not seem to go thru.

@ravi-bharadwaj
Copy link
Contributor

ravi-bharadwaj commented Jan 19, 2025

Do we any way of using the pickle file from browser to be used instead of pickle file we are generating from app. If there is a way, can you please share the steps.

@nat2k5us
Copy link

nat2k5us commented Jan 19, 2025

workaround:
I replaced the pickle access_token and was able to get it to work - the refresh token is valid for 90 days (typically).

Looks like this only works for 30 mins (sorry)

def replace_pickle(code):
    # Step 1: Create a Backup
    shutil.copy(original_file_path, backup_file_path)
    print(f"Backup created at: {backup_file_path}")

    # Step 2: Load the Original Pickle File
    with open(original_file_path, 'rb') as f:
        data = pickle.load(f)

    # Step 3: Replace the Bearer Token
    new_bearer_token = code
    if isinstance(data, dict) and "access_token" in data:
        data["access_token"] = new_bearer_token
        print("Bearer token replaced.")
    else:
        print("Bearer token not found in the pickle file.")

    # Step 4: Save the Updated Pickle File
    with open(original_file_path, 'wb') as f:
        pickle.dump(data, f)
        print(f"Updated pickle file saved at: {original_file_path}")

@Kr1msonReaper
Copy link

I am not the best at usely solely network requests to login, so I'm going to automate the login via selenium, extract the token, and then just give it to the robinhood module.

@Adelantado
Copy link

So what has changed from my perspective :

I can log in via browser, when I do now I get a Robinhood app notification on my phone to verify it's me and a prompt to allow a new device, which I do and I am able to log in to the account via browser. Until now this did not exist and verification was done by entering / passing the 2FA code generated via totp = pyotp.TOTP(log_2FA).now()

Now the Robinhood app notification on my phone happens the same when trying to log in via the script, however, after verifying new device in the app, I get an exception error and script just terminates it self ( have no procedure in place to handle that exception at this time ) . I still generate the 2FA code, the .token folder gets created but no pickle file is written, and the phone app validation fails and program just craps out on the exception created when trying to log in: login = r.login(log_User, log_Pass, mfa_code=totp)

Seems Authentication.py will need to be modified again, whish I was smart enough to figure that one. Sorry Folks and thanks.

@bhyman67
Copy link

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

@cpasean
Copy link

cpasean commented Jan 19, 2025

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

This works perfectly! You truly saved my day, Mr. Hyman. Many thanks and much respect!

@HMSS013
Copy link

HMSS013 commented Jan 19, 2025

brilliant stuff @bhyman67, where's the tip button on this thing?

now the age old question:

does anyone know how long these pickles last?

...and how can we make them last longer.

@Kr1msonReaper
Copy link

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

I appreciate you! Wasn't looking forward to trying to figure out how to fix this. Now I just need to find a service that can grab the sms code for me.

@jmfernandes
Copy link
Owner

@bhyman67 If you have time to create a PR to fix this issue I would greatly appreciate it! I just merged another PR, so you should update your branch.

@bhyman67
Copy link

bhyman67 commented Jan 19, 2025

@bhyman67 If you have time to create a PR to fix this issue I would greatly appreciate it! I just merged another PR, so you should update your branch.

Sounds good, @jmfernandes! I'll try to get to that sometime soon.

@Adelantado
Copy link

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

Thanks so much, I got the Bot to log in.
On a personal note: Now I just wish the new phone verification step could be avoid it / automated as it really screws the concept of having a 24/7/365 BOT running w no human interaction.

@p-o-f
Copy link

p-o-f commented Jan 20, 2025

Is the new phone verification step intentional? That seems to defeat the point of having 2FA if they're going to just ask for a code with SMS anyways...

@cottuzy
Copy link

cottuzy commented Jan 20, 2025

What is the login flow now with these changes if we were to use external 2FA app?

  1. enter user/pass
  2. enter 2FA otp
  3. verify on phone
  4. we get access token on robin?

@realhitta
Copy link

is it possible to login by script without needing to manually approve the login request in the Robinhood app?

@doormat-1010
Copy link

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

I was able to successfully generate a fresh pickle file using this method - thank you for putting this solution together. However, I deleted the pickle file and reran to refresh the pickle, and app on phone is no longer generating the verification. Two minute window expires and no pickle file is generated. Anyone else experiencing this? I tried reverting to original authentication.py to see if it would work after having approved via app verification, but no success. I'm wondering if the revised authentication.py needs additional edits for handling cases where Robinhood account has already logged approval via the new verification process?

@bhyman67
Copy link

bhyman67 commented Jan 21, 2025

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!
bhyman67/Mods-to-robin-stocks-Authentication@02e5491
You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

I was able to successfully generate a fresh pickle file using this method - thank you for putting this solution together. However, I deleted the pickle file and reran to refresh the pickle, and app on phone is no longer generating the verification. Two minute window expires and no pickle file is generated. Anyone else experiencing this? I tried reverting to original authentication.py to see if it would work after having approved via app verification, but no success. I'm wondering if the revised authentication.py needs additional edits for handling cases where Robinhood account has already logged approval via the new verification process?

Yeah I ran into the same problem... It worked initially, but then that same problem happened to me. Which is why I paused on going any further on the pull request.

I think you're right that this needs further edits and testing... This doesn't seem to be a full working solution yet...

#538 (comment)

@Kr1msonReaper
Copy link

For anyone who wants full automation, you can use something like selenium to manually plug in your username, password and possibly Auth app code and then plug in the sms code by linking robinhood to a Google voice number, having it forward the message to your Google email and then by using the Gmail app to read the code programmatically and plug it into the page.

Convoluted, but that's the free option. Otherwise, you could use a Twilio number.

@ssmanji89
Copy link

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

I was able to successfully generate a fresh pickle file using this method - thank you for putting this solution together. However, I deleted the pickle file and reran to refresh the pickle, and app on phone is no longer generating the verification. Two minute window expires and no pickle file is generated. Anyone else experiencing this? I tried reverting to original authentication.py to see if it would work after having approved via app verification, but no success. I'm wondering if the revised authentication.py needs additional edits for handling cases where Robinhood account has already logged approval via the new verification process?

Try a different IP?

@Adelantado
Copy link

Adelantado commented Jan 22, 2025

@ Kr1msonReaper; > For anyone who wants full automation, you can use something like selenium to manually plug in your username, password and possibly Auth app code and then plug in the sms code by linking robinhood to a Google voice number, having it forward the message to your Google email and then by using the Gmail app to read the code programmatically and plug it into the page.

Convoluted, but that's the free option. Otherwise, you could use a Twilio number.

Are you able to have your script log in without having to manually verify new device manually in the app ?
I am having trouble understanding how this may work as I do not receive any SMS code on my cell; I do get a notification to verify new device, then I log in in the app and finally validate prompt; there is no SMS code generated and no place to input anywhere.
Anyway, if you got this to work, would you care to share the code ? Thanks.

@Kr1msonReaper
Copy link

@ Kr1msonReaper; > For anyone who wants full automation, you can use something like selenium to manually plug in your username, password and possibly Auth app code and then plug in the sms code by linking robinhood to a Google voice number, having it forward the message to your Google email and then by using the Gmail app to read the code programmatically and plug it into the page.

Convoluted, but that's the free option. Otherwise, you could use a Twilio number.

Are you able to have your script log in without having to manually verify new device manually in the app ? I am having trouble understanding how this may work as I do not receive any SMS code on my cell; I do get a notification to verify new device, then I log in in the app and finally validate prompt; there is no SMS code generated and no place to input anywhere. Anyway, if you got this to work, would you care to share the code ? Thanks.

I believe the SMS option is only available if you don't have the app. I haven't implemented the automation for myself because a single approval seems to be good for a while. Might get to it when I have the time and motivation.

@Gates8911
Copy link

@mike-labadessa @nickreynolds84 @henryzhangpku @alex-l-zhou @schnup
Just found out that the extended session default time only works on certain accounts or certain circumstances, I just decided to get on my test account to experiment with my AI bot using my RH test account and I had trouble logging in, I went through the code over and over, verified all calls used and full login flow and it wouldnt let me login, so i tried the only thing it could be at that point, the default expiresIn time. Changed it back to 86400 seconds and it began working again, but you can still have your script trigger a refresh session command before the session expires to increase session times, my script still runs for weeks at a time and all i did is have it re-login after an error is caught, which typically i have some sort of minor routine error once or twice a day and this restarts the session time before the token expires as well. Or you can use the rs.update_session() function to be triggered every 23 hours. Ps. I already modified both of my above scripts to use 86400 default session time, sorry if this caused issues for anyone, if so it should be fixed now.

@mike-labadessa
Copy link

@mike-labadessa @nickreynolds84 @henryzhangpku @alex-l-zhou @schnup Just found out that the extended session default time only works on certain accounts or certain circumstances, I just decided to get on my test account to experiment with my AI bot using my RH test account and I had trouble logging in, I went through the code over and over, verified all calls used and full login flow and it wouldnt let me login, so i tried the only thing it could be at that point, the default expiresIn time. Changed it back to 86400 seconds and it began working again, but you can still have your script trigger a refresh session command before the session expires to increase session times, my script still runs for weeks at a time and all i did is have it re-login after an error is caught, which typically i have some sort of minor routine error once or twice a day and this restarts the session time before the token expires as well. Or you can use the rs.update_session() function to be triggered every 23 hours. Ps. I already modified both of my above scripts to use 86400 default session time, sorry if this caused issues for anyone, if so it should be fixed now.

Triggering the rs.update_session() every 23 hours makes sense because 86400 seconds equal one day. Maybe Robinhood devs hardcoded 86400 on their end somewhere. Thanks @Gates8911

@Gates8911
Copy link

If anyone needs another authentication to try let me know, I made one more, wasnt fully convinced I had all possibilities covered. Didnt automatically post it bc i didnt want to spam this chat with more code. Just email me if you need another one to try. [email protected]

@Adelantado
Copy link

If anyone needs another authentication to try let me know, I made one more, wasnt fully convinced I had all possibilities covered. Didnt automatically post it bc i didnt want to spam this chat with more code. Just email me if you need another one to try. [email protected]

Thanks so much for your time and contribution to the cause ;), same goes to all in here.
Fix is working for me as I am able to make the script log in having to authenticate device in the app, however, in my case and I am sure there are many out there in the same boat and, knowing that what I am about to say is on Robinhood's end and has nothing to do with the original API, this or any solution is less than ideal as it makes all the time ( almost 4 years ), money and effort poured into having BOTs running 24/7 on AWS without human interaction useless.
Bots were set up to check status on each other and if found to be down, launch each other; now I had to send myself a notification to make me aware Bot is down and include a pause at login until I am ready to connect to AWS, un-pause and authorize device on app which makes, as I said, all the work done useless. The reason of the pause is cause if you try to loop and keep trying to log in without authenticating in the app, Robin will flag your IP and you will start to get 429 errors which for may can be a game over.
Hope this info is useful to someone, and I also hope Robin will make changes to allow scripts to login automatically once again.
Thanks again to Yall!

@mobeston
Copy link

mobeston commented Feb 6, 2025

Just curious why no one here uses alpaca instead

@Kr1msonReaper
Copy link

Kr1msonReaper commented Feb 6, 2025 via email

@doctorcolossus
Copy link

How specifically are they overpriced? Aren't their spreads similar to Robinhood's?

Although actually building this unofficial Robinhood API has taken a lot of work, it doesn't seems to me like there is any difference in "programming skills" required to use it vs. using Alpaca's official one.

@Gates8911
Copy link

Gates8911 commented Feb 7, 2025

@doctorcolossus @Kr1msonReaper @nickreynolds84 @mobeston @Adelantado @mike-labadessa @henryzhangpku @alex-l-zhou @ravi-bharadwaj @pielshawn @zabidin901 @HMSS013 @jmfernandes @cpasean @nat2k5us @schnup
Since I see theres still a few issues people are having ill post my latest one that has had zero issues now across 3 different accounts, one using device approvals, another using email, and another using sms, (this code can be used with 3rd party 2fa but since robinhoods api doesnt support 2fa with the last update it is not necessary to include 2fa but will still work, I kept the option in bc the case may be that robinhood found security concerns with their 2fa and may fix it and change back to it, in which case it would be nice to still have some of the 2fa code, but this script really just needs username and pass even though 2fa is still an option). I also realize this is quite a bit longer than previous versions but it has to check for all 3 verification methods and added retries to the logic with various events and more output logs for different scenarios to better diagnose a failure. If this doesnt work for you then I would check login method thoroughly (I say this because I literally cant improve this script any further, if someone else can more power to ya, if this works for everyone I think it should be merged for now)

import getpass
import os
import pickle
import secrets
import time
from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *

def generate_device_token():
    """Generates a cryptographically secure device token."""
    rands = [secrets.randbelow(256) for _ in range(16)]
    hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)]
    token = ""
    for i, r in enumerate(rands):
        token += hexa[r]
        if i in [3, 5, 7, 9]:
            token += "-"
    return token


def _get_sherrif_id(data):
    """Extracts the sheriff verification ID from the response."""
    if "id" in data:
        return data["id"]
    raise Exception("Error: No verification ID returned in user-machine response")


def _validate_sherrif_id(device_token: str, workflow_id: str):
    """Handles Robinhood's verification workflow, including email, SMS, and app-based approvals."""
    print("Starting verification process...")
    pathfinder_url = "https://api.robinhood.com/pathfinder/user_machine/"
    machine_payload = {'device_id': device_token, 'flow': 'suv', 'input': {'workflow_id': workflow_id}}
    machine_data = request_post(url=pathfinder_url, payload=machine_payload, json=True)

    machine_id = _get_sherrif_id(machine_data)
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"

    start_time = time.time()
    
    while time.time() - start_time < 120:  # 2-minute timeout
        time.sleep(5)
        inquiries_response = request_get(inquiries_url)

        if not inquiries_response:  # Handle case where response is None
            print("Error: No response from Robinhood API. Retrying...")
            continue

        if "context" in inquiries_response and "sheriff_challenge" in inquiries_response["context"]:
            challenge = inquiries_response["context"]["sheriff_challenge"]
            challenge_type = challenge["type"]
            challenge_status = challenge["status"]
            challenge_id = challenge["id"]
            if challenge_type == "prompt":
                print("Check robinhood app for device approvals method...")
                prompt_url = f"https://api.robinhood.com/push/{challenge_id}/get_prompts_status/"
                while True:
                    time.sleep(5)
                    prompt_challenge_status = request_get(url=prompt_url)
                    if prompt_challenge_status["challenge_status"] == "validated":
                        break
                break

            if challenge_status == "validated":
                print("Verification successful!")
                break  # Stop polling once verification is complete

            if challenge_type in ["sms", "email"] and challenge_status == "issued":
                user_code = input(f"Enter the {challenge_type} verification code sent to your device: ")
                challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/"
                challenge_payload = {"response": user_code}
                challenge_response = request_post(url=challenge_url, payload=challenge_payload)

                if challenge_response.get("status") == "validated":
                    break

    # **Now poll the workflow status to confirm final approval**
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    
    retry_attempts = 5  # Allow up to 5 retries in case of 500 errors
    while time.time() - start_time < 120:  # 2-minute timeout 
        try:
            inquiries_payload = {"sequence": 0, "user_input": {"status": "continue"}}
            inquiries_response = request_post(url=inquiries_url, payload=inquiries_payload,json=True)
            if "type_context" in inquiries_response and inquiries_response["type_context"]["result"] == "workflow_status_approved":
                print("Verification successful!")
                return
            else:
                time.sleep(5)  # **Increase delay between requests to prevent rate limits**
        except requests.exceptions.RequestException as e:
            time.sleep(5)
            print(f"API request failed: {e}")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            print("Retrying workflow status check...")
            continue

        if not inquiries_response:  # Handle None response
            time.sleep(5)
            print("Error: No response from Robinhood API. Retrying...")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            continue

        workflow_status = inquiries_response.get("verification_workflow", {}).get("workflow_status")

        if workflow_status == "workflow_status_approved":
            print("Workflow status approved! Proceeding with login...")
            return
        elif workflow_status == "workflow_status_internal_pending":
            print("Still waiting for Robinhood to finalize login approval...")
        else:
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")

    raise TimeoutError("Timeout reached. Assuming login is approved and proceeding.")



def login(username=None, password=None, expiresIn=86400, scope='internal', store_session=True, mfa_code=None, pickle_path="", pickle_name=""):
    """Handles the login process to Robinhood, including multi-factor authentication, session persistence, and verification handling."""
    print("Starting login process...")
    device_token = generate_device_token()
    home_dir = os.path.expanduser("~")
    data_dir = os.path.join(home_dir, ".tokens")

    if pickle_path:
        if not os.path.isabs(pickle_path):
            pickle_path = os.path.normpath(os.path.join(os.getcwd(), pickle_path))
        data_dir = pickle_path

    if not os.path.exists(data_dir):
        os.makedirs(data_dir)

    creds_file = "robinhood" + pickle_name + ".pickle"
    pickle_path = os.path.join(data_dir, creds_file)

    url = login_url()
    login_payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'device_token': device_token,
        'try_passkeys': False,
        'token_request_path': '/login',
        'create_read_only_secondary_token': True,
    }

    if mfa_code:
        login_payload['mfa_code'] = mfa_code
    # If authentication has been stored in pickle file then load it. Stops login server from being pinged so much.
    if os.path.isfile(pickle_path):
        # **Load cached authentication session if available**
        if store_session:
            try:
                with open(pickle_path, 'rb') as f:
                    pickle_data = pickle.load(f)
                    access_token = pickle_data['access_token']
                    token_type = pickle_data['token_type']
                    refresh_token = pickle_data['refresh_token']
                    pickle_device_token = pickle_data['device_token']
                    login_payload['device_token'] = pickle_device_token
                    set_login_state(True)
                    update_session(
                            'Authorization', '{0} {1}'.format(token_type, access_token))
                    # Try to load account profile to check that authorization token is still valid.
                    res = request_get(
                        positions_url(), 'pagination', {'nonzero': 'true'}, jsonify_data=False)
                    # Raises exception if response code is not 200.
                    res.raise_for_status()
                    return({'access_token': access_token, 'token_type': token_type,
                            'expires_in': expiresIn, 'scope': scope, 
                            'detail': 'logged in using authentication in {0}'.format(creds_file),
                            'backup_code': None, 'refresh_token': refresh_token})
            except Exception:
                    print(
                        "ERROR: There was an issue loading pickle file. Authentication may be expired - logging in normally.", file=get_output())
                    set_login_state(False)
                    update_session('Authorization', None)
        else:
            os.remove(pickle_path)

    # **Attempt to login normally**
    if not username:
        username = input("Robinhood username: ")
        login_payload['username'] = username
    if not password:
        password = getpass.getpass("Robinhood password: ")
        login_payload['password'] = password

    data = request_post(url, login_payload)

    if data:
        try:
            if 'verification_workflow' in data:
                print("Verification required, handling challenge...")
                workflow_id = data['verification_workflow']['id']
                _validate_sherrif_id(device_token, workflow_id)

                # Reattempt login after verification
                data = request_post(url, login_payload)

            if 'access_token' in data:
                token = '{0} {1}'.format(data['token_type'], data['access_token'])
                update_session('Authorization', token)
                set_login_state(True)

            if store_session:
                with open(pickle_path, 'wb') as f:
                    pickle.dump({'token_type': data['token_type'],
                                 'access_token': data['access_token'],
                                 'refresh_token': data['refresh_token'],
                                 'device_token': login_payload['device_token']}, f)
                return data
        except Exception as e:
            print(f"Error during login verification: {e}")

    print("Login failed. Check credentials and try again.")
    return None


@login_required
def logout():
    """Logs out from Robinhood by clearing session data."""
    set_login_state(False)
    update_session('Authorization', None)
    print("Logged out successfully.")

@Adelantado
Copy link

Adelantado commented Feb 8, 2025

Just curious why no one here uses alpaca instead

In my case, and not to be snappy, after almost four years my "trading" code is about 20K lines, and the "add on platform" for supported functionality about another 10K . Wanna help me scratch that and do Alpaca ?
And I am not kidding....

Image

@Dylan-86
Copy link

@doctorcolossus @Kr1msonReaper @nickreynolds84 @mobeston @Adelantado @mike-labadessa @henryzhangpku @alex-l-zhou @ravi-bharadwaj @pielshawn @zabidin901 @HMSS013 @jmfernandes @cpasean @nat2k5us @schnup Since I see theres still a few issues people are having ill post my latest one that has had zero issues now across 3 different accounts, one using device approvals, another using email, and another using sms, (this code can be used with 3rd party 2fa but since robinhoods api doesnt support 2fa with the last update it is not necessary to include 2fa but will still work, I kept the option in bc the case may be that robinhood found security concerns with their 2fa and may fix it and change back to it, in which case it would be nice to still have some of the 2fa code, but this script really just needs username and pass even though 2fa is still an option). I also realize this is quite a bit longer than previous versions but it has to check for all 3 verification methods and added retries to the logic with various events and more output logs for different scenarios to better diagnose a failure. If this doesnt work for you then I would check login method thoroughly (I say this because I literally cant improve this script any further, if someone else can more power to ya, if this works for everyone I think it should be merged for now)

import getpass
import os
import pickle
import secrets
import time
from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *

def generate_device_token():
    """Generates a cryptographically secure device token."""
    rands = [secrets.randbelow(256) for _ in range(16)]
    hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)]
    token = ""
    for i, r in enumerate(rands):
        token += hexa[r]
        if i in [3, 5, 7, 9]:
            token += "-"
    return token


def _get_sherrif_id(data):
    """Extracts the sheriff verification ID from the response."""
    if "id" in data:
        return data["id"]
    raise Exception("Error: No verification ID returned in user-machine response")


def _validate_sherrif_id(device_token: str, workflow_id: str):
    """Handles Robinhood's verification workflow, including email, SMS, and app-based approvals."""
    print("Starting verification process...")
    pathfinder_url = "https://api.robinhood.com/pathfinder/user_machine/"
    machine_payload = {'device_id': device_token, 'flow': 'suv', 'input': {'workflow_id': workflow_id}}
    machine_data = request_post(url=pathfinder_url, payload=machine_payload, json=True)

    machine_id = _get_sherrif_id(machine_data)
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"

    start_time = time.time()
    
    while time.time() - start_time < 120:  # 2-minute timeout
        time.sleep(5)
        inquiries_response = request_get(inquiries_url)

        if not inquiries_response:  # Handle case where response is None
            print("Error: No response from Robinhood API. Retrying...")
            continue

        if "context" in inquiries_response and "sheriff_challenge" in inquiries_response["context"]:
            challenge = inquiries_response["context"]["sheriff_challenge"]
            challenge_type = challenge["type"]
            challenge_status = challenge["status"]
            challenge_id = challenge["id"]
            if challenge_type == "prompt":
                print("Check robinhood app for device approvals method...")
                prompt_url = f"https://api.robinhood.com/push/{challenge_id}/get_prompts_status/"
                while True:
                    time.sleep(5)
                    prompt_challenge_status = request_get(url=prompt_url)
                    if prompt_challenge_status["challenge_status"] == "validated":
                        break
                break

            if challenge_status == "validated":
                print("Verification successful!")
                break  # Stop polling once verification is complete

            if challenge_type in ["sms", "email"] and challenge_status == "issued":
                user_code = input(f"Enter the {challenge_type} verification code sent to your device: ")
                challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/"
                challenge_payload = {"response": user_code}
                challenge_response = request_post(url=challenge_url, payload=challenge_payload)

                if challenge_response.get("status") == "validated":
                    break

    # **Now poll the workflow status to confirm final approval**
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    
    retry_attempts = 5  # Allow up to 5 retries in case of 500 errors
    while time.time() - start_time < 120:  # 2-minute timeout 
        try:
            inquiries_payload = {"sequence": 0, "user_input": {"status": "continue"}}
            inquiries_response = request_post(url=inquiries_url, payload=inquiries_payload,json=True)
            if "type_context" in inquiries_response and inquiries_response["type_context"]["result"] == "workflow_status_approved":
                print("Verification successful!")
                return
            else:
                time.sleep(5)  # **Increase delay between requests to prevent rate limits**
        except requests.exceptions.RequestException as e:
            time.sleep(5)
            print(f"API request failed: {e}")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            print("Retrying workflow status check...")
            continue

        if not inquiries_response:  # Handle None response
            time.sleep(5)
            print("Error: No response from Robinhood API. Retrying...")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            continue

        workflow_status = inquiries_response.get("verification_workflow", {}).get("workflow_status")

        if workflow_status == "workflow_status_approved":
            print("Workflow status approved! Proceeding with login...")
            return
        elif workflow_status == "workflow_status_internal_pending":
            print("Still waiting for Robinhood to finalize login approval...")
        else:
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")

    raise TimeoutError("Timeout reached. Assuming login is approved and proceeding.")



def login(username=None, password=None, expiresIn=86400, scope='internal', store_session=True, mfa_code=None, pickle_path="", pickle_name=""):
    """Handles the login process to Robinhood, including multi-factor authentication, session persistence, and verification handling."""
    print("Starting login process...")
    device_token = generate_device_token()
    home_dir = os.path.expanduser("~")
    data_dir = os.path.join(home_dir, ".tokens")

    if pickle_path:
        if not os.path.isabs(pickle_path):
            pickle_path = os.path.normpath(os.path.join(os.getcwd(), pickle_path))
        data_dir = pickle_path

    if not os.path.exists(data_dir):
        os.makedirs(data_dir)

    creds_file = "robinhood" + pickle_name + ".pickle"
    pickle_path = os.path.join(data_dir, creds_file)

    url = login_url()
    login_payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'device_token': device_token,
        'try_passkeys': False,
        'token_request_path': '/login',
        'create_read_only_secondary_token': True,
    }

    if mfa_code:
        login_payload['mfa_code'] = mfa_code
    # If authentication has been stored in pickle file then load it. Stops login server from being pinged so much.
    if os.path.isfile(pickle_path):
        # **Load cached authentication session if available**
        if store_session:
            try:
                with open(pickle_path, 'rb') as f:
                    pickle_data = pickle.load(f)
                    access_token = pickle_data['access_token']
                    token_type = pickle_data['token_type']
                    refresh_token = pickle_data['refresh_token']
                    pickle_device_token = pickle_data['device_token']
                    login_payload['device_token'] = pickle_device_token
                    set_login_state(True)
                    update_session(
                            'Authorization', '{0} {1}'.format(token_type, access_token))
                    # Try to load account profile to check that authorization token is still valid.
                    res = request_get(
                        positions_url(), 'pagination', {'nonzero': 'true'}, jsonify_data=False)
                    # Raises exception if response code is not 200.
                    res.raise_for_status()
                    return({'access_token': access_token, 'token_type': token_type,
                            'expires_in': expiresIn, 'scope': scope, 
                            'detail': 'logged in using authentication in {0}'.format(creds_file),
                            'backup_code': None, 'refresh_token': refresh_token})
            except Exception:
                    print(
                        "ERROR: There was an issue loading pickle file. Authentication may be expired - logging in normally.", file=get_output())
                    set_login_state(False)
                    update_session('Authorization', None)
        else:
            os.remove(pickle_path)

    # **Attempt to login normally**
    if not username:
        username = input("Robinhood username: ")
        login_payload['username'] = username
    if not password:
        password = getpass.getpass("Robinhood password: ")
        login_payload['password'] = password

    data = request_post(url, login_payload)

    if data:
        try:
            if 'verification_workflow' in data:
                print("Verification required, handling challenge...")
                workflow_id = data['verification_workflow']['id']
                _validate_sherrif_id(device_token, workflow_id)

                # Reattempt login after verification
                data = request_post(url, login_payload)

            if 'access_token' in data:
                token = '{0} {1}'.format(data['token_type'], data['access_token'])
                update_session('Authorization', token)
                set_login_state(True)

            if store_session:
                with open(pickle_path, 'wb') as f:
                    pickle.dump({'token_type': data['token_type'],
                                 'access_token': data['access_token'],
                                 'refresh_token': data['refresh_token'],
                                 'device_token': login_payload['device_token']}, f)
                return data
        except Exception as e:
            print(f"Error during login verification: {e}")

    print("Login failed. Check credentials and try again.")
    return None


@login_required
def logout():
    """Logs out from Robinhood by clearing session data."""
    set_login_state(False)
    update_session('Authorization', None)
    print("Logged out successfully.")

Amazing Job, mate!
Just tried and it works like a charm.

Did you submit a pull request for this fix, by chance?

@Gates8911
Copy link

Gates8911 commented Feb 15, 2025 via email

@foragerr
Copy link

Posted @Gates8911 change as a PR here: #549

@txavier
Copy link
Contributor

txavier commented Feb 18, 2025

Has anyone tried the API key method to see if it will help with not having to authenticate every time our bots run?

https://docs.robinhood.com/crypto/trading/

This is the API for Robinhood crypto trading. But I'm hoping the calls to the stock side will also work with the API key provided for this. Just haven't had any time to experiment with it. Wondering if anyone else tried this already. My apologies if this was already brought up and tried.

@dheerajnagpal
Copy link

Started getting this error since yesterday. No notification on phone for this.

@RobertAgee
Copy link

@doctorcolossus @Kr1msonReaper @nickreynolds84 @mobeston @Adelantado @mike-labadessa @henryzhangpku @alex-l-zhou @ravi-bharadwaj @pielshawn @zabidin901 @HMSS013 @jmfernandes @cpasean @nat2k5us @schnup Since I see theres still a few issues people are having ill post my latest one that has had zero issues now across 3 different accounts, one using device approvals, another using email, and another using sms, (this code can be used with 3rd party 2fa but since robinhoods api doesnt support 2fa with the last update it is not necessary to include 2fa but will still work, I kept the option in bc the case may be that robinhood found security concerns with their 2fa and may fix it and change back to it, in which case it would be nice to still have some of the 2fa code, but this script really just needs username and pass even though 2fa is still an option). I also realize this is quite a bit longer than previous versions but it has to check for all 3 verification methods and added retries to the logic with various events and more output logs for different scenarios to better diagnose a failure. If this doesnt work for you then I would check login method thoroughly (I say this because I literally cant improve this script any further, if someone else can more power to ya, if this works for everyone I think it should be merged for now)

import getpass
import os
import pickle
import secrets
import time
from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *

def generate_device_token():
    """Generates a cryptographically secure device token."""
    rands = [secrets.randbelow(256) for _ in range(16)]
    hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)]
    token = ""
    for i, r in enumerate(rands):
        token += hexa[r]
        if i in [3, 5, 7, 9]:
            token += "-"
    return token


def _get_sherrif_id(data):
    """Extracts the sheriff verification ID from the response."""
    if "id" in data:
        return data["id"]
    raise Exception("Error: No verification ID returned in user-machine response")


def _validate_sherrif_id(device_token: str, workflow_id: str):
    """Handles Robinhood's verification workflow, including email, SMS, and app-based approvals."""
    print("Starting verification process...")
    pathfinder_url = "https://api.robinhood.com/pathfinder/user_machine/"
    machine_payload = {'device_id': device_token, 'flow': 'suv', 'input': {'workflow_id': workflow_id}}
    machine_data = request_post(url=pathfinder_url, payload=machine_payload, json=True)

    machine_id = _get_sherrif_id(machine_data)
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"

    start_time = time.time()
    
    while time.time() - start_time < 120:  # 2-minute timeout
        time.sleep(5)
        inquiries_response = request_get(inquiries_url)

        if not inquiries_response:  # Handle case where response is None
            print("Error: No response from Robinhood API. Retrying...")
            continue

        if "context" in inquiries_response and "sheriff_challenge" in inquiries_response["context"]:
            challenge = inquiries_response["context"]["sheriff_challenge"]
            challenge_type = challenge["type"]
            challenge_status = challenge["status"]
            challenge_id = challenge["id"]
            if challenge_type == "prompt":
                print("Check robinhood app for device approvals method...")
                prompt_url = f"https://api.robinhood.com/push/{challenge_id}/get_prompts_status/"
                while True:
                    time.sleep(5)
                    prompt_challenge_status = request_get(url=prompt_url)
                    if prompt_challenge_status["challenge_status"] == "validated":
                        break
                break

            if challenge_status == "validated":
                print("Verification successful!")
                break  # Stop polling once verification is complete

            if challenge_type in ["sms", "email"] and challenge_status == "issued":
                user_code = input(f"Enter the {challenge_type} verification code sent to your device: ")
                challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/"
                challenge_payload = {"response": user_code}
                challenge_response = request_post(url=challenge_url, payload=challenge_payload)

                if challenge_response.get("status") == "validated":
                    break

    # **Now poll the workflow status to confirm final approval**
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    
    retry_attempts = 5  # Allow up to 5 retries in case of 500 errors
    while time.time() - start_time < 120:  # 2-minute timeout 
        try:
            inquiries_payload = {"sequence": 0, "user_input": {"status": "continue"}}
            inquiries_response = request_post(url=inquiries_url, payload=inquiries_payload,json=True)
            if "type_context" in inquiries_response and inquiries_response["type_context"]["result"] == "workflow_status_approved":
                print("Verification successful!")
                return
            else:
                time.sleep(5)  # **Increase delay between requests to prevent rate limits**
        except requests.exceptions.RequestException as e:
            time.sleep(5)
            print(f"API request failed: {e}")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            print("Retrying workflow status check...")
            continue

        if not inquiries_response:  # Handle None response
            time.sleep(5)
            print("Error: No response from Robinhood API. Retrying...")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            continue

        workflow_status = inquiries_response.get("verification_workflow", {}).get("workflow_status")

        if workflow_status == "workflow_status_approved":
            print("Workflow status approved! Proceeding with login...")
            return
        elif workflow_status == "workflow_status_internal_pending":
            print("Still waiting for Robinhood to finalize login approval...")
        else:
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")

    raise TimeoutError("Timeout reached. Assuming login is approved and proceeding.")



def login(username=None, password=None, expiresIn=86400, scope='internal', store_session=True, mfa_code=None, pickle_path="", pickle_name=""):
    """Handles the login process to Robinhood, including multi-factor authentication, session persistence, and verification handling."""
    print("Starting login process...")
    device_token = generate_device_token()
    home_dir = os.path.expanduser("~")
    data_dir = os.path.join(home_dir, ".tokens")

    if pickle_path:
        if not os.path.isabs(pickle_path):
            pickle_path = os.path.normpath(os.path.join(os.getcwd(), pickle_path))
        data_dir = pickle_path

    if not os.path.exists(data_dir):
        os.makedirs(data_dir)

    creds_file = "robinhood" + pickle_name + ".pickle"
    pickle_path = os.path.join(data_dir, creds_file)

    url = login_url()
    login_payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'device_token': device_token,
        'try_passkeys': False,
        'token_request_path': '/login',
        'create_read_only_secondary_token': True,
    }

    if mfa_code:
        login_payload['mfa_code'] = mfa_code
    # If authentication has been stored in pickle file then load it. Stops login server from being pinged so much.
    if os.path.isfile(pickle_path):
        # **Load cached authentication session if available**
        if store_session:
            try:
                with open(pickle_path, 'rb') as f:
                    pickle_data = pickle.load(f)
                    access_token = pickle_data['access_token']
                    token_type = pickle_data['token_type']
                    refresh_token = pickle_data['refresh_token']
                    pickle_device_token = pickle_data['device_token']
                    login_payload['device_token'] = pickle_device_token
                    set_login_state(True)
                    update_session(
                            'Authorization', '{0} {1}'.format(token_type, access_token))
                    # Try to load account profile to check that authorization token is still valid.
                    res = request_get(
                        positions_url(), 'pagination', {'nonzero': 'true'}, jsonify_data=False)
                    # Raises exception if response code is not 200.
                    res.raise_for_status()
                    return({'access_token': access_token, 'token_type': token_type,
                            'expires_in': expiresIn, 'scope': scope, 
                            'detail': 'logged in using authentication in {0}'.format(creds_file),
                            'backup_code': None, 'refresh_token': refresh_token})
            except Exception:
                    print(
                        "ERROR: There was an issue loading pickle file. Authentication may be expired - logging in normally.", file=get_output())
                    set_login_state(False)
                    update_session('Authorization', None)
        else:
            os.remove(pickle_path)

    # **Attempt to login normally**
    if not username:
        username = input("Robinhood username: ")
        login_payload['username'] = username
    if not password:
        password = getpass.getpass("Robinhood password: ")
        login_payload['password'] = password

    data = request_post(url, login_payload)

    if data:
        try:
            if 'verification_workflow' in data:
                print("Verification required, handling challenge...")
                workflow_id = data['verification_workflow']['id']
                _validate_sherrif_id(device_token, workflow_id)

                # Reattempt login after verification
                data = request_post(url, login_payload)

            if 'access_token' in data:
                token = '{0} {1}'.format(data['token_type'], data['access_token'])
                update_session('Authorization', token)
                set_login_state(True)

            if store_session:
                with open(pickle_path, 'wb') as f:
                    pickle.dump({'token_type': data['token_type'],
                                 'access_token': data['access_token'],
                                 'refresh_token': data['refresh_token'],
                                 'device_token': login_payload['device_token']}, f)
                return data
        except Exception as e:
            print(f"Error during login verification: {e}")

    print("Login failed. Check credentials and try again.")
    return None


@login_required
def logout():
    """Logs out from Robinhood by clearing session data."""
    set_login_state(False)
    update_session('Authorization', None)
    print("Logged out successfully.")

Amazing Job, mate! Just tried and it works like a charm.

Did you submit a pull request for this fix, by chance?

No longer works.

Starting login process...
Error in request_post: Unauthorized so setting login state to false
Login failed. Check credentials and try again.
None

@yunweizhao26
Copy link

yunweizhao26 commented Feb 25, 2025

Updated.


@Gates8911 Thank you for sharing the code. This is very insightful!

I briefly tested today and I found one tiny issue that stop robinhood client from sending email as expected. The issue is in login_payload. I need to include 'challenge_type' entry in order for the client to trigger the email. The challenge_type is a str that could be anything text since it works even if I set it to be 'sms'. Not sure why but this is how it solves the problem for me:

    login_payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'device_token': device_token,
        'try_passkeys': False,
        'token_request_path': '/login',
        'create_read_only_secondary_token': True,
        'challenge_type': challenge_type,
    }

A limitation of the latest implementation is that it still requires user to manually enter the email code in the terminal after a period of time which is not always possible and sometimes inconvenient. To bypass this limitation, I improved the script to automatically login your email (im using gmail but you can check the code to adapt to your email provider, and you would need your environment to have those variables, for instance, gmail address and gmail app password), find the email with verification code and parse it, and automatically respond to the robin's client to login. I also want other people in the community to further test it out (e.g., testing with save_session=false mode). Sometimes, it is possible that it might not read the latest authentication code.

import os
import pickle
import secrets
import time
import imaplib
import email
import re
from bs4 import BeautifulSoup
from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *

def generate_device_token():
    """Generates a cryptographically secure device token."""
    rands = [secrets.randbelow(256) for _ in range(16)]
    hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)]
    token = ""
    for i, r in enumerate(rands):
        token += hexa[r]
        if i in [3, 5, 7, 9]:
            token += "-"
    return token


def _get_sherrif_id(data):
    """Extracts the sheriff verification ID from the response."""
    if "id" in data:
        return data["id"]
    raise Exception("Error: No verification ID returned in user-machine response")

def get_verification_code(email_address, email_password):
    """Fetch the LATEST Robinhood verification code from email inbox."""
    mail = imaplib.IMAP4_SSL("imap.gmail.com")
    mail.login(email_address, email_password)
    mail.select("inbox")

    code = None
    try:
        status, messages = mail.search(None, '(FROM "[email protected]")')
        message_ids = messages[0].split()
        
        if message_ids:
            latest_email_id = message_ids[-1]
            _, msg_data = mail.fetch(latest_email_id, "(RFC822)")
            msg = email.message_from_bytes(msg_data[0][1])

            html_body = ""
            if msg.is_multipart():
                for part in msg.walk():
                    if part.get_content_type() == "text/html":
                        html_body = part.get_payload(decode=True).decode(errors="ignore")
                        break
            else:
                html_body = msg.get_payload(decode=True).decode(errors="ignore")

            soup = BeautifulSoup(html_body, "html.parser")
            full_text = soup.get_text(separator=" ", strip=True)
            
            code_match = re.search(
                r"Please enter this verification code.*?(\d{6})", 
                full_text, 
                re.IGNORECASE | re.DOTALL
            )
            
            if code_match:
                code = code_match.group(1)
                mail.store(latest_email_id, '+FLAGS', '\\Seen')
    finally:
        mail.close()
        mail.logout()
    
    return code

def _validate_sherrif_id(device_token: str, workflow_id: str):
    pathfinder_url = "https://api.robinhood.com/pathfinder/user_machine/"
    machine_payload = {'device_id': device_token, 'flow': 'suv', 'input': {'workflow_id': workflow_id}}
    machine_data = request_post(url=pathfinder_url, payload=machine_payload, json=True)

    machine_id = _get_sherrif_id(machine_data)
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"

    start_time = time.time()
    challenge_resolved = False
    
    while time.time() - start_time < 120 and not challenge_resolved:
        time.sleep(5)
        inquiries_response = request_get(inquiries_url)

        if inquiries_response and "context" in inquiries_response:
            challenge = inquiries_response["context"].get("sheriff_challenge", {})
            challenge_type = challenge.get("type")
            challenge_status = challenge.get("status")
            challenge_id = challenge.get("id")
            
            print(f"Challenge type: {challenge}, status: {challenge_status}")

            if challenge_type == "prompt":
                print("Check robinhood app for device approvals method...")
                prompt_url = f"https://api.robinhood.com/push/{challenge_id}/get_prompts_status/"
                while True:
                    time.sleep(5)
                    prompt_challenge_status = request_get(url=prompt_url)
                    print(f"Prompt status: {prompt_challenge_status}")
                    if prompt_challenge_status["challenge_status"] == "validated":
                        break
                break

            if challenge_status == "validated":
                print("Verification successful!")
                break  # Stop polling once verification is complete

            print("Starting automated verification...")
            if challenge_type in ["sms", "email"] and challenge_status == "issued":
                print(f"Retrieving {challenge_type} verification code...")
                code = get_verification_code(
                    os.environ["GMAIL_USER"], 
                    os.environ["GMAIL_APP_PASSWORD"]
                )
                
                if code:
                    print(f"Submitting verification code: {code}")
                    challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/"
                    challenge_response = request_post(
                        challenge_url, 
                        {"response": code}
                    )
                    
                    if challenge_response.get("status") == "validated":
                        print("Code validation successful!")
                        challenge_resolved = True
                        break

    # Final workflow approval
    retry_count = 0
    while time.time() - start_time < 120 and retry_count < 5:
        try:
            inquiries_payload = {"sequence": 0, "user_input": {"status": "continue"}}
            inquiries_response = request_post(inquiries_url, inquiries_payload, json=True)
            
            if inquiries_response.get("type_context", {}).get("result") == "workflow_status_approved":
                print("Workflow fully approved!")
                return
                
        except Exception as e:
            print(f"Approval check failed: {str(e)}")
            retry_count += 1
            time.sleep(5)

    raise Exception("Verification workflow timeout")



def login(username=None, password=None, expiresIn=689285, scope='internal', store_session=True, mfa_code=None, by_sms=True, pickle_path="", pickle_name=""):
    """Handles the login process to Robinhood, including multi-factor authentication, session persistence, and verification handling."""
    print("Starting login process...")
    device_token = generate_device_token()
    home_dir = os.path.expanduser("~")
    data_dir = os.path.join(home_dir, ".tokens")

    if pickle_path:
        if not os.path.isabs(pickle_path):
            pickle_path = os.path.normpath(os.path.join(os.getcwd(), pickle_path))
        data_dir = pickle_path

    if not os.path.exists(data_dir):
        os.makedirs(data_dir)

    creds_file = "robinhood" + pickle_name + ".pickle"
    pickle_path = os.path.join(data_dir, creds_file)
    challenge_type = "sms" if by_sms else "email"
    url = login_url()
    login_payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'device_token': device_token,
        'try_passkeys': False,
        'token_request_path': '/login',
        'create_read_only_secondary_token': True,
        'challenge_type': challenge_type,
    }

    if mfa_code:
        login_payload['mfa_code'] = mfa_code
    # If authentication has been stored in pickle file then load it. Stops login server from being pinged so much.
    if os.path.isfile(pickle_path):
        # **Load cached authentication session if available**
        if store_session:
            try:
                with open(pickle_path, 'rb') as f:
                    pickle_data = pickle.load(f)
                    access_token = pickle_data['access_token']
                    token_type = pickle_data['token_type']
                    refresh_token = pickle_data['refresh_token']
                    pickle_device_token = pickle_data['device_token']
                    login_payload['device_token'] = pickle_device_token
                    set_login_state(True)
                    update_session(
                            'Authorization', '{0} {1}'.format(token_type, access_token))
                    # Try to load account profile to check that authorization token is still valid.
                    res = request_get(
                        positions_url(), 'pagination', {'nonzero': 'true'}, jsonify_data=False)
                    # Raises exception if response code is not 200.
                    res.raise_for_status()
                    return({'access_token': access_token, 'token_type': token_type,
                            'expires_in': expiresIn, 'scope': scope, 
                            'detail': 'logged in using authentication in {0}'.format(creds_file),
                            'backup_code': None, 'refresh_token': refresh_token})
            except Exception:
                    print(
                        "ERROR: There was an issue loading pickle file. Authentication may be expired - logging in normally.", file=get_output())
                    set_login_state(False)
                    update_session('Authorization', None)
        else:
            os.remove(pickle_path)

    # **Attempt to login normally**
    if not username:
        username = input("Robinhood username: ")
        login_payload['username'] = username
    if not password:
        password = getpass.getpass("Robinhood password: ")
        login_payload['password'] = password

    data = request_post(url, login_payload)

    if data:
        try:
            if 'verification_workflow' in data:
                print("Verification required, handling challenge...")
                workflow_id = data['verification_workflow']['id']
                _validate_sherrif_id(device_token, workflow_id)

                # Reattempt login after verification
                data = request_post(url, login_payload)

            if 'access_token' in data:
                token = '{0} {1}'.format(data['token_type'], data['access_token'])
                update_session('Authorization', token)
                set_login_state(True)

            if store_session:
                with open(pickle_path, 'wb') as f:
                    pickle.dump({'token_type': data['token_type'],
                                 'access_token': data['access_token'],
                                 'refresh_token': data['refresh_token'],
                                 'device_token': login_payload['device_token']}, f)
                return data
        except Exception as e:
            print(f"Error during login verification: {e}")

    print("Login failed. Check credentials and try again.")
    return None


@login_required
def logout():
    """Logs out from Robinhood by clearing session data."""
    set_login_state(False)
    update_session('Authorization', None)
    print("Logged out successfully.")

@mklosi
Copy link

mklosi commented Mar 5, 2025

hi all. read all the comments, but still not sure if there is a working solution. any solution... SMS, non-SMS, with authenticator, without, with robinhood app installed, should I delete it?... what should I do? thanks.

@mklosi
Copy link

mklosi commented Mar 6, 2025

hi all. read all the comments, but still not sure if there is a working solution. any solution... SMS, non-SMS, with authenticator, without, with robinhood app installed, should I delete it?... what should I do? thanks.

ok these changes actually solved it for me. Will use this for now, and wait patientlessly for official fix 👍

@OnishiKenshin
Copy link

OnishiKenshin commented Mar 6, 2025

Well, I couldn't believe it, but they must really hate us automating logins. They've removed SMS as a method to verify. Unreal. SMS was able to be automated, now I have no idea how to workaround this while they're only allowing device approvals.

EDIT: If you are logged out of Robinhood on your phone, leaving NO ability for the device approvals function to work, then the authentication.py script will default to SMS. So it still exists, but isn't a selectable option in the app.

@Gates8911
Copy link

Gates8911 commented Mar 6, 2025

I automated my main script by triggering a logout and re-login after 84000 seconds (which is before the shortest possible approved session expires which is 86400, typically robinhoods backend will give you a longer session but my current understanding is it varies even though the input is 86400 and under those circumstances in order to ensure a re-login is always triggered before session is we must assume and account for shortest session time which is why i had mine trigger at 84000). anyway this is my latest authentication script, keep in mind if you use mfa with this auth, robinhoods backend forces the in-app prompt verification, if you use sms, last I tried it worked as expected, same as email.

import getpass
import os
import pickle
import secrets
import time
from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *

def generate_device_token():
    """Generates a cryptographically secure device token."""
    rands = [secrets.randbelow(256) for _ in range(16)]
    hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)]
    token = ""
    for i, r in enumerate(rands):
        token += hexa[r]
        if i in [3, 5, 7, 9]:
            token += "-"
    return token


def _get_sherrif_id(data):
    """Extracts the sheriff verification ID from the response."""
    if "id" in data:
        return data["id"]
    raise Exception("Error: No verification ID returned in user-machine response")


def _validate_sherrif_id(device_token: str, workflow_id: str):
    """Handles Robinhood's verification workflow, including email, SMS, and app-based approvals."""
    print("Starting verification process...")
    pathfinder_url = "https://api.robinhood.com/pathfinder/user_machine/"
    machine_payload = {'device_id': device_token, 'flow': 'suv', 'input': {'workflow_id': workflow_id}}
    machine_data = request_post(url=pathfinder_url, payload=machine_payload, json=True)
    machine_id = _get_sherrif_id(machine_data)
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    inquiries_response = request_get(inquiries_url)
    retries = 5

    if not inquiries_response:  # Handle case where response is None
        while True:
            print("Error: No response from Robinhood API. Retrying...")
            time.sleep(4)
            inquiries_response = request_get(inquiries_url)
            if inquiries_response:
                break
            retries -= 1
            if retries == 0:
                raise ConnectionError("Check internet connection, robinhood API not responding...")

    elif "context" in inquiries_response:
        challenge = inquiries_response["context"]["sheriff_challenge"]
        challenge_type = challenge["type"]
        challenge_status = challenge["status"] or challenge["challenge_status"]
        challenge_id = challenge["id"]

        if "prompt" in challenge_type:
            start_time = time.time()
            prompt_url = f"https://api.robinhood.com/push/{challenge_id}/get_prompts_status/"
            print("Check robinhood app for device approval prompt...")
            while True:
                prompt_challenge_status = request_get(url=prompt_url)["challenge_status"]
                time.sleep(10)
                if prompt_challenge_status == "validated":
                    break
                elif time.time() - start_time > 120:
                    raise TimeoutError("Login verification timed out, retry login...") 

        elif challenge_type in ["sms", "email"]:
            start_time = time.time()
            while True: 
                user_code = input(f"Enter the {challenge_type} verification code sent to your device: ")
                challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/"
                challenge_payload = {"response": user_code}
                challenge_response = request_post(url=challenge_url, payload=challenge_payload, json=True)
                if challenge_response["status"] == "validated" or challenge_response["challenge_status"] == "validated":
                    break
                elif time.time() - start_time > 120:
                    raise TimeoutError("Login verification timed out, retry login...")            

    # **Now poll the workflow status to confirm final approval**
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    
    retry_attempts = 5  # Allow up to 5 retries in case of 500 errors
    while time.time() - start_time < 60:  # 1-minute timeout 
        try:
            inquiries_payload = {"sequence": 0, "user_input": {"status": "continue"}}
            inquiries_response = request_post(url=inquiries_url, payload=inquiries_payload, json=True)
            if "type_context" in inquiries_response and inquiries_response["type_context"]["result"] == "workflow_status_approved":
                print("Verification successful!")
                return
            else:
                time.sleep(5)  # **Increase delay between requests to prevent rate limits**
        except requests.exceptions.RequestException as e:
            time.sleep(5)
            print(f"API request failed: {e}")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            print("Retrying workflow status check...")
            continue

        if not inquiries_response:  # Handle None response
            time.sleep(5)
            print("Error: No response from Robinhood API. Retrying...")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            continue

        workflow_status = inquiries_response.get("verification_workflow", {}).get("workflow_status")

        if workflow_status == "workflow_status_approved":
            print("Workflow status approved! Proceeding with login...")
            return
        elif workflow_status == "workflow_status_internal_pending":
            print("Still waiting for Robinhood to finalize login approval...")
        else:
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")

    raise TimeoutError("Timeout reached. Assuming login is approved and proceeding.")



def login(username=None, password=None, expiresIn=86400, scope='internal', store_session=True, mfa_code=None, pickle_path="", pickle_name=""):
    """Handles the login process to Robinhood, including multi-factor authentication, session persistence, and verification handling."""
    print("Starting login process...")
    device_token = generate_device_token()
    home_dir = os.path.expanduser("~")
    data_dir = os.path.join(home_dir, ".tokens")

    if pickle_path:
        if not os.path.isabs(pickle_path):
            pickle_path = os.path.normpath(os.path.join(os.getcwd(), pickle_path))
        data_dir = pickle_path

    if not os.path.exists(data_dir):
        os.makedirs(data_dir)

    creds_file = "robinhood" + pickle_name + ".pickle"
    pickle_path = os.path.join(data_dir, creds_file)

    url = login_url()
    login_payload = {
    "device_token": device_token,
    "client_id": "c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS",
    "create_read_only_secondary_token": "true",
    "expires_in": expiresIn,
    "grant_type": "password",
    "scope": "internal",
    "token_request_path": "/login/",
    "username": username,
    "password": password,
}

    if mfa_code:
        login_payload['mfa_code'] = mfa_code
    # If authentication has been stored in pickle file then load it. Stops login server from being pinged so much.
    if os.path.isfile(pickle_path):
        # **Load cached authentication session if available**
        if store_session:
            try:
                with open(pickle_path, 'rb') as f:
                    pickle_data = pickle.load(f)
                    access_token = pickle_data['access_token']
                    token_type = pickle_data['token_type']
                    refresh_token = pickle_data['refresh_token']
                    pickle_device_token = pickle_data['device_token']
                    login_payload['device_token'] = pickle_device_token
                    set_login_state(True)
                    update_session(
                            'Authorization', '{0} {1}'.format(token_type, access_token))
                    # Try to load account profile to check that authorization token is still valid.
                    res = request_get(
                        positions_url(), 'pagination', {'nonzero': 'true'}, jsonify_data=False)
                    # Raises exception if response code is not 200.
                    res.raise_for_status()
                    return({'access_token': access_token, 'token_type': token_type,
                            'expires_in': expiresIn, 'scope': scope, 
                            'detail': 'logged in using authentication in {0}'.format(creds_file),
                            'backup_code': None, 'refresh_token': refresh_token})
            except Exception:
                    print(
                        "ERROR: There was an issue loading pickle file. Authentication may be expired - logging in normally.", file=get_output())
                    set_login_state(False)
                    update_session('Authorization', None)
        else:
            os.remove(pickle_path)

    # **Attempt to login normally**
    if not username:
        username = input("Robinhood username: ")
        login_payload['username'] = username
    if not password:
        password = getpass.getpass("Robinhood password: ")
        login_payload['password'] = password

    data = request_post(url, login_payload)

    if data:
        try:
            if 'verification_workflow' in data:
                print("Verification required, handling challenge...")
                workflow_id = data['verification_workflow']['id']
                _validate_sherrif_id(device_token, workflow_id)

                # Reattempt login after verification
                data = request_post(url, login_payload)

            if 'access_token' in data:
                token = '{0} {1}'.format(data['token_type'], data['access_token'])
                update_session('Authorization', token)
                set_login_state(True)

                if store_session:
                    with open(pickle_path, 'wb') as f:
                        pickle.dump({'token_type': data['token_type'],
                                    'access_token': data['access_token'],
                                    'refresh_token': data['refresh_token'],
                                    'device_token': login_payload['device_token']}, f)
                print("login successful!")
                return data
            else:
                raise AttributeError("Login failed. Check credentials and/or internet connection and try again...")
        except Exception as e:
            print(f"Error during login verification: {e}")


@login_required
def logout():
    """Logs out from Robinhood by clearing session data."""
    set_login_state(False)
    update_session('Authorization', None)
    print("Logged out successfully.")

@mike-labadessa
Copy link

I automated my main script by triggering a logout and re-login after 84000 seconds (which is before the shortest possible approved session expires which is 86400, typically robinhoods backend will give you a longer session but my current understanding is it varies even though the input is 86400 and under those circumstances in order to ensure a re-login is always triggered before session is we must assume and account for shortest session time which is why i had mine trigger at 84000). anyway this is my latest authentication script, keep in mind if you use mfa with this auth, robinhoods backend forces the in-app prompt verification, if you use sms, last I tried it worked as expected, same as email.

import getpass
import os
import pickle
import secrets
import time
from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *

def generate_device_token():
    """Generates a cryptographically secure device token."""
    rands = [secrets.randbelow(256) for _ in range(16)]
    hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)]
    token = ""
    for i, r in enumerate(rands):
        token += hexa[r]
        if i in [3, 5, 7, 9]:
            token += "-"
    return token


def _get_sherrif_id(data):
    """Extracts the sheriff verification ID from the response."""
    if "id" in data:
        return data["id"]
    raise Exception("Error: No verification ID returned in user-machine response")


def _validate_sherrif_id(device_token: str, workflow_id: str):
    """Handles Robinhood's verification workflow, including email, SMS, and app-based approvals."""
    print("Starting verification process...")
    pathfinder_url = "https://api.robinhood.com/pathfinder/user_machine/"
    machine_payload = {'device_id': device_token, 'flow': 'suv', 'input': {'workflow_id': workflow_id}}
    machine_data = request_post(url=pathfinder_url, payload=machine_payload, json=True)
    machine_id = _get_sherrif_id(machine_data)
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    inquiries_response = request_get(inquiries_url)
    retries = 5

    if not inquiries_response:  # Handle case where response is None
        while True:
            print("Error: No response from Robinhood API. Retrying...")
            time.sleep(4)
            inquiries_response = request_get(inquiries_url)
            if inquiries_response:
                break
            retries -= 1
            if retries == 0:
                raise ConnectionError("Check internet connection, robinhood API not responding...")

    elif "context" in inquiries_response:
        challenge = inquiries_response["context"]["sheriff_challenge"]
        challenge_type = challenge["type"]
        challenge_status = challenge["status"] or challenge["challenge_status"]
        challenge_id = challenge["id"]

        if "prompt" in challenge_type:
            start_time = time.time()
            prompt_url = f"https://api.robinhood.com/push/{challenge_id}/get_prompts_status/"
            print("Check robinhood app for device approval prompt...")
            while True:
                prompt_challenge_status = request_get(url=prompt_url)["challenge_status"]
                time.sleep(10)
                if prompt_challenge_status == "validated":
                    break
                elif time.time() - start_time > 120:
                    raise TimeoutError("Login verification timed out, retry login...") 

        elif challenge_type in ["sms", "email"]:
            start_time = time.time()
            while True: 
                user_code = input(f"Enter the {challenge_type} verification code sent to your device: ")
                challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/"
                challenge_payload = {"response": user_code}
                challenge_response = request_post(url=challenge_url, payload=challenge_payload, json=True)
                if challenge_response["status"] == "validated" or challenge_response["challenge_status"] == "validated":
                    break
                elif time.time() - start_time > 120:
                    raise TimeoutError("Login verification timed out, retry login...")            

    # **Now poll the workflow status to confirm final approval**
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    
    retry_attempts = 5  # Allow up to 5 retries in case of 500 errors
    while time.time() - start_time < 60:  # 1-minute timeout 
        try:
            inquiries_payload = {"sequence": 0, "user_input": {"status": "continue"}}
            inquiries_response = request_post(url=inquiries_url, payload=inquiries_payload, json=True)
            if "type_context" in inquiries_response and inquiries_response["type_context"]["result"] == "workflow_status_approved":
                print("Verification successful!")
                return
            else:
                time.sleep(5)  # **Increase delay between requests to prevent rate limits**
        except requests.exceptions.RequestException as e:
            time.sleep(5)
            print(f"API request failed: {e}")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            print("Retrying workflow status check...")
            continue

        if not inquiries_response:  # Handle None response
            time.sleep(5)
            print("Error: No response from Robinhood API. Retrying...")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            continue

        workflow_status = inquiries_response.get("verification_workflow", {}).get("workflow_status")

        if workflow_status == "workflow_status_approved":
            print("Workflow status approved! Proceeding with login...")
            return
        elif workflow_status == "workflow_status_internal_pending":
            print("Still waiting for Robinhood to finalize login approval...")
        else:
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")

    raise TimeoutError("Timeout reached. Assuming login is approved and proceeding.")



def login(username=None, password=None, expiresIn=86400, scope='internal', store_session=True, mfa_code=None, pickle_path="", pickle_name=""):
    """Handles the login process to Robinhood, including multi-factor authentication, session persistence, and verification handling."""
    print("Starting login process...")
    device_token = generate_device_token()
    home_dir = os.path.expanduser("~")
    data_dir = os.path.join(home_dir, ".tokens")

    if pickle_path:
        if not os.path.isabs(pickle_path):
            pickle_path = os.path.normpath(os.path.join(os.getcwd(), pickle_path))
        data_dir = pickle_path

    if not os.path.exists(data_dir):
        os.makedirs(data_dir)

    creds_file = "robinhood" + pickle_name + ".pickle"
    pickle_path = os.path.join(data_dir, creds_file)

    url = login_url()
    login_payload = {
    "device_token": device_token,
    "client_id": "c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS",
    "create_read_only_secondary_token": "true",
    "expires_in": expiresIn,
    "grant_type": "password",
    "scope": "internal",
    "token_request_path": "/login/",
    "username": username,
    "password": password,
}

    if mfa_code:
        login_payload['mfa_code'] = mfa_code
    # If authentication has been stored in pickle file then load it. Stops login server from being pinged so much.
    if os.path.isfile(pickle_path):
        # **Load cached authentication session if available**
        if store_session:
            try:
                with open(pickle_path, 'rb') as f:
                    pickle_data = pickle.load(f)
                    access_token = pickle_data['access_token']
                    token_type = pickle_data['token_type']
                    refresh_token = pickle_data['refresh_token']
                    pickle_device_token = pickle_data['device_token']
                    login_payload['device_token'] = pickle_device_token
                    set_login_state(True)
                    update_session(
                            'Authorization', '{0} {1}'.format(token_type, access_token))
                    # Try to load account profile to check that authorization token is still valid.
                    res = request_get(
                        positions_url(), 'pagination', {'nonzero': 'true'}, jsonify_data=False)
                    # Raises exception if response code is not 200.
                    res.raise_for_status()
                    return({'access_token': access_token, 'token_type': token_type,
                            'expires_in': expiresIn, 'scope': scope, 
                            'detail': 'logged in using authentication in {0}'.format(creds_file),
                            'backup_code': None, 'refresh_token': refresh_token})
            except Exception:
                    print(
                        "ERROR: There was an issue loading pickle file. Authentication may be expired - logging in normally.", file=get_output())
                    set_login_state(False)
                    update_session('Authorization', None)
        else:
            os.remove(pickle_path)

    # **Attempt to login normally**
    if not username:
        username = input("Robinhood username: ")
        login_payload['username'] = username
    if not password:
        password = getpass.getpass("Robinhood password: ")
        login_payload['password'] = password

    data = request_post(url, login_payload)

    if data:
        try:
            if 'verification_workflow' in data:
                print("Verification required, handling challenge...")
                workflow_id = data['verification_workflow']['id']
                _validate_sherrif_id(device_token, workflow_id)

                # Reattempt login after verification
                data = request_post(url, login_payload)

            if 'access_token' in data:
                token = '{0} {1}'.format(data['token_type'], data['access_token'])
                update_session('Authorization', token)
                set_login_state(True)

                if store_session:
                    with open(pickle_path, 'wb') as f:
                        pickle.dump({'token_type': data['token_type'],
                                    'access_token': data['access_token'],
                                    'refresh_token': data['refresh_token'],
                                    'device_token': login_payload['device_token']}, f)
                print("login successful!")
                return data
            else:
                raise AttributeError("Login failed. Check credentials and/or internet connection and try again...")
        except Exception as e:
            print(f"Error during login verification: {e}")


@login_required
def logout():
    """Logs out from Robinhood by clearing session data."""
    set_login_state(False)
    update_session('Authorization', None)
    print("Logged out successfully.")

Yo Gates, what’s up

@jso1
Copy link

jso1 commented Mar 7, 2025

Is there any way to use/force sms to login? I don't get device approvals and some of the scripts above only send me an sms after I manually exit the authentication process with control c.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests