Authentication
Sign in
Make sure you're passing all Security headers or you might stumble upon {"code":28,"message":"ACCOUNT_BANNED","profileId":null,"type":1,"reason":null,"isBanAutomated":true,"thirdPartyUserIdToShow":null,"banSubReason":null} — but don't fret — it's a fake error, your account isn't banned and the API simply blocked your request, not the account. Endpoint selection depends on the recaptcha_first_party assignment: when disabled (default) sign-in uses /v8/sessions; when enabled the client uses POST /v9/sessions with an extra captchaToken, sharing the same body otherwise.
POST /v8/sessionsBody:
email— string with emailpassword— string with password, don't specify if usingauthTokenauthToken— string obtained from login+password flow ornulltoken— FCM (push service) string ornullgeohash— geohash string ornull
Response:
profileId— string with numbers, account's IDsessionId— Session IDauthToken— string, Auth token for session refresh
Errors:
ACCOUNT_BANNED— could be malformed requestINVALID_INPUT_PARAMETERS— incorrect credentials
Sign in via reCAPTCHA
reCAPTCHA variant of POST /v8/sessions. The client uses this endpoint instead of /v8/sessions only when the recaptcha_first_party assignment is enabled. That assignment is sent by the server and resolved before login. The body is identical to SessionCreateRequest plus a captchaToken (a reCAPTCHA Enterprise token for the login action). The official client always populates captchaToken here and aborts locally if it can't obtain one, so a null/absent token never reaches this endpoint from the app.
POST /v9/sessionsBody:
Response:
profileId— string with numbers, account's IDsessionId— Session IDauthToken— string, Auth token for session refresh
Errors:
ACCOUNT_BANNED— could be malformed requestINVALID_INPUT_PARAMETERS— incorrect credentials; on this endpoint a rejectedcaptchaToken(e.g. expired or low-score) is also a plausible cause — server behavior, not confirmed from the client
Login via third party, WIP
NOTE
This endpoint hasn't been researched yet
Single endpoint used for both initial third-party sign-in and third-party session refresh, distinguished by the request body shape (oneOf).
Facebook (vendor 1): the Facebook user access token (scopes public_profile and email, app id 1273378622718674) is sent here verbatim as thirdPartyToken (ThirdPartyRequest). There is no exchange step, and nothing else from the Facebook session — user id, signed_request, id_token, granted scopes, expiry — is forwarded. See Facebook login dialog for how that token is obtained.
Google (vendor 2): a Google server auth code obtained on-device is exchanged for a Google access_token via Exchange Google auth code (POST /v3/users/thirdparty/exchange), and that token is sent here as thirdPartyToken.
When registered is true, authenticationResponse carries the session. When registered is false, the identity has no account yet: the server resolves the provider identity from the token on its own and returns it as thirdPartyUserInfo, and sign-up continues at Create third party account.
A third outcome is possible on this call and on the account-creation calls: HTTP 403 with ERR_VERIFICATION_REQUIRED (Grindr error code 30) and a profile id in the error body. That profile must pass phone verification (POST /v4/sms/verification/{profileId}/sendcode then /verifycode, see SMS verification) before a session can be obtained.
Session refresh reuses this same URL with ThirdPartySessionRequest ({ thirdPartyUserId, authToken, geohash }): the provider token is not re-sent — only the stable thirdPartyUserId and the rolling Grindr authToken. The response is the same ThirdPartyAuthResponse, yielding a fresh sessionId (JWT) and a rotated authToken.
There is no reCAPTCHA variant of this endpoint and no captchaToken field: the recaptcha_first_party assignment only switches Sign in between /v8/sessions and /v9/sessions, and recaptcha_third_party only affects account creation.
POST /v8/sessions/thirdpartyBody:
One of:
Response:
registered— boolean,trueif a Grindr account is already linked to this provider identity.profileId— string with numbers, the linked account's ID, ornullwhenregisteredisfalsethirdPartyUserInfo— ThirdPartyUserInfo, provider identity used to pre-fill account creation whenregisteredisfalse, ornullauthenticationResponse— ThirdPartyAuthenticationResponse, session payload, present whenregisteredistrue, otherwisenull
Exchange Google auth code, WIP
NOTE
This endpoint hasn't been researched yet
First step of the Google sign-in flow. A Google server auth code, requested on-device with requestOfflineAccess against server client id 1036042917246-68g7siev1clho9lhqevahao9hlbpvssf.apps.googleusercontent.com, is sent here. The backend holds the matching web-client secret, performs the authorization-code → token exchange with Google, and returns the Google OAuth tokens. The access_token then goes to Login via third party as thirdPartyToken. Session refreshes do not repeat this step.
Google only. Facebook sign-in has no counterpart to this endpoint — the Facebook SDK yields a user access token directly on-device, and it goes straight to Login via third party with thirdPartyVendor: 1.
POST /v3/users/thirdparty/exchangeBody:
code— string, Google OAuth 2.0 server auth code (serverAuthCodefrom the Google Sign-In result).
Response:
access_token— string, Google OAuth access token. Used asthirdPartyTokeninPOST /v8/sessions/thirdparty.token_type— string, OAuth token type, e.g."Bearer".id_token— Google OpenID Connect ID token (JWT).expires_in— long integer, Access token lifetime in seconds.
SessionCreateRequest
email— string with emailpassword— string with password, don't specify if usingauthTokenauthToken— string obtained from login+password flow ornulltoken— FCM (push service) string ornullgeohash— geohash string ornull
SessionCreateRequestV9
Request body for Sign in via reCAPTCHA (POST /v9/sessions). Identical to SessionCreateRequest plus one extra field, captchaToken. The client uses this shape only when the recaptcha_first_party assignment is enabled for the device.
- everything from SessionCreateRequest
captchaToken— string, reCAPTCHA Enterprise token for theFirstPartyLoginaction. Non-null for the email + password sign-in;nullfor theauthTokensession-refresh flow. The token is only fetched by the device when therecaptcha_first_partyassignment is enabled.
AccountBannedError
Error body from sign-in / session refresh when the account, device, or network is restricted. code 28 (ACCOUNT_BANNED) is often a false positive from missing/invalid security headers, not a real ban.
code— integermessage— stringprofileId— string ornull, optionaltype— integer ornull, optionalreason— BanReason ornull, optionalbanSubReason— BanSubReason ornull, optionalisBanAutomated— boolean ornull, optionalthirdPartyUserIdToShow— string ornull, optionalmail— string ornull, optionalphoneNumber— string ornull, optionaldialCode— string ornull, optional
BanReason
Account ban reason (timed-ban classification), from the JWT banDetails.reason claim. Unknown values map to Safety & Privacy on the client.
"Harassment""Impersonation""Underage User""Seeking Underage / Child Endangerment""Underage Impersonation / Vigilante Account""Commercial Use / Advertising / Promotion""Sexual Solicitation""Illegal Drug Related Content""Drug Selling""Hate Speech / Discrimination / Hateful Content""Serious Offline Incident""Scam / Spam""Underage - 12 and under""Safety & Privacy""TRUST_VENDOR_REJECTED"
BanSubReason
Account ban sub-reason, from the JWT banDetails.subReason claim and the sign-in ban error banSubReason. Unknown values map to Other on the client.
"UNDERAGE_ACCESS""UNDERAGE_IMPERSONATION""CHILD_ENDANGERMENT""UNWANTED_MESSAGING""PROFILE_SPAM""PHISHING_AND_FRAUD""BOT_ACTIVITY""DRUG_SALES""FREQUENT_DRUG_SOLICITATION""OCCASIONAL_DRUG_SOLICITATION""DRUG_PROMOTION""NON_ILLICIT_SUBSTANCES""PAID_SEXUAL_SERVICES""MILD_SOLICITATION""SALE_OF_SEXUAL_CONTENT""MILD_INSULTS""BULLYING""HATE_SPEECH""BLACKMAIL_AND_THREATS""PRIVACY_VIOLATIONS""IMPERSONATION""CONSENT_VIOLATIONS""OFFLINE_INCIDENTS""GENERAL_ADVERTISING""BUSINESS_SOLICITATION""TRANSACTIONAL_SERVICES""SCAMS_AND_PROMOTIONS""DRUG_SEEKING""DRUG_FACILITATION""NON_CONSENSUAL_INTIMATE_IMAGERY""SEXTORTION""BLOCK_DECISION""Other"
AuthSessionResponse
profileId— string with numbers, account's IDsessionId— Session IDauthToken— string, Auth token for session refresh
GoogleAccessTokenRequest
Request body for Exchange Google auth code (POST /v3/users/thirdparty/exchange). Wraps the Google server auth code obtained on-device from native Google Sign-In.
code— string, Google OAuth 2.0 server auth code (serverAuthCodefrom the Google Sign-In result).
GoogleAccessTokenResponse
Response from Exchange Google auth code. Standard Google OAuth 2.0 token payload returned after the backend exchanges the server auth code. Only access_token is needed; it goes to Login via third party as thirdPartyToken.
access_token— string, Google OAuth access token. Used asthirdPartyTokeninPOST /v8/sessions/thirdparty.token_type— string, OAuth token type, e.g."Bearer".id_token— Google OpenID Connect ID token (JWT).expires_in— long integer, Access token lifetime in seconds.
ThirdPartyRequest
Request body for the initial Login via third party (POST /v8/sessions/thirdparty). For Google, thirdPartyToken is the access_token from Exchange Google auth code; for Facebook it is the user access token the Facebook SDK produced on-device. A null geohash is omitted from the body rather than sent as null.
thirdPartyVendor— integer,1— Facebook,2— GooglethirdPartyToken— string, provider access token: a Facebook user access token for vendor1, a Googleaccess_tokenfor vendor2. Sent verbatimgeohash— geohash string ornull, optional
ThirdPartySessionRequest
Request body for session refresh via third party, sent to the same POST /v8/sessions/thirdparty endpoint as the initial login. Used once a thirdPartyUserId is stored (account created/logged in via Google or Facebook). The provider token is not re-sent — only the stable thirdPartyUserId plus the stored Grindr authToken.
thirdPartyUserId— string, Stable provider user id from the previous login (authenticationResponse.thirdPartyUserId).authToken— string, GrindrauthTokenfrom the previous session (rotated on every refresh).geohash— geohash string ornull, optional
ThirdPartyAuthenticationResponse
Session payload nested in ThirdPartyAuthResponse.authenticationResponse; present when registered is true. sessionId is the same JWT as in AuthSessionResponse.
profileId— string, Account ID.sessionId— Session IDauthToken— string, Auth token used for session refresh.thirdPartyUserId— string, Stable provider user id; persisted and re-sent asthirdPartyUserIdon refresh.thirdPartyUserIdToShow— Display variant of the provider user id (e.g. email/handle).
ThirdPartyAuthResponse
Response from Login via third party and from third-party session refresh. When registered is true, authenticationResponse carries the session. When registered is false, no account is linked yet and sign-up continues at Create third party account using thirdPartyUserInfo.
registered— boolean,trueif a Grindr account is already linked to this provider identity.profileId— string with numbers, the linked account's ID, ornullwhenregisteredisfalse, optionalthirdPartyUserInfo— ThirdPartyUserInfo, provider identity used to pre-fill account creation whenregisteredisfalse, ornull, optionalauthenticationResponse— ThirdPartyAuthenticationResponse, session payload, present whenregisteredistrue, otherwisenull, optional
ThirdPartyUserInfo
Provider identity carried by Login via third party when registered is false. Grindr's backend resolves it from the provider token server-side, which is why a bare Facebook access token is enough to reach the sign-up flow. Every field except id may come back null.
id— string with the provider user id, prefixed with the vendor name, e.g.facebook122104356681457161. It is this prefixed form that is echoed back asthirdPartyUserIdon account creation, persisted, and re-sent on every session refresh, optionalname— string with the provider display name, ornull. Not echoed on account creation; it pre-fills thedisplayNamesubmitted on the profile update that follows sign-up, optionalemail— string with email from the provider, ornull. Pre-fills the sign-up form and is sent back asemailon account creation, optionalage— integer, age in whole years, ornull. Overwritten during sign-up by an age computed from the birthday collected there, optionalidentity— object with provider-supplied identity details, ornull. Shape unverified; the matching client model holdspronounsCategory(integer) andpronounsDisplay(string), optionalphotoUrl— string with the provider profile picture, ornull, optionalaccessToken— string, the provider access token echoed back, ornull, optional
Session ID
JWT obtained from authentication flow. Decoded JWT content:
Headers claims structure:
kid— key IDalg—"RS256"typ—"JWT"
Payload claims:
exp— number, unix timestamp in seconds defining token expiration dateprofileId— string with numbers, account's IDroles— unknown array, appears to be emptyfeatures— array of strings, e.g."HidePremiumStore","CreateVideoCall","VideoCallDailyFree"featureFlags— array of strings, e.g."profile-insights","online-until-updates","enable-account-filters-bulk-exposure","a-list-v3","discover-v2","boost_purchase_fixes","discover-studfinder-upsell"experiments— object with experiment name → variant string, known keys:explore-paywall-profiles— string, e.g."test"limiting_chat_credits_from_unlimited_to_5_for_xtra_users_in_explore— string, e.g."treatment"validation-aa-backend-profileid— string, e.g."test"grindr-core-day-pass— string, e.g."control"llm-age-verification-methods— string, e.g."variant_b"trans_tribe_filtering_changes— string, e.g."Test"green_dot_v2— string, e.g."treatment-1"sponsored-profiles-cascade-selection-mode— string, e.g."treatment"read-receipt-ad-reward— string, e.g."variant_2"taps-paywall— string, e.g."treatment-1"explore-insertables-v1— string, e.g."treatment-insertables-below-mpu"reduce_number_of_free_profiles_from_99_to_90— string, e.g."test"cascade-mpu-explore-studfinder-2026-03-12— string, e.g."control"reduce_number_of_results_with_age_filter_for_free_users— string, e.g."test"mpu-heuristic-algorithm-optimizations-q125— string, e.g."control"ships_in_the_night_v3— string, e.g."treatment"cascade-mpu-disable-poc-heuristic-2026-03-02— string, e.g."treatment"for-you-recsys-v1_1— string, e.g."control"cascade-mpu-studfinder-profile-limit-2026-03-12— string, e.g."treatment"reduce_free_user_results_with_any_filter— string, e.g."control"cascade-mpu-studfinder-unlimited-2026-02-02— string, e.g."treatment"mpu-rest-of-world— string, e.g."treatment"top_18_mpu_profiles_are_online_in_extended_cascade_and_explore— string, e.g."control"for-you-v2— string, e.g."Test"rewarded-ads-viewed-me-v2— string, e.g."variant_1"mega-boost-v1— string, e.g."mega-boost-low"
systemTime— unix timestamp in millisecondsupsells— unknown object, appears to be emptyrestriction— string ornull; account restriction type, one of"AGE_RESTRICTED","TIMED_BAN","TRUST_VENDOR_REJECTED"; absent/nullwhen unrestrictedrestrictionReason— string ornull; forAGE_RESTRICTEDone of"UK_VERIFICATION_REQUIRED","BR_VERIFICATION_REQUIRED","AU_VERIFICATION_REQUIRED","US_VERIFICATION_REQUIRED"(calls for an app store age signal, see Submit store age verification result),"BANNED_USER","VERIFICATION_REQUESTED"; forTRUST_VENDOR_REJECTEDa free-form reasonbanDetails— object, present whenrestrictionis"TIMED_BAN":expiryTime(unix seconds),reason(seeBanReason),subReason(seeBanSubReason),isAutomated,isAgreementCompleted,systemTime(unix ms)grit— unknown UUIDv4 string