Here's an approach that works for me:
from playwright.sync_api import sync_playwright # 1.53.0
USERNAME = "<your username>"
PASSWORD = "<your password>"
BASE_URL = "https://www.teamstats.net/"
TEAM_URL = "https://www.teamstats.net/enyimbafcaba/home"
emails = (
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
)
with sync_playwright() as p:
browser = p.chromium.launch(headless=False, args=["--ignore-certificate-errors"])
context = browser.new_context(ignore_https_errors=True)
page = context.new_page()
print("🔑 Logging in with credentials.")
page.goto(BASE_URL)
page.get_by_role("button", name="Login").click()
page.get_by_role("textbox", name="Username or email address").wait_for()
page.get_by_role("textbox", name="Username or email address").fill(USERNAME)
page.get_by_role("textbox", name="Password").wait_for()
page.get_by_role("textbox", name="Password").fill(PASSWORD)
page.get_by_role("button", name="LOG IN").click()
page.wait_for_url(TEAM_URL, wait_until="domcontentloaded")
page.goto(TEAM_URL, wait_until="domcontentloaded")
page.get_by_role("link", name="squad").click()
for email in emails:
first, last = [f"foo {email}", "bar"]
print(f"➕ Adding {first} {last} ({email})")
page.get_by_role("button", name="ADD NEW").click()
page.get_by_role("button", name="Add Single Member").click()
page.locator(".ts-card .ts-list-item").first.wait_for()
first_name_input = page.locator("div.ts-text-input input").first
first_name_input.fill(first)
last_name_input = page.locator("div.ts-text-input input").nth(1)
last_name_input.fill(last)
email_input = page.locator("div.ts-text-input input").nth(4)
email_input.fill(email)
page.get_by_role("button", name="SAVE").click()
try:
page.get_by_text("NO", exact=True).click(force=True, timeout=5_000)
except:
pass
print("🎉 Finished adding all players.")
browser.close()
The most critical parts are:
page.locator(".ts-card .ts-list-item").first.wait_for() which waits for the player list to load. For whatever reason, inputs don't seem to work until this list is populated.
email_input = page.locator("div.ts-text-input input").nth(4) the email is the 5th, not the 3rd element in the input list.
A potentially better approach is to send direct HTTP requests.
Once you have the cookie, then you can often skip browser automation entirely and just make a fetch request to create the data from Node or Python, or from the browser console (with evaluate).
If you open the network tab, fill out a new team member manually as a human, then find the POST request that was sent, you can copy the payload:


