Closed hz2018tv closed 5 years ago
I'm not having any issues with that.
Just reinstall the latest repo, same problem. not sure if it is account-related? do we need any new user settings, like new user agreements? Thanks
I use my own repo and it's working fine. My login function is identical to the parent repo, though, so it shouldn't make any difference. Do you mind pasting the code you're running to test it?
I use the example test app from repo and have the same problem.
from Robinhood import Robinhood
my_trader = Robinhood()
my_trader.login(username="
then it failed with "400" bad request with my real account login and a fake one. So, I think it is not related to account, since the fake user/pass failed the same way.
I turned on debugging, see the logs below
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.robinhood.com send: 'POST /oauth2/token/ HTTP/1.1\r\nHost: api.robinhood.com\r\nAccept-Language: en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5\r\nAccept-Encoding: gzip, deflate\r\nX-Robinhood-API-Version: 1.0.0\r\nAccept: /\r\nUser-Agent: Robinhood/823 (iPhone; iOS 7.1.2; Scale/2.00)\r\nConnection: keep-alive\r\nContent-Type: application/x-www-form-urlencoded; charset=utf-8\r\nContent-Length: 100\r\n\r\nusername=user1&password=pass2&client_id=c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS&grant_type=password' reply: 'HTTP/1.1 400 Bad Request\r\n' header: Date: Fri, 26 Apr 2019 19:17:32 GMT header: Content-Type: application/json header: Content-Length: 56 header: Connection: keep-alive header: Server: nginx header: Allow: POST, OPTIONS header: X-Robinhood-API-Version: 0.0.0 header: Content-Security-Policy: default-src 'none' header: X-Frame-Options: SAMEORIGIN header: x-content-type-options: nosniff header: x-xss-protection: 1; mode=block DEBUG:urllib3.connectionpool:https://api.robinhood.com:443 "POST /oauth2/token/ HTTP/1.1" 400 56 <Response [400]>
It has been working for a long time, just stopped since last night. Did you guys use two-factor login? or just password?
I'll do more tests, but Robinhood just introduced a new version of Robinhood Gold. Sometimes when they introduce something new, you have to login to their app. Try doing that, see if there are any notifications for you, and try to login with code again.
already did this morning before posting. upgraded to the latest mobile app, logout, relogin, no notifications, (that is why I asked whether there are new user settings, new user agreements,etc.). same error with repo test app, my own app, my own login, fake login. ...
I'm having the same error. I've tried turning on and off the Web Beta setting, 2 factor auth. Logout/login after each change and still getting the same error when trying to auth via API. Tried in my app and postman (both were working few days ago), but having no luck now.
Same error too.
It's still working for me so I can't help much. Might want to go to https://robinhood.com/login and inspect the auth code. There's an invisibleRecaptchaSiteKey in the JavaScript. Maybe that's new and you're getting blocked out from that? Just a wild hunch.
I am not using your API I have my own written in Java but I use others API's as references. However like you guys I also started getting login errors.
The server responds with the following from the endpoint "https://api.robinhood.com/oauth2/token/":
{"detail":"This version of Robinhood is no longer supported. Please update your app or use Robinhood for Web to log in to your account."}
So we need to figure out how to get the api to think we are on a newer version.
Also make sure you guys are using HTTP/2 otherwise you will get "400 Bad Request" like someone mentioned earlier. Not sure what the Python equivalent is but if you switch to Java use the new HttpRequest which supports and by default uses HTTP/2.
Same issue here.
{"detail":"This version of Robinhood is no longer supported. Please update your app or use Robinhood for Web to log in to your account."}
And, it depends on account. In my case, this happens only on my main account.
I already have it working. I think pretty much everyone is using my code from reddit for the oauth2 system when they snuck that change in last year.
The same login system works, but we have to add some details.
The old dictionary post was this: {"username", username }, {"password", password }, {"grant_type", "password" }, {"client_id", "c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS" },
Add these variables now: {"expires_in", "86400" }, {"scope", "internal" }, {"device_token", !!!device_token!!! }, {"challenge_type", "sms" }
So for device token, you'll have 2 options. You can mine it from robinhood.com/login
ga('create', 'UA-46330882-9', 'robinhood.com', {
appName: 'Robinhood Web App',
appVersion: "0.6.291",
clientId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" <- this will be the one to mine
});
or alternatively, I wrote code mimicking exactly how they generate it. It's in c#, but you can port it to whatever pretty easily.
public static string GenerateDeviceToken()
{
List<int> rands = new List<int>();
var rng = new Random();
for (int i = 0; i < 16; i++)
{
var r = rng.NextDouble();
double rand = 4294967296.0 * r;
rands.Add(((int)((uint)rand >> ((3 & i) << 3))) & 255);
}
List<string> hex = new List<string>();
for (int i = 0; i < 256; ++i)
{
hex.Add(Convert.ToString(i + 256, 16).Substring(1));
}
string id = "";
for (int i = 0; i < 16; i++)
{
id += hex[rands[i]];
if (i == 3 || i == 5 || i == 7 || i == 9)
{
id += "-";
}
}
return id;
}
Make sure you save the device_id you generate unless you wanna get SMS challenged each time you log in. Only regenerate it if your auth_token you have saved stops being valid.
Now, to answer the SMS response/challenge you'll have to face now, which is basically just a forced 2 factor authentication you're gonna get this response:
public class Challenge
{
public string detail;
public ChallengeInfo challenge;
public class ChallengeInfo
{
public string id;
public string user;
public string type;
public string alternate_type;
public string status;
public int remaining_retries;
public int remaining_attempts;
public string expires_at;
}
}
If "status" == "issued", you're gonna have to save the "id" from the challenge object.
Then you'll send a response to https://api.robinhood.com/challenge/{id}/respond/, where {id} is the id you saved.
The response format you'll send them is just { "response" : number_you_received_on_sms }. It's just a 6 digit number.
They'll respond with:
public class ChallengeResponse
{
public string id;
public string user;
public string alternate_type;
public string status;
public int remaining_retries;
public int remaining_attempts;
public string expires_at;
}
If "status" == "validated", you're good to go. Make sure you saved the device_token you validated along with the auth_token so you can "hopefully" just keep refreshing it indefinitely. If you go a day without logging in and let the auth_token period lapse or refuse to save these values to use to log in on consecutive app launches, you WILL be forced to redo this two-auth system each time.
Just a minor annoyance, but we can all go back to trading now.
Enjoy!
@derekshreds I know I speak on everyone's behalf when I say thank you for this. How did you figure this out / where did you find this?
@aamazie I spent the morning reverse engineering the web app to figure it out. If you look at the source for robinhood.com/login they link to an App-xxxxxxxxx.js file. It's their web app, but also includes an API client, I just had to waste time undoing the obfuscation to figure out the login changes.
The device_token id generation is basically nonsense as far as I'm concerned, so I'm pretty sure this update was specifically aimed at stopping public API use.
For some reason I'm still able to log in, but this code should work for just scraping the device_id in Python:
from urllib.request import Request, urlopen
from bs4 import BeautifulSoup as soup
import re
req = Request("https://robinhood.com/login", headers={'User-Agent': 'Mozilla Chrome Safari'})
webpage = urlopen(req).read()
urlopen(req).close()
page_soup = soup(webpage, "lxml")
container = str(page_soup.findAll("script"))
device_token = re.search('clientId: "(.+?)"', container).group(1)
@hz2018tv , @derekshreds addressed that in the last paragraph.
As I've been saying, I can't test this since my log in function still works, but so far I've coded the beginning of what might work. I need to find a way to see if the device_token is still valid, and then how to interact with the SMS and what it sends back. Could somebody let me know what a response comes back as (I'm sure it's not in C#)? Or someone could take what I'm posting below and build the rest off of it.
self.device_token = ""
def GetDeviceToken():
req = Request("https://robinhood.com/login", headers={'User-Agent': 'Mozilla Chrome Safari'})
webpage = urlopen(req).read()
urlopen(req).close()
page_soup = soup(webpage, "lxml")
container = str(page_soup.findAll("script"))
self.device_token = re.search('clientId: "(.+?)"', container).group(1)
def login(self,
username,
password,
mfa_code=None):
"""Save and test login info for Robinhood accounts
Args:
username (str): username
password (str): password
Returns:
(bool): received valid auth token
"""
self.username = username
self.password = password
if self.device_token == "":
GetDeviceToken()
#(and/or) if self.device_token is invalid: (need to find a way to check for this)
# GetDeviceToken()
payload = {
'password': self.password,
'username': self.username,
'grant_type': 'password',
'client_id': self.client_id,
'expires_in': '86400',
'scope': 'internal',
'device_token': self.device_token,
'challenge_type': 'sms'
}
if mfa_code:
payload['mfa_code'] = mfa_code
try:
res = self.session.post(endpoints.login(), data=payload, timeout=15)
res.raise_for_status()
data = res.json()
except requests.exceptions.HTTPError:
raise RH_exception.LoginFailed()
if 'mfa_required' in data.keys(): # pragma: no cover
raise RH_exception.TwoFactorRequired() # requires a second call to enable 2FA
if 'access_token' in data.keys() and 'refresh_token' in data.keys():
self.auth_token = data['access_token']
self.refresh_token = data['refresh_token']
self.headers['Authorization'] = 'Bearer ' + self.auth_token
return True
return False
Also, I personally feel that mining for the device token is the best way to go about this particular instance. I've been against scraping in the API in the past because it's slow but I figure that it doesn't have to scrape very often if it's saved. Plus, they could change how they calculate/qualify the device token (in the JS it's called client_Id).
I assume,
@hz2018tv I didn't see that second question. That's a question for @derekshreds or someone else.
Your first assumption is right. And Derek coded that in C#.
Your second assumption I think is right.. I'm using urllib.request. It may only be good for Python 3.
And to your 3rd point in your 3-point question, it doesn't need an extra auth_code. It just needs "status" == "validated".
I know C# so I'll give that custom generation a go.
And to your 3rd point in your 3-point question, it doesn't need an extra auth_code. It just needs "status" == "validated".
All the steps are done to get the "access_token/auth_token", but I am not sure how/where to get it after the success challenge when status is changed from "issued" to "validated". you need this access_token/auth_token for renewal. just the status=="validated" will not be enough, I guess.
Ok, this should work for the custom device_token:
import random
def GenerateDeviceToken():
rands = []
for i in range(0,16):
r = random.random()
rand = 4294967296.0 * r
rands.append((int(rand) >> ((3 & i) << 3)) & 255)
hexa = []
for i in range(0,256):
hexa.append(str(hex(i+256)).lstrip("0x").rstrip("L")[1:])
id = ""
for i in range(0,16):
id += hexa[rands[i]]
if (i == 3) or (i == 5) or (i == 7) or (i == 9):
id += "-"
# self.device_token = id
print(id) #can get rid of this in API code
GenerateDeviceToken()
@derekshreds I have no idea how you reverse engineered that. You're sick.
@derekshreds , please help me understand the flow. step 1) post to "/oauth2/token" with device_id and challenge_type added. this will trigger sms/email verification and this op will be blocked till validated. step 2) wait till you get the 6-digit verification code, then post it to /challenge/id/respond/, then get the status changed from "issued" to "validated", step 3) now where/when/how to get access_token/auth_token? if you post to oauth2/token site again, it will challenge again I assume. Or step1 is waiting when blocked while doing step2 in a different thread in an async way? Thanks.
@aamazie Could you be more specific on how to login with only status=="validated" ? I tried following the steps above but still cannot login... basically same issue with @hz2018tv
Thanks guys I am able to login now. Still stuck at {"status":"validated"}
but thats ok it's something.
Here is the Java equivalent of @derekshreds GenerateDeviceToken()
method:
//Thanks
//https://github.com/Jamonek/Robinhood/issues/176#issuecomment-487310801
String generateDeviceToken() {
List<Integer> rands = new ArrayList<>();
var rng = new Random();
for (int i = 0; i < 16; i++) {
var r = rng.nextDouble();
double rand = 4294967296.0 * r;
var a = ((int) rand >> ((3 & i) << 3)) & 255;
rands.add(a);
}
List<String> hex = new ArrayList<>();
for (int i = 0; i < 256; ++i) {
var a = Integer.toHexString((i + 256)).substring(1);
hex.add(a);
}
String s = null;
for (int i = 0; i < 16; i++) {
s += hex.get(rands.get(i));
if (i == 3 || i == 5 || i == 7 || i == 9) {
s += "-";
}
}
return id;
}
I'm on it. Gimme a few hours to go through their source again. I was able to log in with my changes until this morning.
Alright, so after you get the "status" == "validated", save the challenge_id.
We're gonna repost to login the same way as before, but we're going to add a header:
{ "X-ROBINHOOD-CHALLENGE-RESPONSE-ID" : your_challenge_id }
After this, it'll give you the access_token as usual, and you'll be good to go.
Cheers!
Right on! Now it makes perfect sense. Thanks @derekshreds
how to renew the access_token?
{scope: r, grant_type: "refresh_token", client_id: const_client_id, refresh_token: saved_refresh_token} post the above, no complaints, but after that got "Unauthorized client". @derekshreds
Make sure you are also posting username / password
added "username" and "password", same error. Is "scope" still 'internal'? after a succes posting to /oauth2/token, response contains a long "access_token" and a shorter "refresh_token", I used the shorter one for renewal. is that right?
Anybody have this working? I'd like to see some code or a pull request. My login stopped working this morning. Going to give this a go myself now.
@aamazie I use curl with -H -d to test the login process, now stuck at token renewal. have not jumped into python yet, meanwhile lets see what else changes Robinhood is rolling out.
You guys know you can renew it also just by reposting the same stuff you did to get it, right? That's what I always do. You don't have to repost the challenge_id header after the first time though.
@derekshreds good to know. thx. will try when home. was trying to follow the renew() from the js, a little tough for me coz I dunno much about js.
Ok, so there has to be some leniency going on with their login code. I haven't gotten the old login to break on my local machine, but this morning I was running a test script on a virtual server and it was throwing the login errors after market open. Now it seems to be working again without any changes. I updated my API repo anyway but only up until the SMS challenge.. not sure where to code further with that yet. Not very familiar with cURL. Going to be looking into how to code the Python more unless someone gives me some code first. Either way, you can check my repo for some skeleton code as it stands. As it is, this iteration works for the time being but I'm sure it'll break once the market opens again tomorrow.
Has anybody solved this login issue for Python? If so, could you please share some sample code. I have had no luck so far.
I've been very busy. If someone can help build on the Python I've already written following Derek's code, that would be very appreciated. If not, I'll try to get to it in the next few days.
FYI...there is some code via https://github.com/westonplatter/fast_arrow/issues/85 that has been found to work. Not production pretty but functional. Also catches/parses email (from gmail) rather than manual SMS. When I get some time I'll try to pretty-fy it.
So I'm getting an email with a several minute delay sent but I'm getting a 400 return so I can't grab the challenge. I think it may be related to the HTTP/2 mentioned above. Any thoughts on what I'm doing wrong?
` import os import requests import random from fast_arrow.util import get_last_path from fast_arrow.resources.user import User from fast_arrow.resources.account import Account from fast_arrow.exceptions import AuthenticationError from fast_arrow.exceptions import NotImplementedError
CLIENT_ID = "c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS"
HTTP_ATTEMPTS_MAX = 2
class Client(object):
def __init__(self, **kwargs):
self.options = kwargs
self.account_id = None
self.account_url = None
self.access_token = None
self.refresh_token = None
self.mfa_code = None
self.scope = None
self.authenticated = False
self.device_token = None
self.certs = os.path.join(os.path.dirname(__file__), 'ssl_certs/certs.pem')
def authenticate(self):
'''
Authenticate using data in `options`
'''
if "username" in self.options and "password" in self.options:
self.login_oauth2(self.options["username"], self.options["password"], self.options.get('mfa_code'))
elif "access_token" in self.options and "refresh_token" in self.options:
self.access_token = self.options["access_token"]
self.refresh_token = self.options["refresh_token"]
self.__set_account_info()
else:
self.authenticated = False
return self.authenticated
def get(self, url=None, params=None, retry=True):
'''
Execute HTTP GET
'''
headers = self._gen_headers(self.access_token, url)
attempts = 1
while attempts <= HTTP_ATTEMPTS_MAX:
try:
res = requests.get(url, headers=headers, params=params, timeout=15, verify=self.certs)
res.raise_for_status()
return res.json()
except requests.exceptions.RequestException as e:
attempts += 1
if res.status_code in [400]:
raise e
elif retry and res.status_code in [403]:
self.relogin_oauth2()
def post(self, url=None, payload=None, retry=True):
'''
Execute HTTP POST
'''
headers = self._gen_headers(self.access_token, url)
attempts = 1
while attempts <= HTTP_ATTEMPTS_MAX:
try:
print(requests.post(url, headers=headers, data=payload, timeout=15, verify=self.certs))
res = requests.post(url, headers=headers, data=payload, timeout=15, verify=self.certs)
res.raise_for_status()
if res.headers['Content-Length'] == '0':
return None
else:
return res.json()
except requests.exceptions.RequestException as e:
attempts += 1
if res.status_code in [400]:
raise e
elif retry and res.status_code in [403]:
self.relogin_oauth2()
def _gen_headers(self, bearer, url):
'''
Generate headders, adding in Oauth2 bearer token if present
'''
headers = {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
}
if bearer:
headers["Authorization"] = "Bearer {0}".format(bearer)
if url == "https://api.robinhood.com/options/orders/":
headers["Content-Type"] = "application/json; charset=utf-8"
return headers
def GenerateDeviceToken(self):
rands = []
for i in range(0,16):
r = random.random()
rand = 4294967296.0 * r
rands.append((int(rand) >> ((3 & i) << 3)) & 255)
hexa = []
for i in range(0,256):
hexa.append(str(hex(i+256)).lstrip("0x").rstrip("L")[1:])
id = ""
for i in range(0,16):
id += hexa[rands[i]]
if (i == 3) or (i == 5) or (i == 7) or (i == 9):
id += "-"
return id
def login_oauth2(self, username, password, mfa_code=None):
'''
Login using username and password
'''
if self.device_token == None:
self.device_token = self.GenerateDeviceToken()
data = {
"grant_type": "password",
"scope": "internal",
"client_id": CLIENT_ID,
"expires_in": 86400,
"password": password,
"username": username,
"challenge_type": 'email',
"device_token": self.device_token
}
if mfa_code is not None:
data['mfa_code'] = mfa_code
url = "https://api.robinhood.com/oauth2/token/"
res = self.post(url, payload=data, retry=False)
if res is None:
if mfa_code is None:
msg = "Client.login_oauth2(). Could not authenticate. Check username and password."
raise AuthenticationError(msg)
else:
msg = "Client.login_oauth2(). Could not authenticate. Check username and password, and enter a valid MFA code."
raise AuthenticationError(msg)
elif res.get('mfa_required') is True:
msg = "Client.login_oauth2(). Could not authenticate. MFA is required."
raise AuthenticationError(msg)
self.access_token = res["access_token"]
self.refresh_token = res["refresh_token"]
self.mfa_code = res["mfa_code"]
self.scope = res["scope"]
self.__set_account_info()
return self.authenticated
def __set_account_info(self):
account_urls = Account.all_urls(self)
if len(account_urls) > 1:
msg = "fast_arrow 'currently' does not handle multiple account authentication."
raise NotImplementedError(msg)
elif len(account_urls) == 0:
msg = "fast_arrow expected at least 1 account."
raise AuthenticationError(msg)
else:
self.account_url = account_urls[0]
self.account_id = get_last_path(self.account_url)
self.authenticated = True
def relogin_oauth2(self):
'''
(Re)login using the Oauth2 refresh token
'''
url = "https://api.robinhood.com/oauth2/token/"
data = {
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
"scope": "internal",
"client_id": CLIENT_ID,
"expires_in": 86400,
}
res = self.post(url, payload=data, retry=False)
self.access_token = res["access_token"]
self.refresh_token = res["refresh_token"]
self.mfa_code = res["mfa_code"]
self.scope = res["scope"]
def logout_oauth2(self):
'''
Logout for given Oauth2 bearer token
'''
url = "https://api.robinhood.com/oauth2/revoke_token/"
data = {
"client_id": CLIENT_ID,
"token": self.refresh_token,
}
res = self.post(url, payload=data)
if res == None:
self.account_id = None
self.account_url = None
self.access_token = None
self.refresh_token = None
self.mfa_code = None
self.scope = None
self.authenticated = False
return True
else:
raise AuthenticationError("fast_arrow could not log out.")
`
Alright, so after you get the "status" == "validated", save the challenge_id.
We're gonna repost to login the same way as before, but we're going to add a header:
{ "X-ROBINHOOD-CHALLENGE-RESPONSE-ID" : your_challenge_id }
After this, it'll give you the access_token as usual, and you'll be good to go.
Cheers!
Could you please post the
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Hi,
Thanks a lot for posting this
1) should we use the same logic you have described to generate the client/device id or any random hexadecimal string in this format will work - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ?
2) I am able to get any random id in the above format validated, I also get an email saying that I logged in from US but I am not getting the access token in the next step, which is POSTing the payload along with the challenge id as the header.
Login from Python API not working for me today. Tried after 2 weeks or so... Below is the login code:
import robin_stocks as r
login = r.login('xx@xx.com','xxxxxxx')
my_stocks = r.build_holdings()
#print(my_stocks)
Below is the error:
400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-e8d9c6d0d00c> in <module>()
----> 1 login = r.login('xxxxxxx','xxxxxxxxxxx')
2 my_stocks = r.build_holdings()
3 #print(my_stocks)
C:\ProgramData\Anaconda3\lib\site-packages\robin_stocks\authentication.py in login(username, password, expiresIn, scope)
27 }
28 data = helper.request_post(url,payload)
---> 29 token = 'Bearer {}'.format(data['access_token'])
30 helper.update_session('Authorization',token)
31 return(data)
TypeError: 'NoneType' object is not subscriptable
Sorry guys, I'm back. Busy week.
Using the plan that @derekshreds laid out, I've written code up until I send {'response':'the_sms_code_they_sent'}, but I only have the SMS code on my phone and obviously it changes everytime. Once we get that, we'll be back up and running.
Any thoughts on how to do this the simplest? It doesn't return with their json response and I'm not sure how to get that off my phone.
I assume this is why @wccramer and his forum went for the email route, but it seems the issue with that is the lag in receiving the email.
After posting the above, I'm realizing that unless @derekshreds or someone else has a magical way to get the SMS response code simply, that going the SMS route is going to be a serious pain. I'm going to try to do this the email way and code for having to wait for the email.
This is going to mean that we're going to have to give our emails and email passwords and for those of us GMail users, we're going to have to lower security on the accounts we use for this.
The other option is to do this for our phones and write code for if you have an iPhone, Android, etc.....
Even if we were to pull off the Email way, there would still be a lag in automated trading software using this API when having to log back in.
It's lookin like the golden days of the unofficial Robinhood API are over.
I'm pasting the code I used below for the SMS. Convert it to test the Email way by changing the payload challenge_type to "email". If you use this with the Endpoints.py and Excpeptions.py files in the same directory, you'll see what I was working with. The commented out code in the Login function was where I was testing.
import logging
import warnings
from enum import Enum
#External dependencies
from six.moves.urllib.parse import unquote # pylint: disable=E0401
from six.moves.urllib.request import getproxies # pylint: disable=E0401
from six.moves import input
import getpass
import requests
import six
import dateutil
import time
import random
#Application-specific imports
import exceptions as RH_exception
import endpoints
class Robinhood:
def __init__(self):
self.session = requests.session()
self.session.proxies = getproxies()
self.headers = {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Robinhood-API-Version": "1.0.0",
"Connection": "keep-alive",
"User-Agent": "Robinhood/823 (iPhone; iOS 7.1.2; Scale/2.00)"
}
self.session.headers = self.headers
self.auth_method = self.login_prompt
self.device_token = ""
self.challenge_id = ""
def GenerateDeviceToken(self):
rands = []
for i in range(0,16):
r = random.random()
rand = 4294967296.0 * r
rands.append((int(rand) >> ((3 & i) << 3)) & 255)
hexa = []
for i in range(0,256):
hexa.append(str(hex(i+256)).lstrip("0x").rstrip("L")[1:])
id = ""
for i in range(0,16):
id += hexa[rands[i]]
if (i == 3) or (i == 5) or (i == 7) or (i == 9):
id += "-"
self.device_token = id
def login_prompt(self): # pragma: no cover
"""Prompts user for username and password and calls login() """
username = input("Username: ")
password = getpass.getpass()
return self.login(username=username, password=password)
def login(self,
username,
password,
mfa_code=None):
"""Save and test login info for Robinhood accounts
Args:
username (str): username
password (str): password
Returns:
(bool): received valid auth token
"""
self.username = username
self.password = password
if self.device_token == "":
self.GenerateDeviceToken()
#or if self.device_token is invalid. need to find a way to check for this
payload = {
'password': self.password,
'username': self.username,
'grant_type': 'password',
'client_id': "c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS",
'expires_in': '86400',
'scope': 'internal',
'device_token': self.device_token,
'challenge_type': 'sms'
}
if mfa_code:
payload['mfa_code'] = mfa_code
try:
res = self.session.post(endpoints.login(), data=payload, timeout=15)
print(res.json())
response_data = res.json()
self.challenge_id = response_data["challenge"]["id"]
print(self.challenge_id)
sms_challenge_endpoint = "https://api.robinhood.com/challenge/{}/respond/".format(self.challenge_id)
challenge_res = {"response":"wtf"}
res2 = self.session.post(sms_challenge_endpoint, data=challenge_res, timeout=15)
print(res2.json())
# res.raise_for_status()
# data = res.json()
# print(data)
except requests.exceptions.HTTPError:
raise RH_exception.LoginFailed()
if 'mfa_required' in data.keys(): # pragma: no cover
raise RH_exception.TwoFactorRequired() # requires a second call to enable 2FA
if 'access_token' in data.keys() and 'refresh_token' in data.keys():
self.auth_token = data['access_token']
self.refresh_token = data['refresh_token']
self.headers['Authorization'] = 'Bearer ' + self.auth_token
return True
return False
def investment_profile(self):
"""Fetch investment_profile """
res = self.session.get(endpoints.investment_profile(), timeout=15)
res.raise_for_status() # will throw without auth
data = res.json()
return data
def instruments(self, stock):
"""Fetch instruments endpoint
Args:
stock (str): stock ticker
Returns:
(:obj:`dict`): JSON contents from `instruments` endpoint
"""
res = self.session.get(endpoints.instruments(), params={'query': stock.upper()}, timeout=15)
res.raise_for_status()
res = res.json()
# if requesting all, return entire object so may paginate with ['next']
if (stock == ""):
return res
return res['results']
def instrument(self, id):
"""Fetch instrument info
Args:
id (str): instrument id
Returns:
(:obj:`dict`): JSON dict of instrument
"""
url = str(endpoints.instruments()) + "?symbol=" + str(id)
try:
req = requests.get(url, timeout=15)
req.raise_for_status()
data = req.json()
except requests.exceptions.HTTPError:
raise RH_exception.InvalidInstrumentId()
return data['results']
def quote_data(self, stock=''):
"""Fetch stock quote
Args:
stock (str): stock ticker, prompt if blank
Returns:
(:obj:`dict`): JSON contents from `quotes` endpoint
"""
url = None
if stock.find(',') == -1:
url = str(endpoints.quotes()) + str(stock) + "/"
else:
url = str(endpoints.quotes()) + "?symbols=" + str(stock)
#Check for validity of symbol
try:
req = self.session.get(url, headers=self.headers, timeout=15)
req.raise_for_status()
data = req.json()
except requests.exceptions.HTTPError:
raise RH_exception.InvalidTickerSymbol()
return data
my_trader = Robinhood()
print(my_trader.login(username="username",password="password"))
For my own purposes I'm not sure I want to run with an API like this with my money. If someone has a better fix or an idea to help, let me know. Hope I was some help. I'm disappointed that Robinhood would alienate all of their quantitative trader customers like this.
There is the option to have the API request for the SMS code through the console when logging in, but that would mean this API would no longer be suitable to trade totally autonomously because you'd have to input your SMS every day.
There is the option to have the API request for the SMS code through the console when logging in, but that would mean this API would no longer be suitable to trade totally autonomously because you'd have to input your SMS every day.
I struggled with the concept of automating the SMS code... the only thing I could think of was using Google Messages, which allows you to link your desktop to your mobile SMS using a QR Code, you would then receive all your text messages, including the authentication text, on both devices...
it's probably a terrible workaround... but i'm all out of ideas.
This is what I'm going to do: I've made the API on my repository use the console to input the SMS code. I'm going to clean up the API's code more and it should be ready to go for trading tomorrow morning if all goes well. I'll test it.
In theory, one could use this new API to trade with an algorithm if they signed in every day they were going to trade with it. This should be more reliable than having to wait for a login from an email scrape, which could occur at a bad time.
@Jamonek it's up to you what you want to do with the parent code
Nevermind about it being ready for tomorrow. There are issues with how Robinhood will ask for an additional login before placing the first order of the day. I'm going to be testing for a while. Will let you know when I think it's good.
It happened last night (04/25/2019) when I restarted my client app and has been like this since. Anyone else having the same issue? Thanks