POST
/
api
/
v1
/
auth
/
oauth
/
google
Sign in with Google
curl --request POST \
  --url https://api.example.com/api/v1/auth/oauth/google
import requests

url = "https://api.example.com/api/v1/auth/oauth/google"

response = requests.post(url)

print(response.text)
const options = {method: 'POST'};

fetch('https://api.example.com/api/v1/auth/oauth/google', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/auth/oauth/google",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
package main

import (
"fmt"
"net/http"
"io"
)

func main() {

url := "https://api.example.com/api/v1/auth/oauth/google"

req, _ := http.NewRequest("POST", url, nil)

res, _ := http.DefaultClient.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/auth/oauth/google")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.example.com/api/v1/auth/oauth/google")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)

response = http.request(request)
puts response.read_body

Overview

Signs a user in (or registers them on first use) with their Google account. The mobile app performs native Google sign-in via the Firebase Auth SDK, obtains a Firebase ID token, and POSTs that token here. The backend verifies the token with firebase-admin — checking the signature against Google’s rotating public keys, the expiry, the issuer, and that the token’s audience matches our Firebase project — then returns a Swappr access + refresh token pair, exactly like /login. There are three outcomes:
  • First-time Google user → a new account is created (emailVerified mirrors the token’s email_verified claim, firstName / lastName start null, consent flags start false). The response is 201 Created with isNewUser: true. The client should route the user into onboarding.
  • Returning Google user (same Google identity) → 200 OK with isNewUser: false.
  • Email already owned by a non-Google account409 ACCOUNT_EXISTS_VIA_OAUTH. Swappr does not silently attach Google to a pre-existing email/password (or Apple) account — that would allow account takeover by anyone able to mint a Google token for that address. The user must sign in with their original method and link Google deliberately from settings (account linking is future work).
New Google users flow into the same onboarding state machine as email users; use isNewUser (or onboardingStep) to decide whether to show onboarding.

Authentication

None required. The Firebase ID token in the body is the credential.

Path parameters

None.

Query parameters

None.

Request body

FieldTypeRequiredAllowed valuesExample
idTokenstringyesA Firebase ID token (signed JWT) from the app’s native Google sign-in. 1..8192 chars.eyJhbGciOiJSUzI1NiIsImtpZCI6...

Example payload

{
  "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE2N..."
}

Response — 201 Created (new user) / 200 OK (returning user)

FieldTypeNotesExample
accessTokenstringShort-lived RS256 JWT (15 min). Send as Authorization: Bearer <token>.eyJhbGci...
refreshTokenstringOpaque 256-bit token. Exchange via /refresh.r8Kf...
isNewUserbooleantrue when this call created the account (HTTP 201), false for a returning user (HTTP 200).true
user.idstringThe user’s ID.usr_01HZQ7K3M4N5P6Q7R8S9T0V1W2
user.emailstringThe verified Google email (lowercased).alice@gmail.com
user.firstNamestring | nullnull until set during onboarding / profile edit.null
user.lastNamestring | nullnull until set.null
user.avatarUrlstring | nullnull until set.null
user.onboardingStepstringCurrent onboarding step; first step for a brand-new user.verify_tenancy
user.tenancyStatusstringTenancy verification status.not_submitted
user.subscriptionStatusstringSubscription state.free_launch

Example response — 201

{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "r8Kf3n0pQv...",
  "isNewUser": true,
  "user": {
    "id": "usr_01HZQ7K3M4N5P6Q7R8S9T0V1W2",
    "email": "alice@gmail.com",
    "firstName": null,
    "lastName": null,
    "avatarUrl": null,
    "onboardingStep": "verify_tenancy",
    "tenancyStatus": "not_submitted",
    "subscriptionStatus": "free_launch"
  }
}

Error responses

StatusCodeMeaning
400VALIDATION_FAILEDidToken missing, blank, or longer than 8192 chars.
401OAUTH_INVALID_TOKENToken failed verification — malformed, expired, wrong signature, or minted for a different Firebase project; or it carried no email.
403ACCOUNT_BANNEDThe matched account is banned.
409ACCOUNT_EXISTS_VIA_OAUTHThe email already belongs to a non-Google account. Sign in with the original method, then link Google.
503OAUTH_NOT_CONFIGUREDFirebase credentials (FCM_SERVICE_ACCOUNT_JSON + FIREBASE_PROJECT_ID) are not set in this environment.

Example error — 401

{
  "type": "https://api.swappr.co.uk/errors/oauth-invalid-token",
  "title": "OAuth token is invalid",
  "status": 401,
  "code": "OAUTH_INVALID_TOKEN",
  "detail": "ID token verification failed: Firebase ID token has expired",
  "instance": "/api/v1/auth/oauth/google",
  "requestId": "01HZQ7K3M4N5P6Q7R8S9T0V1W2"
}

Side effects

  • First-time sign-in only: inserts a new row into the users collection with oauthProvider: 'GOOGLE', oauthId set to the Firebase uid, passwordHash: null, and emailVerified taken from the token.
  • Issues a new session: inserts a refresh_tokens row and signs an access JWT.
  • No email is sent (no OTP needed — Google has already verified the address).

Rate limiting

This endpoint shares the auth burst limiter (10 requests / minute / IP) with /login, on top of the router-level /auth/* cap.

Notes for the client

  1. Configure Firebase in the app with the bundled google-services.json (Android) / GoogleService-Info.plist (iOS) for project swappr-de6ce, bundle id com.swappr.app.
  2. Do native Google sign-in (Firebase Auth GoogleAuthProvider), then read the ID token (user.getIdToken()), not the access token.
  3. POST { idToken } here, store the returned Swappr accessToken / refreshToken, and proceed exactly as with email login.

See also

curl

curl -X POST https://api.swappr.co.uk/api/v1/auth/oauth/google \
  -H "Content-Type: application/json" \
  -d '{
    "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE2N..."
  }'