About these notes
A note for Rockstar Games
SocialClub Plus is a free fan project, not affiliated with or endorsed by Rockstar Games or Take-Two Interactive. It exists to make the Social Club easier to use for GTA Online players, where the official website offers little, and it's not meant to harm Rockstar's services or anyone who uses them. The app uses no exploits and no vulnerabilities. It sends the same requests the Social Club website, the Rockstar Games Launcher and the game send, on the player's own account, and keeps its requests few. Problems I found in Rockstar's services along the way are reported to Rockstar privately and are not published here. If Rockstar would like anything on this page changed or removed, you can reach me through the contact page.
None of this is documented by Rockstar. I read it from the Social Club website's scripts, the overlay's scripts and the game's own code, and checked it against the live services. Any of it can change without notice.
| Measured | Seen in answers from Rockstar's servers. |
| Read | Read in code: the website's scripts, the game's, or the app's own. |
| Assumed | An inference, not verified yet. |
The game's encryption keys and a few problems still pending with Rockstar are left out on purpose.
Services
Website API
Job, playlist, crew, player and photo search, job details, bookmarks, profiles, crew emblems.
Website sign-in, some calls work signed outRockstar Online Services (ROS)
The game's back end: user content (playlists, photos), matchmaking, presence, crews.
Launcher sign-in and a ticketOverlay API
What the in-game Social Club overlay calls: profile, avatars, friends, privacy.
A ROS ticketContent server
Job pictures, job creator files, crew emblems.
PublicOld website
The pre-2019 Social Club, still online: career pages, the playlist browser, the emblem editor.
Website sign-in, browser cookiesNotes
- The website session and the ROS session are separate, and tokens from one are not accepted by the other. One sign-in can give both, though (below).
- The website API doesn't reject unknown query parameters. A wrong parameter name has no effect and gives no error, so every filter has to be checked by comparing results.
- Some limits are enforced without an error: invalid jobs are dropped from a playlist, job search results past the 10,000th can't be paged to, and a lobby closed by its host is still advertised.
- The website API allows about 30 searches back to back. The 31st answers
429, with no header saying how long to wait, and it clears after about 30 seconds. The limit is per address, not per account: right after a 429 the same search signed out got a 429 too. Measured
Sign-in
Website
| Item | Details |
|---|---|
| Session | rsso-<suffix> cookie on signin.rockstargames.com, HttpOnly, 365 days. The suffix differs per account. Measured |
| Token | BearerToken cookie, a JWT valid for about 5 minutes. Sent as Authorization: Bearer <token>. |
| Minting | Set by the sign-in page's JavaScript while the rsso session is valid, not by an HTTP redirect. |
There are two authorize entry points, and only one of them gives a token that can be renewed: Measured
| Entry point | Token set on | Renewable |
|---|---|---|
/connect/authorize/socialclub?lang=en-US&returnUrl=… | socialclub.rockstargames.com | Yes |
/connect/authorize/rsg?lang=en-us&returnUrl=… | www.rockstargames.com | No (401) |
Renewal:
POST https://socialclub.rockstargames.com/connect/refreshaccess X-Requested-With: XMLHttpRequest Content-Type: application/x-www-form-urlencoded; charset=utf-8 Cookie: <the cookies left by the sign-in> accessToken=<current token>
Sent with the token alone, without the sign-in cookies, it answers 401. Measured
Game (ROS)
ROS only accepts sessions obtained the way the Rockstar Games Launcher obtains them. Tickets from an email and password (CreateTicketSc3) are refused with NotAllowed/Password. Measured
| Step | Request | Result |
|---|---|---|
| 1 | GET https://signin.rockstargames.com/signin/user-form?cid=launcher. The page runs in launcher mode and expects the launcher's native bridge. | An authCode, valid for about a minute |
| 2 | GET https://rgl.rockstargames.com/api/connect/gateway?code=<authCode>&fingerprint=<state>, where the state is {"fp": <device fingerprint>} | A ScAuthToken, a launcher ticket, and a remembered-machine token that may be empty |
| 3 | POST https://gta5-prod.ros.rockstargames.com/gta5/11/gameservices/auth.asmx/CreateTicketScAuthToken2 | A ticket |
Step 1: the launcher bridge
Inside the launcher, the sign-in page talks to the launcher through two functions on window, rgscQuery and rgscAddSubscription. Each takes an object with method, params and an onSuccess callback. In a normal browser they're missing and the page can't finish. These are the answers the page needs: Read
| Call | Answer |
|---|---|
rgscQuery GetWindowConfig | {"serviceInfo": {"rosPlatform": "pc"}, "platformInfo": {}} |
rgscQuery GetDeviceId | {"fingerprintKvp": <device fingerprint>} |
rgscQuery GetVersionInfo | {"isDebugSdk": false} |
rgscQuery GetProfileList | {"profiles": []}. With no saved profile, the page shows the email and password form. |
rgscQuery CallAuthResult | Not a question: the page hands over the result here, in params.authCode. |
rgscAddSubscription OnStartAuth | {"launchPlatform": 0, "titleLaunchInfo": {"activeTitleName": "gta5", "externalPlatformInfo": {}}, "profile": null, "errorMessage": null} |
The device fingerprint is an object of five strings, in this order: device_name, machine_name, os_version, volume_serial, cpu_info. The launcher fills them in from the machine. SocialClub Plus sends its own name as the device and machine, and a random id kept per install for the serial and CPU, so the sign-in shows up as SocialClub Plus and not as the launcher. The page runs its own bot checks, and the password only goes into Rockstar's form.
Step 2: the gateway
GET https://rgl.rockstargames.com/api/connect/gateway?code=<authCode>&fingerprint=<state, URL-encoded> X-Requested-With: XMLHttpRequest Origin: https://rgl.rockstargames.com Referer: https://rgl.rockstargames.com/ Cookie: <the sign-in window's cookies for rgl.rockstargames.com, if any>
The state is the fingerprint wrapped as {"fp": {...}}, with one space after the colon, as the launcher writes it. The same string goes with every ticket request and has to match. The JSON answer holds ScAuthToken (164 characters) and RememberedMachineToken. A ScAuthToken keeps minting tickets for days, one about 8 days old still worked. Measured
Step 3: the ticket
POST https://gta5-prod.ros.rockstargames.com/gta5/11/gameservices/auth.asmx/CreateTicketScAuthToken2 User-Agent: ros <encoded version string> SCS-MFAMachineFingerPrint: <the state from step 2> Content-Type: application/x-www-form-urlencoded; charset=utf-8 <the form, encrypted>
| Field | Value |
|---|---|
ticket | Empty |
platformName | pcros (Legacy) or pcrosalt (Enhanced) |
scAuthToken | From step 2 |
rememberedMachineToken | From step 2, may be empty |
The version string is e=1,t=gta5,p=<platformName>,v=11. The User-Agent sends it as ros followed by the Base64 of four bytes 0xCD and then each byte of the string XORed with 0xCD. A GET, or platformName=pc, doesn't work. Measured
The platform name, the key and the platform in the User-Agent must agree, otherwise the answer is DoesNotMatch/TitleInfo. The XML answer holds Ticket, SessionKey, SessionTicket, CloudKey, PlayerAccountId, RockstarId, Privileges, Services (a host per service) and SecsUntilExpiration (86,399, so one day). Measured
One ROS session per account. A new ticket invalidates the previous one (AuthenticationFailed/DupLogin), and a game running on that account is signed out of GTA Online. Measured
| ROS error | Meaning |
|---|---|
AuthenticationFailed/DupLogin | Another session replaced this one |
NotAllowed/Privilege | The ticket has no write privilege on this build |
NotAllowed/Profane | Text refused by the word filter |
InvalidData/MissionCount | Too many jobs in a playlist |
RateLimitExceeded | Too many writes in a short time |
DoesNotMatch/TitleInfo | Platform name, key and User-Agent disagree |
Renewing it from a ticket
A live ticket can make a new ScAuthToken with socialclub.asmx/CreateScAuthToken on prod.ros, fields ticket, audience, ttlminutes and userdata (may be empty). The token comes back in <Result>. The old Social Club SDK does exactly this after signing in, with audience Game and 30 days, and signs in with that token the next time. Read A token made this way with 30 days minted a working Enhanced ticket. Measured Note that GetScAuthToken looks similar but gives a web-linking token that the ticket calls refuse.
So the launcher sign-in can be kept alive without the password, but only by minting a ticket, which ends the account's GTA Online session anyway.
One sign-in for both
The launcher's sign-in leaves the website's session behind too. Right after it, the browser profile holds an rsso-<suffix> cookie, and opening /connect/authorize/socialclub in that profile gives a BearerToken for the same account in about 2 seconds, with no form. Measured
It only works that way round. After the website's own sign-in, the launcher's page still asks for the password, and the ScAuthToken inside the website token's claims (scAuth.ScAuthToken, 160 characters) is refused for a ticket with AuthenticationFailed/ScAuthToken. Measured That's why the app's one sign-in is the launcher's.
The launcher sign-in itself mints no ROS ticket, so signing in doesn't end a GTA Online session. Only the first ticket does.
Game requests
Every ROS call, the ticket request included, sends its form encrypted and gets its answer encrypted. The scheme is the one in FiveM's open source code (ros-patches-five), and the app follows it line by line. Each build of the game has its own platform key, built into the game. The keys are not given here. Read
The platform key
A Base64 string. Decoded, byte 0 isn't used, bytes 1 to 32 are an RC4 key, and bytes 33 to 48 and 49 to 64 are two 16-byte keys, here called the mask key and the hash key. Both are stored encrypted with that RC4 key, each with a fresh RC4 state.
The form
Fields sorted by name in byte order, written as name=value and joined with &. Values are encoded as UTF-8: letters, digits and - _ . ~ stay as they are, a space becomes +, and every other byte becomes %xx in lowercase hex. Calls made with a ticket carry it in the ticket field.
Encrypting a request
- Pick 16 random bytes: the request key.
- Write the request key XORed with the mask key. These are the first 16 bytes of the body.
- With a ticket, XOR the request key with the first 16 bytes of the ticket's
SessionKey(Base64). The ticket request itself has none, and the request key stays as it is. - Encrypt the form with RC4 under the request key, and append it.
- Append a 20-byte trailer, computed over everything written so far followed by the hash key: SHA-1 without a ticket, HMAC-SHA1 keyed with the request key with one.
Reading an answer
The first 16 bytes, XORed with the mask key (and with the session key, for a call made with a ticket), are an RC4 key. One RC4 stream then runs over the rest. Bytes 16 to 19 give a block size, big-endian. From byte 20 on, the answer comes in pieces of the block size plus 20 bytes: the block, then a 20-byte trailer that is skipped. The last piece can be shorter. Inside there's XML. An error raised by the server itself, and not by the service, comes back as plain text.
Calls made with a ticket
They carry four more headers. Measured
| Header | Value |
|---|---|
ros-SecurityFlags | 239 |
ros-SessionTicket | The ticket's SessionTicket |
ros-Challenge | 8 random bytes, Base64 |
ros-HeadersHmac | Base64 of an HMAC-SHA1 keyed with the session key XORed with the mask key (16 bytes), over the method, the path, the security flags, the session ticket and the challenge as sent, each followed by a zero byte, and then the hash key |
Every ROS host answers over https with a valid certificate. A wrong key gives an answer that can't be decrypted, while an error like NotAllowed/… or InvalidArgument/<field> means the server did read the request. Measured
Website API
Host https://scapi.rockstargames.com. Every call carries X-Requested-With: XMLHttpRequest, and the bearer token when signed in.
Access without a token
Signed out, the website sends X-AMC: true (allow mature content) after its age gate. Signed in, it leaves it out. Read
| Call | Without a token |
|---|---|
/search/mission | 200 with X-Requested-With and X-AMC: true, 403 otherwise |
/ugc/mission/details | 200 with both headers |
/search/playlist | 200 with both headers |
/search/crew | 200 |
/search/user | 401 |
/profile/* | 401 |
Measured
Job search
GET /search/mission?title=gtav&platform=pcalt&missionType=race&subType=landrace&sort=likes&searchTerm=&pageIndex=0&pageSize=30
| Parameter | Values |
|---|---|
title | gtav |
platform | pc (Legacy), pcalt (Enhanced), ps4, ps5, xboxone, xboxsx |
missionType | The job type |
subType | The website's lowercase values: landrace, stuntrace, targetrace, airrace, waterrace, creator, adversary, arenadeathmatch, … Ignored without its missionType. |
sort | likes, plays, createdDate (first published), updatedDate (last update), name, relevance. Anything else falls back to last update. |
filter | rockstar, rockstarVerified, community (Rockstar's Community Series, about 35 jobs, not all player jobs), scMembers (all player jobs), trending (recent jobs being played a lot right now), myBookmarks, or crew with crewId |
area | A map zone code (Search by area) |
players | A player count: only jobs whose minimum is at most that and whose maximum is at least that |
creatorRockstarId | A creator's Rockstar ID |
dateRangeCreated | today, last7, lastmonth, lastyear, any, or custom with startDate and endDate |
startDate, endDate | Both accept a time of day, in UTC (2020-03-10T13:00:00). endDate is exclusive. |
searchTerm | Free text over name, description and tags. #tag searches the tags. The API accepts it empty, it's only the website's form that doesn't. |
pageIndex, pageSize | Up to 30 per page |
Measured
- The dates are on two different axes. The presets (
today,last7and so on) filter onrootCreatedDate, when the job was first published. A custom range filters oncreatedDate, the date of the current version, which for an updated job is its last update. I didn't find a custom range on the first publication. Measured - The website writes a crew filter as
crew_<id>in its own address, but the API only filters with the split form,filter=crew&crewId=<id>. communityandrockstarVerifiedignore the platform and return jobs of every build. Measured- No result past the 10,000th can be fetched: at 30 per page, page 332 answers and 333 returns 500.
- The answer has three collections,
items,usersandcrews, linked by id. Signed in, each item carriesbookmarkedandplayedfor the account. - There's no sort direction, no player range, no rank, no vehicle class and no language filter. Each was tried with names that would change the result, and all were ignored. POST answers 405. Measured
- A job's page on the website is
/job/gtav/<id>. The id is the same in playlists and on the content server.
Job details
GET /ugc/mission/details?title=gtav&contentId=<id>
Returns what the website's job page shows, plus content.data.mission, a trimmed copy of the creator file. Signed in, it adds liked, played, bookmarked, and lastPlayed for a job the account has played. Measured
| Field | Meaning |
|---|---|
gen.min, gen.num | Minimum and maximum players |
gen.rank | Minimum rank |
race.chp, race.lap, race.rdis | Checkpoints, laps, length in metres |
race.chl | Checkpoint coordinates |
rule.*, race.aveh, race.adlc | Not known yet (time of day, traffic, vehicle classes as bit masks) |
Crews, players, photos
| Call | Notes |
|---|---|
GET /search/crew | Finds only open crews unless crewtype=all. Other values: opencrews, rockstar, chatterboxes, thrillseekers, rebels, soldiers, allstars. sort: relevance or membercount. Text in searchTerm. |
GET /search/user | Text in searchTerm. service searches the name on another service: sc, np, xbl, xblpc, steam, twitch. total is capped at the page size. |
GET /profile/getbasicprofile?rockstarId=<id> | Name and short profile for any Rockstar ID, one per call. An id it can't read returns the viewer's own profile, not an error. |
GET /profile/playedplatforms?game=gtav | The signed-in account's platforms, for example {"platforms":["pc","pcalt"]}. Always the viewer's own. |
GET /search/photo | Needs platform, and creatorRockstarId for one player. A photo's page is /photo/gtav/<id>. |
Measured
Search by area
The job search has a map zone filter, area=<zone code>, which the website's script accepts but its jobs page has no control for. It returns the jobs whose meta.loc holds that code exactly. meta.loc is a list of zones the game writes into the creator file when the job is published (Job files). Measured
- Races: the zones of the checkpoints, minus the zone the race starts in, even when later checkpoints are in it. So a race that only starts in an area isn't found by that area, and a race entirely inside its start zone has none. Measured
- Other modes: usually the zone they start in. Measured
- The codes are the game's zone names in upper case, cut to seven characters (
BANHAMC, notBANHAMCA). One per call, case-sensitive, and an unknown code gives 0. 93 codes find jobs. Measured - The golf course is spelled two ways: older jobs say
GOLF, newer onesgolf, and they're different jobs. A search for the golf course takes both calls. - Outside every zone (high in the sky, the Arena War arena, Cayo Perico) a job gets
SanAnd, written just like that.
To place a point in a zone the game takes the smallest box by volume among the boxes around it. Read The app reads the boxes from the game's files and draws its map with the same rule (without the height, since the map is flat), and it matched the meta.loc of 29 real races checkpoint by checkpoint. Measured That's why a tunnel or a creek can be a zone of its own.
Bookmarks
Each platform has its own list. The newer generation's list includes the older one's: pcalt includes pc, ps5 includes ps4, xboxsx includes xboxone. Measured
| Action | Request |
|---|---|
| List | GET /search/mission?filter=myBookmarks&platform=<p>. Without a platform it answers status:false. Every job comes with bookmarked: true and played. |
| Set | POST /ugc/mission/bookmark, JSON {"title":"gtav","contentId":"<id>","platformType":"<p>","bookmark":true|false}. It sets the state, it doesn't toggle. Answer {"status":true}. |
platformType: Measured
- A PC job takes its own platform,
pcorpcalt. - A console job, from an account that plays on PC: any platform name returns 500, while an empty value is accepted and lands on that console's list. A player reported the empty value failing on their account too.
- The website sends the job's own platform for every job. Read An account that plays on that console probably accepts it. Assumed
The list is a search, so it counts against the same limit of about 30 searches (Notes). A player with about 1,000 bookmarks hit the 429 reading all six lists. The app now skips an older generation's list when the newer one already has all of it, and spaces its reads. Measured
Job files
A job's full creator file, the data the game loads to run it, is public:
GET https://prod.cloud.rockstargames.com/ugc/gta5mission/{id}/0_{version}_{language}.json
| Part | Value |
|---|---|
{id} | The job id. Measured |
{language} | The language the job was created in, and only that one. It's not written anywhere, so the app tries them in turn. Rockstar's own jobs answered in every language tried. Measured |
{version} | The second number in the job picture's file name (…/2_3.jpg → 3). Assumed |
mission holds sections under short internal names: gen, race, prop, dprop, veh, obj, weap, ene, zone, rule, … The fields that are settled:
| Path | Meaning |
|---|---|
mission.gen.nm | Name |
mission.gen.dec | Description, in chunks |
mission.gen.ownerid | Creator's nickname |
mission.gen.type | Family: 0 mission, 1 deathmatch, 2 race, 3 survival |
mission.gen.min, num | Minimum and maximum players |
mission.gen.start | Where the job starts |
mission.race.chp, lap, rdis, chl | Checkpoints, laps, length in metres, checkpoint coordinates |
meta.loc | The map zones the job is filed under (Search by area) |
Legacy and Enhanced
| Legacy | Enhanced | |
|---|---|---|
| Website platform | pc | pcalt |
| ROS platform | pcros | pcrosalt |
| Jobs a playlist can hold | Legacy | Enhanced and Legacy |
| Bookmark list | Legacy | Enhanced, including Legacy's |
Write privilege follows the build played last. A ticket's Privileges list decides whether it can write. An account that had never played GTA Online on Enhanced got an Enhanced ticket with no privileges: reads worked, writes returned NotAllowed/Privilege. After one session on Enhanced, the Enhanced ticket was complete and the Legacy one empty. The same goes for lobby searches and photo uploads. Measured
Playlists
The website can read playlists but can't create them. They're created through ROS user content, ugc-gta5-prod.ros.rockstargames.com/gta5/11/gameservices/ugc.asmx, content type gta5playlist, with a ticket that has write privilege on the build. Measured
Playlist data:
{ "h2h": true, "list": [ { "cid": "<job id>", "name": "<job name>", "type": 2 } ] }
listis the jobs, in play order. The game only readscid. Readtypeis the family: 0 mission, 1 deathmatch, 2 race, 3 survival.h2handch: meaning not known.- A playlist is published or a draft. A draft stays on the account and can't be played.
- Reads lag behind writes by a minute or two. A running game needs a restart to list a new playlist.
- The game plays jobs in list order unless Shuffle is on in the menu the playlist is started from, and that option starts off every time. Read
| Limit | Value |
|---|---|
| Jobs | 16. 17 returns InvalidData/MissionCount. |
| Unknown job ids | Dropped without a word. The count is checked before the drop. |
| Jobs from the other build | An Enhanced job in a Legacy playlist is dropped the same way |
| Name | 100 characters (101 returns NotAllowed/Profane). The game only keeps the first 63 bytes, since its scripts copy the name into 64-byte buffers. |
| Names containing "SCP" | Always refused as profane |
| Playlists per account | More than 22. I didn't look for the real cap. |
| Create rate | About 9 or 10 creates, then RateLimitExceeded for 2 to 3 minutes |
Measured
Playlist search
The website API has a playlist search that the website itself never calls (its playlist page is still the old one). It answers with no account and does more than the old page. Measured
GET /search/playlist?title=gtav&platform=pcalt&sort=plays&searchTerm=stunt&pageIndex=0&pageSize=30
| Parameter | Values |
|---|---|
platform | Required. A platform includes its older one: pcalt returns Legacy playlists too, ps5 returns ps4 ones. |
creatorRockstarId | One player's playlists. A player can only be named by Rockstar ID here. |
startDate, endDate | A date range, both needed. As in the job search, endDate is exclusive and a time of day counts. |
dateRange | today, last7, lastMonth, lastYear |
sort | date (newest, the default), plays, name. createdDate answers 500. |
filter | rockstar, scMembers (everyone but Rockstar), or crew with crewId |
searchTerm | Words in the name |
pageIndex, pageSize | Up to 30 per page, and nothing past the 50,000th result |
Each playlist in the answer already lists its jobs, with name, picture, plays and likes, so one call gives the jobs of up to 30 playlists. A playlist's own page is /ugc/playlist/details?title=gtav&contentId=<id>, also without an account.
Lobbies
Open job lobbies and free roam sessions aren't exposed by the website. They're advertised in ROS matchmaking, mm-gta5-prod.ros.rockstargames.com, and a search needs a ticket with privileges on the build.
Advertised attributes
| Attribute | Meaning |
|---|---|
| Owner | The host's Rockstar ID |
| Kind | Job lobby or free roam |
| Activity type | 0 mission, 1 deathmatch, 2 race, 3 survival. Special modes have their own values. |
| Activity id | joaat of the job id, as a signed 32-bit integer. Not the name and not the id itself. |
| Activity players | Free places, not players |
| Region | 1 South America, 2 US East, 3 Europe, 4 Asia, 5 Australia, 6 US West, 7 Japan |
| Language | 0 en, 1 fr, 2 de, 3 it, 4 es, 5 pt-BR, 6 pl, 7 ru, 8 ko, 9 zh-TW, 10 ja, 11 es-MX, 12 zh-CN |
| Discriminator | 32-bit value that must match exactly for players to meet |
Measured
- Players in a job lobby are the job's maximum (
gen.numin its details) minus the free places. In free roam another attribute looks like the player count (up to 32), not checked against a real session. Assumed - No call turns an activity id into a job. To name a lobby's job you hash known job ids until one matches. An index of every Enhanced job (about 3.5 million) named every open race lobby in a sample. Measured
Discriminator bits
| Bits | Meaning |
|---|---|
| 0 to 7 | Port |
| 8 to 11 | Version |
| 12 to 13 | Build |
| 14 | Bad sport pool |
| 15 | Matchmaking open. Off when the host closed the lobby, which stays advertised anyway. |
| 16 | In progress: the job has already started |
| 17 | Free aim. Free and assisted aim players never match. |
| 18 to 31 | Hash of runtime values of the game (file checksums, player pool) |
Read bit 15: Measured
Joining
Joining is done by the game client, peer to peer, and GTA V has no launch option that targets a job or a session. Read When a player starts a job, the game looks for an open lobby with the same activity id for about 60 seconds before hosting one. Read So starting a job from the bookmarks while a lobby of it is open should join that lobby, but I haven't seen it happen yet. Assumed
Profile picture
The profile picture is set through the in-game overlay's API, on scui.rockstargames.com. The overlay is a web page packed in scui.pak in the Social Club folder, and every call it makes is in its script. Read The calls are authorised with the ROS ticket itself, as Authorization: SCAUTH val="<ticket>", plus X-Title-Name: gta5, so like every ticket they end the account's GTA Online session. Measured
| Call | What it does |
|---|---|
POST /api/profile/GetCustomAvatars | The account's photos it may use as a picture (selfies and mugshots), each with i, cid (the photo's content id), src and t |
POST /api/profile/GetAvatars | Rockstar's stock avatars by game |
POST /api/profile/UpdateAvatar | Sets it: {"AvatarUrl":"","AvatarId":<i>,"AvatarContextId":"<cid>","CustomAvatar":true} for a photo, the stock avatar's i and CustomAvatar false otherwise |
Measured
The new picture was live within two minutes, at prod-avatars.akamaized.net/custom-avatars/n/<cid>.png (128 by 128, and /l/ for 256). The avatar is a fixed square cut from the photo, not its centre. Measured
A photo is uploaded the way the game does it, as ROS user content of type gta5photo: a create that answers with a CDN address and a short-lived upload token, the JPEG sent there, then a publish. Once it's on the account it shows up in GetCustomAvatars and can be set like any selfie. This needs write privilege on the build, like a playlist. Measured
Crew emblems
Emblems are made in the old website's editor (/emblems/new). An emblem is a stack of layers, each one of Rockstar's shapes placed with a position, scale, rotation, colour and so on, on a 512 by 512 canvas. These calls use the website's session, not a ticket, so they don't end a GTA Online session. Read
| Call | What it does |
|---|---|
POST /emblems/save | Saves an emblem to the account's library (up to 10). The answer lists the crews the account may publish to. |
POST /emblems/publish | {emblemId, crewId}: puts it in the crew's gallery. It does not make it the active emblem. |
GET /crew/emblems?crewId=<id> (scapi) | The crew's gallery, with who made each emblem |
POST /crew/activateEmblem?crewId=<id>&emblemId=<id> (scapi) | Makes a gallery emblem the active one. The parameters go in the query string, with a JSON body it answers 404. |
Measured
The old website's calls carry the page's anti-forgery token like its other calls (Old website). The active emblem's files on the content server change within seconds of the activation, and a new gallery emblem's pictures appear over about a minute. Measured
Every crew has a public folder on the content server, prod.cloud.rockstargames.com/crews/sc/<bucket>/<crew id>/publish/. The bucket is a djb2 hash of the crew id written in decimal, modulo 10,000, padded to four digits. emblem/emblem_<size>.png is the active emblem in 32, 64, 128, 256 and 512, and emblem_128.dds is the texture the game asks for. Measured
Old website
- Pages under
socialclub.rockstargames.com/games/gtav/<platform>/. Calls are the site's own routes, with the browser's cookies and, for most of them, the anti-forgery token each page carries (__RequestVerificationToken, sent back as a header). - The platform is session state: calls take no platform parameter and answer for the platform of the last page loaded.
- Career pages: a player's GTA Online statistics per platform, depending on the player's privacy settings.
Playlist search, the one the website still uses: Measured
GET /games/gtav/playlists/ajax/search?SearchOptSort=Played&SearchOptPublisher=&SearchOptDate=&TextQuery=stunt&page=1
| Parameter | Values |
|---|---|
SearchOptSort | Played, CreatedDate, Name |
SearchOptPublisher | Empty (any), me, friends, rockstar, members, crew<id> |
SearchOptDate | Empty, today, last7, lastMonth |
TextQuery | Free text |
Bookmarked | true |
page | 1, 2, … (20 per page) |
Each job in a result carries the same mission block as the job details, and meta.loc. The new API's playlist search does more: one player, any date range, and no account needed.
Glossary
- scapi
scapi.rockstargames.com, the API behind the Social Club website- ROS
- Rockstar Online Services, the game's back end (
*.ros.rockstargames.com) - scui
- The in-game Social Club overlay and its API
rsso- The website sign-in's session cookie
- BearerToken
- The website API's five-minute token
- ScAuthToken
- The launcher sign-in's token, which mints ROS tickets
- Ticket
- A ROS session: one day, one per account
- UGC
- User-generated content: jobs, playlists, photos
- Content id
- A job's id, shared by the website, playlists and the content server
- Rockstar ID
- An account's number
pc/pcalt- Website names of Legacy and Enhanced
pcros/pcrosalt- ROS names of Legacy and Enhanced
meta.loc- The map zones a job is filed under, written by the game when it's published
- joaat
- The game's string hash (Jenkins one-at-a-time, lower-cased input)
- Discriminator
- Session value that must match for players to meet
X-AMC- "Allow mature content" header, sent by the website when signed out
If you find something wrong here, tell me on the Discord server.