-
Notifications
You must be signed in to change notification settings - Fork 21
Description
Error
From 2025-10-24, I've noticed I was getting the following error:
[i] Logging into Substack...
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): substack.com:443
DEBUG:urllib3.connectionpool:https://substack.com:443 "GET /api/v1/user/profile/self HTTP/1.1" 200 None
[-] An unexpected error occurred: 'NoneType' object is not subscriptableCode
This is my code which is in use:
print("[i] Logging into Substack...")
try:
api = Api(
debug=logging.getLogger().isEnabledFor(logging.DEBUG),
publication_url=os.getenv("PUBLICATION_URL"),
cookies_path=cookie,
email=email,
password=password
)
print("[+] Logged into Substack!")
except Exception as e:
print(f"[-] An unexpected error occurred: {e}")
sys.exit(1)API (Today)
Digging into the API response as of today (2025-12-18):
$ curl \
--silent \
--header 'Cookie: substack.sid=[REMOVED]' \
'https://substack.com/api/v1/user/profile/self' \
| jq '.. | objects | keys'
[
"bio",
"can_dm",
"facebookAccount",
"followerCount",
"followsViewer",
"handle",
"hasActivity",
"hasGuestPost",
"hasHiddenPublicationUsers",
"hasHiddenSubscriptions",
"hasLikes",
"id",
"isFollowing",
"isSubscribed",
"lists",
"max_pub_tier",
"name",
"photo_url",
"previousSlug",
"previous_name",
"primaryPublicationSubscriptionState",
"profile_disabled",
"profile_set_up_at",
"publicationUsers",
"reader_installed_at",
"slug",
"status",
"subscriberCount",
"subscriberCountNumber",
"subscriberCountString",
"subscribesToViewerSubdomain",
"subscriptions",
"subscriptionsTruncated",
"theme",
"tos_accepted_at",
"twitterAccount",
"userLinks",
"visibleSubscriptionsCount"
]
[
"id",
"label",
"type",
"url",
"value"
]
[
"id",
"label",
"type",
"url",
"value"
]
[
"id",
"label",
"type",
"url",
"value"
]
[]
[
"badge",
"bestsellerTier",
"leaderboard",
"paidPublicationIds",
"subscriber",
"subscriberTier",
"vip"
]
$API (Previously)
Looking at NHagar/substack_ap (WayBackMachine), was able to see a able to see the historical version of the API response:
$ cat /tmp/usage_walkthrough.json | jq 'keys'
[
"bestseller_badge_disabled",
"bestseller_tier",
"bio",
"followsViewer",
"handle",
"hasActivity",
"hasGuestPost",
"hasHiddenPublicationUsers",
"hasLikes",
"id",
"isFollowing",
"isPersonalEligible",
"isSubscribed",
"lists",
"max_pub_tier",
"name",
"photo_url",
"previousSlug",
"previous_name",
"primaryPublication",
"profile_disabled",
"profile_set_up_at",
"publicationUsers",
"rough_num_free_subscribers",
"rough_num_free_subscribers_int",
"slug",
"subdomainUrl",
"subscriberCount",
"subscriberCountNumber",
"subscriberCountString",
"subscriptions",
"subscriptionsTruncated",
"tos_accepted_at",
"userLinks",
"visibleSubscriptionsCount"
]
$Converted JSON: usage_walkthrough.json
Compare APIs
$ cat /tmp/usage_walkthrough.json| jq 'keys' > usage_walkthrough-keys
$
$ curl \
--silent \
--header 'Cookie: substack.sid=[REMOVED]' \
'https://substack.com/api/v1/user/profile/self' | jq 'key > self-keys
$
$ diff self-keys usage_walkthrough-keys
1a2,3
> "bestseller_badge_disabled",
> "bestseller_tier",
3,5d4
< "can_dm",
< "facebookAccount",
< "followerCount",
11d9
< "hasHiddenSubscriptions",
14a13
> "isPersonalEligible",
22c21
< "primaryPublicationSubscriptionState",
---
> "primaryPublication",
26c25,26
< "reader_installed_at",
---
> "rough_num_free_subscribers",
> "rough_num_free_subscribers_int",
28c28
< "status",
---
> "subdomainUrl",
32d31
< "subscribesToViewerSubdomain",
35d33
< "theme",
37d34
< "twitterAccount",
$Following the error
Looking for /api/v1/user/profile/self:
$ git clone https://github.com/ma2za/python-substack
[...]
$
$ cd ./python-substack/
$
$ grep -Rn '/api/v1/user/profile/self'
$
$ grep -Rn '/self'
./substack/api.py:234: response = self._session.get(f"{self.base_url}/user/profile/self")
$
$ grep -Rn -C 5 '/self'
./substack/api.py-229-
./substack/api.py-230- def get_user_profile(self):
./substack/api.py-231- """
./substack/api.py-232- Gets the users profile
./substack/api.py-233- """
./substack/api.py:234: response = self._session.get(f"{self.base_url}/user/profile/self")
./substack/api.py-235-
./substack/api.py-236- return Api._handle_response(response=response)
./substack/api.py-237-
./substack/api.py-238- def get_user_settings(self):
./substack/api.py-239- """Can see that ./substack/api.py has get_user_profile().
$ grep -n get_user_profile ./substack/api.py | wc -l
4
$
$ grep -n get_user_profile ./substack/api.py
179: profile = self.get_user_profile()
205: profile = self.get_user_profile()
218: profile = self.get_user_profile()
230: def get_user_profile(self):
$And its only used called in 3 places.
$ grep -nB 6 self.get_user_profile ./substack/api.py
173- def get_user_id(self):
174- """
175-
176- Returns:
177-
178- """
179: profile = self.get_user_profile()
--
199-
200- def get_user_primary_publication(self):
201- """
202- Gets the users primary publication
203- """
204-
205: profile = self.get_user_profile()
--
212-
213- def get_user_publications(self):
214- """
215- Gets the users publications
216- """
217-
218: profile = self.get_user_profile()
$ get_user_id()get_user_primary_publication()get_user_publications()
get_user_id()
$ grep get_user_id ./substack/api.py
def get_user_id(self):
$...As our code doesn't use get_user_id, its not going to be this
get_user_primary_publication() / get_user_publications()
$ grep self.get_user_primary_publication ./substack/api.py
user_publication = self.get_user_primary_publication()
$
$ grep self.get_user_publications ./substack/api.py
user_publications = self.get_user_publications()
$ So let's see where these are being called:
class Api:
[...]
def __init__(
[...]
# if the user provided a publication url, then use that
if publication_url:
[...]
user_publications = self.get_user_publications()
[...]
else:
# get the users primary publication
user_publication = self.get_user_primary_publication()Bingo! This starts to match up whats in the code (right at the top)
So using Api(), if publication_url is set, get_user_publications() is called, Otherwise its get_user_primary_publication()
For what its worth, publication_url is set for me
get_user_publications()
Looking at the function:
def get_user_publications(self):
"""
Gets the users publications
"""
profile = self.get_user_profile()
# Loop through users "publicationUsers" list, and return a list
# of dictionaries of "name", and "subdomain", and "id"
user_publications = []
for publication in profile["publicationUsers"]:
[...]Its expecting publicationUsers in the API response back....
It USED to be there, but its not anymore!
get_user_primary_publication()
Looking at the function:
def get_user_primary_publication(self):
"""
Gets the users primary publication
"""
profile = self.get_user_profile()
primary_publication = profile["primaryPublication"]
[...]Its expecting primaryPublication in the API response back....
Again It USED to be there, but its not anymore!
This may start to explain why its failing....