This gives:
await fetch("https://api.teamstats.net/api/member/setMember.asp", {
"headers": {
"accept": "application/json, text/plain, */*",
"accept-language": "en-US,en;q=0.9",
"content-type": "application/x-www-form-urlencoded;charset=UTF-8",
"priority": "u=1, i",
"sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"cookie": "_scor_uid=3f65e4a414ca420d8cb09150b34efaeb; _gid=GA1.2.908336851.1756579405; _fbp=fb.1.1756579404993.119640189542663532; crisp-client%2Fsession%2F5af2bc25-165c-4cb1-b9e9-a56f55a70791=session_116014da-1f89-4d26-8e6d-e1f415d390b3; pb_ga=GA1.2.1069470341.1756579405; pb_ga_gid=GA1.2.838530501.1756579416; serverName=https%3A%2F%2Fwww%2Eteamstats%2Enet; _ga=GA1.2.1069470341.1756579405; pb_ga_ga_2ZPQ07VLSX=GS2.2.s1756579416$o1$g1$t1756579777$j60$l0$h0; ASPSESSIONIDCEQRABSS=DNOELLFBOIMNBKFJFDJIMJNI; cto_bundle=lI3fGF9yJTJGd3FmYmJOdFBmd3JkUnUzb3pBMkJJVWw3TUFHTkw2emdVVEkxTU41bE1KYXM2Q2dsUEZJaVFDOHRQSCUyQmxtVWYwVWl5ZkV4Q1VGTEFhUkdvTmRvVTE1VmprR1RGQTNZa1BnS0tRVERDSkpMYTR0cE1qajRnY2pwWllZWnVZZlhTTyUyQnlPcnhUYmxBWDJwTm9GaGJUcU43cWljSUpqN1Q3U3JGVHc4dmVvRkxSY1FoRHlPeHI4dDJtdHBnclhYNERwUWpIa0laSiUyQnAlMkJPdDh3eDNRMWV6dyUzRCUzRA; _gat_UA-85619626-2=1; visitorHash=; _ga_YCF3Y4G5KN=GS2.1.s1756579404$o1$g1$t1756579868$j42$l0$h0; amp_eb3a2f=L_J-EHB6EYh4PKZj_xwY3G...1j3u50ls0.1j3u53bhe.4.0.4",
"Referer": "https://www.teamstats.net/"
},
"body": "sid=D86DD940-D285-F011-BB93-14187749D29F&path=%2Fenyimbafcaba%2Fadmin%2Fmembers%2Fedit%2F0%2Fnew%2Fdetails&mobile=false&lastAccessedDate=20250830%2011%3A51%3A33&stateName=admin.members.edit.details&stateUniqueId=0&stateUrlTitle=new&adsDataLastUpdated=30-Aug-2025%2019%3A33%3A12&tsVersionNum=3.4.05&tsPlatform=web&appDataLastUpdated=29-Aug-2025%2009%3A16%3A12&teamDataLastUpdated=30-Aug-2025%2019%3A33%3A12&teamId=1679386&clubId=48521&timeZoneOffset=1&teamName=Enyimba%20FC%20Aba&teamSiteUrl=enyimbafcaba&timeZoneCountryCode=NG&sportId=1&isClub=false&userUsername=uiwehee&userId=719499&userDataLastUpdated=30-Aug-2025%2019%3A50%3A51&isTeamMember=true&isTeamAdmin=true&isLoggedIn=true&jsonData=%7B%22UserID%22%3A0%2C%22UserTitle%22%3A%22%22%2C%22UserFirstName%22%3A%22xyx%22%2C%22UserSurname%22%3A%22bar%22%2C%22UserUsername%22%3A%22%22%2C%22UserPassword%22%3A%22%22%2C%22UserLastLoggedIn%22%3Anull%2C%22UserEmail%22%3A%22baz%40gmail.com%22%2C%22UserMobileNumber%22%3A%22%22%2C%22UserEmailSecondary%22%3A%22%22%2C%22UserMobileNumberSecondary%22%3A%22%22%2C%22UserDOB%22%3A%22%22%2C%22UserAddress%22%3A%22%22%2C%22UserPostCode%22%3A%22%22%2C%22UserNotes%22%3A%22%22%2C%22UserTwitterUsername%22%3A%22%22%2C%22TeamMemberID%22%3A0%2C%22TeamMemberIntroText%22%3A%22%22%2C%22TeamMemberPhotoMediaID%22%3Anull%2C%22TeamMemberCoverMediaID%22%3Anull%2C%22TeamMemberPhotoFileName%22%3Anull%2C%22TeamMemberCoverPhotoFileName%22%3Anull%2C%22TeamMemberPhotoPath%22%3A%22%2Fteamstats-media%2Ficons%2Favatar-circle.png%22%2C%22TeamMemberCoverPhotoPath%22%3A%22%2Fteamstats-media%2Fimages%2Fteam-member-cover.jpg%22%2C%22TeamMemberNickName%22%3A%22%22%2C%22TeamMemberPlayerPositionID%22%3A5%2C%22TeamMemberTeamRoleID%22%3A1%2C%22TeamMemberRoleText%22%3A%22%22%2C%22UploadQueueID%22%3Anull%2C%22TeamAdminUserID%22%3A719499%2C%22TeamAdminFullName%22%3A%22Uche%20Iwehee%22%2C%22TeamAdminFullNameShort%22%3A%22U.%20Iwehee%22%2C%22TeamAdminEmail%22%3A%22uiwehee%40gmail.com%22%2C%22TeamAdminMobile%22%3A%22%22%2C%22MobileSendMethodID%22%3A3%2C%22DefaultDOB%22%3A%2201%20Jan%202000%22%7D",
"method": "POST"
});
From this, I was able to make players from the Node repl:
Response {
status: 200,
statusText: 'OK',
headers: Headers {
'cache-control': 'private',
'content-type': 'text/html; Charset=utf-8',
server: 'TeamStats',
'set-cookie': 'serverName=https%3A%2F%2Fwww%2Eteamstats%2Enet; path=/, ASPSESSIONIDCEQRABSS=EJMILLFBIFPLFLKFADLMLFAI; secure; path=/',
'access-control-allow-headers': 'Origin, X-Requested-With, Content-Type, Accept',
'access-control-allow-methods': 'OPTIONS, GET, POST, PUT, DELETE',
'access-control-allow-credentials': 'true',
'access-control-allow-origin': 'http://localhost:51926',
'content-security-policy': "frame-ancestors 'self' https://www.teamstats.net https://api.teamstats.net https://staging.teamstats.net",
date: 'Sun, 31 Aug 2025 03:22:53 GMT',
'content-length': '59646'
},
body: ReadableStream { locked: false, state: 'readable', supportsBYOB: true },
bodyUsed: false,
ok: true,
redirected: false,
type: 'basic',
url: 'https://api.teamstats.net/api/member/setMember.asp'
}
Next step is to URL encode the parameters and reverse engineer any other dynamic values to pull from the browser, although the latter may not be necessary depending on your use case (this may be a one-off operation).
Here's a first step towards this:
import json
import requests
url = "https://api.teamstats.net/api/member/setMember.asp"
data = {
"UserID": 0,
"UserTitle": "",
"UserFirstName": "aa new made 2",
"UserSurname": "asdf",
"UserUsername": "",
"UserPassword": "",
"UserLastLoggedIn": None,
"UserEmail": "[email protected]",
"UserMobileNumber": "",
"UserEmailSecondary": "",
"UserMobileNumberSecondary": "",
"UserDOB": "",
"UserAddress": "",
"UserPostCode": "",
"UserNotes": "",
"UserTwitterUsername": "",
"TeamMemberID": 0,
"TeamMemberIntroText": "",
"TeamMemberPhotoMediaID": None,
"TeamMemberCoverMediaID": None,
"TeamMemberPhotoFileName": None,
"TeamMemberCoverPhotoFileName": None,
"TeamMemberPhotoPath": "/teamstats-media/icons/avatar-circle.png",
"TeamMemberCoverPhotoPath": "/teamstats-media/images/team-member-cover.jpg",
"TeamMemberNickName": "",
"TeamMemberPlayerPositionID": 5,
"TeamMemberTeamRoleID": 1,
"TeamMemberRoleText": "",
"UploadQueueID": None,
"TeamAdminUserID": 719499,
"TeamAdminFullName": "Uche Iwehee",
"TeamAdminFullNameShort": "U. Iwehee",
"TeamAdminEmail": "[email protected]",
"TeamAdminMobile": "",
"MobileSendMethodID": 3,
"DefaultDOB": "01 Jan 2000"
}
params = {
"sid": "D7546C14-0386-F011-BB93-14187749D29F",
"path": "/enyimbafcaba/admin/members/edit/0/new/details",
"mobile": "false",
"lastAccessedDate": "20250830 20:34:59",
"stateName": "admin.members.edit.details",
"stateUniqueId": "0",
"stateUrlTitle": "new",
"adsDataLastUpdated": "31-Aug-2025 04:34:13",
"tsVersionNum": "3.4.05",
"tsPlatform": "web",
"appDataLastUpdated": "29-Aug-2025 09:16:12",
"teamDataLastUpdated": "31-Aug-2025 04:34:13",
"teamId": "1679386",
"clubId": "48521",
"timeZoneOffset": "1",
"teamName": "Enyimba FC Aba",
"teamSiteUrl": "enyimbafcaba",
"timeZoneCountryCode": "NG",
"sportId": "1",
"isClub": "false",
"userUsername": "uiwehee",
"userId": "719499",
"userDataLastUpdated": "31-Aug-2025 01:40:22",
"isTeamMember": "true",
"isTeamAdmin": "true",
"isLoggedIn": "true",
"jsonData": json.dumps(data)
}
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "en-US,en;q=0.5",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "same-site",
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
"Priority": "u=0",
"Pragma": "no-cache",
"Cache-Control": "no-cache",
"Referer": "https://www.teamstats.net/",
}
response = requests.post(url, data=params, headers=headers)
print(response.status_code)
print(response.text[:500])
Here's a request to edit an existing member using the same endpoint:
import json
import requests
url = "https://api.teamstats.net/api/member/setMember.asp"
data = {
"UserID": 724222,
"UserTitle": None,
"UserFirstName": "updated via request",
"UserSurname": "hello",
"UserUsername": "xa1679386",
"UserUsernameOriginal": "xa1679386",
"UserPassword": "",
"UserLastLoggedIn": None,
"UserEmail": "",
"UserEmailOriginal": "",
"UserEmailSecondary": "",
"UserMobileNumber": "",
"UserMobileOriginal": "",
"UserMobileNumberSecondary": "",
"UserDOB": None,
"UserAddress": "",
"UserPostCode": "",
"UserNotes": "",
"UserFacebookID": None,
"UserFacebookEmail": "",
"UserFacebookUsername": None,
"UserFacebookLastLoggedIn": None,
"UserFacebookAddedByAdmin": None,
"UserTwitterID": None,
"UserTwitterUsername": "",
"UserTwitterEmail": "",
"UserTwitterLastLoggedIn": None,
"UserGoogleID": None,
"UserGoogleUsername": None,
"UserGoogleEmail": "",
"UserGoogleLastLoggedIn": None,
"UserMobileAppLastLoggedIn": None,
"UserWhatsAppAccountID": None,
"UserWhatsAppAccountSynced": False,
"UserWhatsAppAccountLinkClicked": False,
"TeamAdminUserID": 719499,
"TeamAdminFullName": "Uche Iwehee",
"TeamAdminFullNameShort": "U. Iwehee",
"TeamAdminEmail": "[email protected]",
"TeamAdminMobile": "",
"MobileSendMethodID": 3,
"TeamMemberID": 726569,
"Age": None,
"TeamMemberIntroText": "",
"TeamMemberPhotoMediaID": None,
"TeamMemberCoverMediaID": None,
"TeamMemberPhotoPath": "/teamstats-media/icons/avatar-circle.png",
"TeamMemberPhotoFileName": None,
"TeamMemberCoverPhotoPath": "/teamstats-media/images/team-member-cover.jpg",
"TeamMemberCoverPhotoFileName": None,
"UploadQueueID": "",
"TeamMemberNickName": "",
"Position": "Not Specified",
"TeamMemberPlayerPositionID": 5,
"TeamMemberTeamRoleID": 1,
"TeamRole": "Player Only",
"TeamMemberRoleText": "Not Specified",
"DefaultDOB": "01 Jan 2000"
}
params = {
"sid": "D7546C14-0386-F011-BB93-14187749D29F",
"path": "/enyimbafcaba/admin/members/edit/726569/x-a/details",
"mobile": "false",
"lastAccessedDate": "20250830 20:09:19",
"stateName": "admin.members.edit.details",
"stateUniqueId": "726569",
"stateUrlTitle": "x-a",
"adsDataLastUpdated": "31-Aug-2025 03:46:49",
"tsVersionNum": "3.4.05",
"tsPlatform": "web",
"appDataLastUpdated": "29-Aug-2025 09:16:12",
"teamDataLastUpdated": "31-Aug-2025 03:46:48",
"teamId": "1679386",
"clubId": "48521",
"timeZoneOffset": "1",
"teamName": "Enyimba FC Aba",
"teamSiteUrl": "enyimbafcaba",
"timeZoneCountryCode": "NG",
"sportId": "1",
"isClub": "false",
"userUsername": "uiwehee",
"userId": "719499",
"userDataLastUpdated": "31-Aug-2025 01:40:22",
"isTeamMember": "true",
"isTeamAdmin": "true",
"isLoggedIn": "true",
"jsonData": json.dumps(data)
}
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/201001 Firefox/142.0",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "en-US,en;q=0.5",
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site",
"Priority": "u=0",
"Referer": "https://www.teamstats.net/",
}
response = requests.post(url, data=params, headers=headers)
print(response.status_code)
I should re-emphasize that some of these tokens may go stale, so you'll need to re-copy the fetch from your browser session, pull relevant tokens and/or update the cookie after it expires. All of this can be done programmatically, but I'll leave the rest as an exercise for the reader for now. Hopefully this communicates the main idea though.
email_input, but subsequent cookie runs hitwaiting for get_by_role("button", name="ADD NEW").