Grindr API client for Rust https://opengrind.org/grindr-api/
Find a file
2026-06-02 01:09:28 +02:00
.vscode Initial commit 2026-06-02 01:09:28 +02:00
examples Initial commit 2026-06-02 01:09:28 +02:00
src Initial commit 2026-06-02 01:09:28 +02:00
.gitignore Initial commit 2026-06-02 01:09:28 +02:00
Cargo.lock Initial commit 2026-06-02 01:09:28 +02:00
Cargo.toml Initial commit 2026-06-02 01:09:28 +02:00
LICENSE Initial commit 2026-06-02 01:09:28 +02:00
README.md Initial commit 2026-06-02 01:09:28 +02:00

grindr.rs

Unofficial async Rust client for the Grindr API, powering Open Grind client.

Important

This is an unofficial library, not affiliated with or endorsed by Grindr. It is provided for research and interoperability. Automating access may violate Grindr's Terms of Service. You are responsible for how you use it.

Features

  • Async, clonable client built on tokio and wreq
  • Fingerprint matching Grindr's official Android APK's network lib: TLS (JA3/JA4), HTTP/2 (frames, pseudoheaders), required headers
  • Session handling — tokens are refreshed automatically
  • Background WebSocket with automatic reconnect and states callback
  • Device identities spoofing — store DeviceInfo along session token to decrease the chance of triggering Cloudflare block pages

This crate is a transport: it handles authentication, fingerprinting, connection, but does not ship typed models for every endpoint. You choose the path and deserialize the body yourself.

Installation

[dependencies]
grindr = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] }
serde_json = "1"

Versioning

This crate's version is <lib version>+<Grindr APK version>. For example, 0.1.0+26.9.1.163471 is library 0.1.0 targeting APK 26.9.1.163471. The +<apk> suffix is SemVer build metadata: informational only, and ignored by Cargo when resolving versions. Retargeting the APK is treated as a breaking change, so it bumps the minor, requiring manual upgrade. The targeted version is also exposed as grindr::APP_VERSION.

Quick start

use grindr::{DeviceInfo, GrindrClient, Method};

#[tokio::main]
async fn main() -> Result<(), grindr::GrindrError> {
    let device = DeviceInfo::generate();
    let client = GrindrClient::new(device, None)?;

    let me = client.login("m@example.com", "yourpassword").await?;
    println!("logged in as profile {}", me.profile_id);

    // URL must start with `/`
    // Session token is added automatically
    // API reference: <https://opengrind.org/grindr-api/>
    // Dev tool: <https://git.opengrind.org/open-grind/grindr-api-dev-tool>
    let resp = client
        .request_authenticated_raw(Method::GET, "/v3/me/profile", None)
        .await?;
    println!("status {}", resp.status);
    let profile: serde_json::Value = serde_json::from_slice(&resp.body).unwrap();
    println!("{profile:#?}");

    Ok(())
}

Sessions & device identity

// Load `device` and `saved` from disk
let client = GrindrClient::new(device, saved)?;

// Persist session whenever it changes
let mut sessions = client.session_receiver();
tokio::spawn(async move {
    while sessions.changed().await.is_ok() {
        // Option<Session>
        let current = sessions.borrow().clone();
        // Serialize to disk and store securely
        save_session(&current);
    }
});

WebSocket

The client maintains a background WebSocket once a session exists. Subscribe to events and send commands:

use grindr::WsCommand;

// Subscribe to events
let mut events = client.ws_receiver();
tokio::spawn(async move {
    while let Ok(event) = events.recv().await {
        println!("event {}: {}", event.event_type, event.payload);
    }
});

// Send a command
client
    .ws_sender()
    .send(WsCommand {
        r#type: "chat.v1.typing".to_owned(),
        ref_id: "1".to_owned(),
        payload: serde_json::json!({ "conversationId": "abc" }),
    })
    .await
    .ok();

The async background task starts on the first authenticated call. When you resume a stored session and want the socket up before issuing any request, call client.connect().await explicitly. Watch the connection with GrindrClient::connection_state, and observe failed background token refreshes via GrindrClient::auth_event_receiver.

API reference

Full generated docs: https://docs.rs/grindr.

GrindrClient

Method Description
new(device, session) -> Result<Self> Create a client, optionally resuming a stored Session
login(email, password) -> Result<LoginResult> Email + password login
google_sign_in(access_token) -> Result<LoginResult> Google OAuth sign-in
refresh_token() -> Result<LoginResult> Force token refresh
logout() Clear the session and disconnect the websocket
request_authenticated_raw(method, path, body) -> Result<RawResponse> Authenticated API call returning the raw status + body
rotate_device(device) -> Result<DeviceInfo> Set the device identity in place, keeping the session; returns old device info
current_device() -> DeviceInfo Get the device identity currently in use
recaptcha_first_party_enabled() -> Result<bool> Check whether first-party reCAPTCHA is enabled
session_receiver() -> watch::Receiver<Option<Session>> Watch the current session (updates on login/refresh/logout)
connection_state() -> watch::Receiver<WsConnectionState> Watch the websocket connection state
ws_receiver() -> broadcast::Receiver<WsEvent> Subscribe to websocket events
ws_sender() -> mpsc::Sender<WsCommand> Get websocket sender for commands
connect() Start the background websocket now (e.g. on a resumed session)
auth_event_receiver() -> broadcast::Receiver<AuthEvent> Subscribe to background token refresh failures

Types

  • DeviceInfo — device identity, build with DeviceInfo::generate() or DeviceInfo::default()
  • Session — session token and other secrets
  • SessionKindEmail or Google
  • LoginResult{ profile_id }, returned by the auth methods
  • RawResponse{ status: u16, body: Vec<u8> }
  • WsCommand — websocket command { type, ref_id, payload }
  • WsEvent — websocket event { event_type, payload }
  • WsConnectionStateConnected / Disconnected
  • AuthEvent{ message, unauthorized } from background refreshes
  • GrindrError — the crate error type (Http, Auth, Api, Unauthorized, InvalidRequest)
  • Method — re-exported wreq::Method for request_authenticated_raw

Low-level helpers

For building your own wreq::Client with an identical fingerprint:

  • probe_emulation() -> wreq::EmulationProvider — tls/http2 emulation profile
  • build_user_agent(device, tier) -> StringUser-Agent value
  • build_device_info_header(device) -> StringL-Device-Info value
  • GrindrHeaders::build(device, ua, authorization, roles) — full and correctly ordered headers list

Examples

Observe TLS session resumption:

cargo run --example warm_probe

Assert the emulated fingerprint:

cargo run --example fingerprint_check

The fingerprint_check example verifies JA3/JA4, the Akamai http/2 fingerprint and header ordering against tls.peet.ws. Pass --all flag to check both http/2 and http/1.1 (websocket) clients.

Minimum supported Rust version

Rust 1.80.

License

MIT