Used by thousands of students worldwide - help us reach 150 members
Users & Authentication
/userAccount creation, email/password login, Google sign-in (browser redirect flow and mobile ID-token flow), password reset, and the current user's profile and profile picture. Successful logins return a JWT that must be sent as `Authorization: Bearer <token>` on authenticated requests.
/user/signupCreate an account with username, email and passwordPOST/user/loginLog in with username or email and passwordGET/userGet the current user's profilePUT/user/profile-pictureUpload or replace the current user's profile pictureDELETE/user/profile-pictureRemove the current user's uploaded profile picturePOST/user/forgot-passwordRequest a password reset emailPOST/user/reset-passwordSet a new password using a reset tokenGET/user/auth/googleStart the Google OAuth sign-in redirect flowGET/user/auth/google/callbackGoogle OAuth callback that completes sign-inGET/user/auth/google/linkStart a Google OAuth redirect for a logged-in userPOST/user/auth/google/unlinkUnlink the Google account from the current userPOST/user/auth/google/mobileSign in with a Google ID token (mobile)Create an account with username, email and password
Creates a new local account. The request is rejected if another user already has the same username or the same email. A Stripe customer is created for the new user and its id is stored as `stripeCustomerId`. New users receive the default `member` role. If `referralCode` matches an active affiliate code, a referral record is created for the new user (a failure to record the referral never blocks signup). A welcome email is sent asynchronously after the response. The response contains a JWT for immediate use.
Body parameters
usernamerequiredstring | Desired username. Must be unique. |
emailrequiredstring | Email address. Must be unique. |
passwordrequiredstring | Plain-text password; it is hashed with bcrypt before being stored. |
referralCodestring | Affiliate referral code. Ignored if it does not match an active affiliate. |
turnstileTokenrequiredstring | Cloudflare Turnstile token from the challenge widget on the Sharh signup page. Tokens are single-use and expire after 5 minutes. Email sign-up is CAPTCHA-protected, so it cannot be scripted; Google sign-in is not challenged. |
Errors
400JSON `{"error":"CAPTCHA_REQUIRED","message":"...","signupUrl":"..."}` when turnstileToken is missing; plain text `Bad Request: Missing required fields` when username, password or email is missing; or `Bad Request: User with the same username or email already exists`.403JSON `{"error":"CAPTCHA_FAILED","message":"..."}` — the Turnstile token was invalid, expired or already used.503JSON `{"error":"CAPTCHA_UNAVAILABLE","message":"..."}` — the token could not be verified with Cloudflare; no account is created. Retry shortly.500Plain text `Internal Server Error` (database or Stripe customer creation failure).
curl -X POST "https://app.ummahspot.com/user/signup" \
-H "Content-Type: application/json" \
-d '{"username":"string","email":"string","password":"string","turnstileToken":"string"}'{
"token": "eyJhbGciOi...",
"user": {
"id": "665f1c2e9b1d4a0012ab34cd",
"username": "ibnrushd",
"email": "ibnrushd@example.com",
"stripeCustomerId": "cus_XXXXXXXXXXXXXX",
"roles": ["member"],
"profilePicture": null
}
}Log in with username or email and password
Looks the user up by `identifier`, which is matched against both the username and the email fields, then verifies the password. On success returns a JWT plus a summary of the user. `profilePicture` is a presigned S3 URL (valid for 1 hour) when the user has uploaded a picture, otherwise the picture URL from their Google account, otherwise `null`.
Body parameters
identifierrequiredstring | The account's username or email address. |
passwordrequiredstring | The account password. |
Errors
400Plain text `Bad Request: Missing required fields` when identifier or password is missing.401Plain text `Unauthorized: Incorrect credentials` when no user matches the identifier or the password is wrong.500Plain text `Internal Server Error`.
curl -X POST "https://app.ummahspot.com/user/login" \
-H "Content-Type: application/json" \
-d '{"identifier":"string","password":"string"}'{
"token": "eyJhbGciOi...",
"user": {
"id": "665f1c2e9b1d4a0012ab34cd",
"username": "ibnrushd",
"email": "ibnrushd@example.com",
"stripeCustomerId": "cus_XXXXXXXXXXXXXX",
"roles": ["member"],
"profilePicture": "https://<bucket>.s3.amazonaws.com/profile-pictures/665f1c2e9b1d4a0012ab34cd-<uuid>.jpg?X-Amz-Signature=..."
}
}Get the current user's profile
Returns the full user document of the authenticated user with the `password` field removed. If the user has an uploaded profile picture (`profilePictureKey`), `profilePicture` is replaced with a presigned S3 URL valid for 1 hour; otherwise it keeps the URL from the Google account, if any.
Errors
401Plain text `Unauthorized: No token provided`.403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.404Plain text `User not found`.500Plain text `Internal Server Error`.
curl -X GET "https://app.ummahspot.com/user" \
-H "Authorization: Bearer $SHARH_TOKEN"{
"_id": "665f1c2e9b1d4a0012ab34cd",
"username": "ibnrushd",
"email": "ibnrushd@example.com",
"stripeCustomerId": "cus_XXXXXXXXXXXXXX",
"roles": ["member"],
"createdAt": "2025-01-12T09:30:00.000Z",
"authMethod": "local",
"profilePictureKey": "profile-pictures/665f1c2e9b1d4a0012ab34cd-<uuid>.jpg",
"profilePicture": "https://<bucket>.s3.amazonaws.com/profile-pictures/665f1c2e9b1d4a0012ab34cd-<uuid>.jpg?X-Amz-Signature=...",
"mobileSubscription": {
"platform": null,
"productId": "",
"transactionId": "",
"originalTransactionId": "",
"status": "none",
"isActive": false,
"expiresAt": null,
"environment": "",
"lastValidatedAt": null,
"source": ""
},
"followersCount": 0,
"followingCount": 0,
"creatorProfile": {
"status": "none",
"tagline": "",
"bio": "",
"publicationTitle": "",
"slug": "",
"coverImage": "",
"bannerText": "",
"featuredQuote": "",
"socialLinks": {
"website": "",
"x": "",
"youtube": "",
"instagram": ""
},
"approvedAt": null,
"approvedBy": null,
"customPlatformFeePercent": null,
"stripeConnectAccountId": "",
"stripeConnectOnboarded": false,
"stripeChargesEnabled": false,
"stripePayoutsEnabled": false
},
"__v": 0
}Upload or replace the current user's profile picture
Uploads an image to S3 under `profile-pictures/` and stores its key on the user. If the user already had an uploaded picture, the old S3 object is deleted. The response contains a presigned URL for the new picture, valid for 1 hour. Only JPEG, PNG, GIF and WebP images up to 5 MB are accepted.
Send the body as multipart/form-data.
Body parameters
profilePicturerequiredfile | The image file. Allowed MIME types: image/jpeg, image/png, image/gif, image/webp. Maximum size 5 MB. |
Errors
400JSON `{ "error": "No image file provided" }` when the `profilePicture` field is missing.401Plain text `Unauthorized: No token provided`.403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.404User not found.500JSON `{ "error": "Failed to upload profile picture" }` on S3/database failure. A file with a disallowed type or larger than 5 MB is rejected by the upload middleware and surfaces as a plain-text 500 from the global error handler.
curl -X PUT "https://app.ummahspot.com/user/profile-picture" \
-H "Authorization: Bearer $SHARH_TOKEN" \
-F "profilePicture=@/path/to/file"{
"profilePicture": "https://<bucket>.s3.amazonaws.com/profile-pictures/665f1c2e9b1d4a0012ab34cd-<uuid>.png?X-Amz-Signature=..."
}Remove the current user's uploaded profile picture
Deletes the uploaded picture from S3 (if there is one) and clears it from the user. The response returns the picture the account falls back to: the Google account picture URL if the user has one, otherwise `null`.
Errors
401Plain text `Unauthorized: No token provided`.403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.404User not found.500JSON `{ "error": "Failed to delete profile picture" }`.
curl -X DELETE "https://app.ummahspot.com/user/profile-picture" \
-H "Authorization: Bearer $SHARH_TOKEN"{
"profilePicture": null
}Request a password reset email
Looks up the account by the lower-cased email. If one exists, a reset token valid for 1 hour is generated, stored on the user, and emailed as a link of the form `https://sharhapp.com/reset-password?token=<token>`. The response is identical whether or not an account exists, so the endpoint does not reveal which emails are registered.
Body parameters
emailrequiredstring | Email address of the account. Must contain an `@`. |
Errors
400JSON `{ "error": "Valid email address is required" }` when email is missing or has no `@`.500JSON `{ "error": "Internal Server Error" }`.
curl -X POST "https://app.ummahspot.com/user/forgot-password" \
-H "Content-Type: application/json" \
-d '{"email":"string"}'{
"message": "If an account with that email exists, a password reset link has been sent"
}Set a new password using a reset token
Verifies the reset token from the password reset email. The token must be a valid, unexpired password-reset token, must match the token currently stored on the user, and the stored expiry must not have passed. On success the password is replaced and the stored token is cleared, so each token can be used only once.
Body parameters
tokenrequiredstring | The `token` query parameter from the reset link sent by email. |
newPasswordrequiredstring | The new password. |
Errors
400JSON `{ "error": "..." }` with one of: `Token and new password are required`, `Invalid or expired token`, `Invalid token type`, `Invalid token or user not found`, `Token expired or invalid`.500JSON `{ "error": "Internal Server Error" }`.
curl -X POST "https://app.ummahspot.com/user/reset-password" \
-H "Content-Type: application/json" \
-d '{"token":"string","newPassword":"string"}'{
"message": "Password reset successful"
}Start the Google OAuth sign-in redirect flow
Browser-navigation endpoint, not a JSON API. It packs the query parameters below into a base64url-encoded JSON `state` value and redirects the browser to Google's consent screen requesting the `profile` and `email` scopes. After the user approves, Google redirects the browser to `GET /user/auth/google/callback`, which finishes the sign-in and redirects back to the frontend (or to `redirectUrl` for the extension flow) with the token. Because the callback hands the session token to the redirect target, both redirect parameters are validated: `redirect` must be a path on the Sharh site, and `redirectUrl` is only accepted when it is a Chrome extension identity URL (`https://<32-character-extension-id>.chromiumapp.org/...`, as returned by `chrome.identity.getRedirectURL()`); any other `redirectUrl` is rejected with a 400 before the redirect to Google.
Query parameters
redirectstring | Same-site frontend path to send the user to after sign-in. It must start with a single "/" — a value that does not start with "/", starts with "//", or contains a backslash (for example an absolute URL to another origin) is replaced with "/". It is passed back as the `redirect` query parameter of the final frontend redirect.Default: / |
sourcestring | Where the flow started. `extension` (together with `redirectUrl`) makes the callback redirect to `redirectUrl` instead of the web frontend; any other value uses the web flow.Default: web |
redirectUrlstring | URL the callback redirects to when `source` is `extension`. Must be an `https://<extension-id>.chromiumapp.org/...` URL (the value of `chrome.identity.getRedirectURL()`): https only, the host is a 32-character extension id (letters a-p) followed by `.chromiumapp.org`, with no port or credentials. When the server is configured with an ALLOWED_EXTENSION_IDS list, the extension id must also be on that list. Anything else results in a 400. |
refstring | Affiliate referral code. Recorded only if the sign-in creates a new account and the code matches an active affiliate. |
Errors
400`redirectUrl` was supplied but is not an allowed `https://<extension-id>.chromiumapp.org/...` URL (JSON: `{ "message": "Invalid redirectUrl" }`)
curl -X GET "https://app.ummahspot.com/user/auth/google"Redirects (302) to the Google OAuth consent screen. No JSON body.Google OAuth callback that completes sign-in
Called by Google, via browser redirect, after the consent screen; clients do not call it directly. The Google profile is resolved to a Sharh user in this order: an existing user with the same Google id; otherwise an existing user with the same email, whose account is then linked to Google (`authMethod` becomes `both` for a local account); otherwise a new user is created with a username derived from the email local part (a numeric suffix is added if taken), a new Stripe customer and the `member` role. A JWT is issued and the `state` value is decoded to recover `redirectTo`, `source`, `redirectUrl` and the referral code. Because the state round-trips through the browser unsigned, its values are re-validated exactly as in `GET /user/auth/google`: a `redirectTo` that is not a same-site path (starting with a single "/") is replaced with "/", and a `redirectUrl` that is not an allowed `https://<extension-id>.chromiumapp.org/...` URL is discarded. If the user was newly created and the referral code matches an active affiliate, a referral is recorded. The browser is then redirected. If `source` is `extension` and a valid `redirectUrl` was supplied, the redirect goes to `<redirectUrl>?token=<jwt>&user=<url-encoded JSON>`. Otherwise — including when the `redirectUrl` in the state was invalid — the normal web flow is used and it goes to `<FRONTEND_URL>/auth/callback?token=<jwt>&user=<url-encoded JSON>&redirect=<url-encoded redirect path>`, where the frontend base URL defaults to `https://sharhapp.com`. The `user` parameter is a URL-encoded JSON object with the fields `id`, `username`, `email`, `stripeCustomerId`, `roles`, `authMethod` and `profilePicture` (a presigned S3 URL if the user uploaded a picture, else the Google picture URL). If Google authentication fails, the browser is redirected to `<FRONTEND_URL>/login?error=oauth_failed`. If an error occurs while completing sign-in, it is redirected to `<FRONTEND_URL>/login?error=auth_error`.
Query parameters
coderequiredstring | Authorization code appended by Google; consumed by the OAuth middleware. |
statestring | The base64url-encoded JSON state created by `GET /user/auth/google`. When missing or undecodable, the defaults are used (`redirect` = `/`, `source` = `web`). Invalid `redirectTo` / `redirectUrl` values inside it are ignored. |
curl -X GET "https://app.ummahspot.com/user/auth/google/callback"Redirects (302) to the frontend with the credentials in the query string, e.g.
https://sharhapp.com/auth/callback?token=eyJhbGciOi...&user=%7B%22id%22%3A%22665f1c2e9b1d4a0012ab34cd%22%2C...%7D&redirect=%2Fbooks
Decoded "user" parameter:
{
"id": "665f1c2e9b1d4a0012ab34cd",
"username": "ibnrushd",
"email": "ibnrushd@example.com",
"stripeCustomerId": "cus_XXXXXXXXXXXXXX",
"roles": ["member"],
"authMethod": "google",
"profilePicture": "https://lh3.googleusercontent.com/a/..."
}Start a Google OAuth redirect for a logged-in user
Requires a valid Bearer token, then redirects to Google's consent screen requesting the `profile` and `email` scopes. No `state` is sent, so the flow finishes in `GET /user/auth/google/callback` with the default web behaviour (redirect to `<FRONTEND_URL>/auth/callback?token=...&user=...&redirect=%2F`). The callback resolves the account from the Google profile only: a Google account is linked to an existing Sharh account when their email addresses match.
Errors
401Plain text `Unauthorized: No token provided`.403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.404Plain text `User not found` (the token's user no longer exists).
curl -X GET "https://app.ummahspot.com/user/auth/google/link" \
-H "Authorization: Bearer $SHARH_TOKEN"Redirects (302) to the Google OAuth consent screen. No JSON body.Unlink the Google account from the current user
Removes the Google id from the authenticated user and sets `authMethod` to `local`. Refused when the account was created through Google and has no password, since the user would be left with no way to log in; set a password first (for example through the password reset flow).
Errors
400JSON `{ "error": "Cannot unlink Google account: No password set. Please set a password first." }`.401Plain text `Unauthorized: No token provided`.403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.404Plain text `User not found`.500Plain text `Internal Server Error`.
curl -X POST "https://app.ummahspot.com/user/auth/google/unlink" \
-H "Authorization: Bearer $SHARH_TOKEN"{
"message": "Google account unlinked successfully",
"user": {
"id": "665f1c2e9b1d4a0012ab34cd",
"username": "ibnrushd",
"email": "ibnrushd@example.com",
"authMethod": "local"
}
}Sign in with a Google ID token (mobile)
For native apps that obtain a Google ID token from the Google Sign-In SDK. The token is verified against the server's Google client id, then the Google profile is resolved to a Sharh user exactly as in the browser flow: match by Google id, else match by email and link the account, else create a new user (with a Stripe customer and the `member` role). Returns a JWT and a summary of the user. `profilePicture` is a presigned S3 URL (valid 1 hour) if the user uploaded a picture, otherwise the Google picture URL.
Body parameters
idTokenrequiredstring | Google ID token issued for the app's Google client id. |
Errors
400JSON `{ "message": "idToken is required" }` or `{ "message": "Google account has no email" }`.401JSON `{ "message": "Google token expired. Please try again." }` when the ID token is too old, or `{ "message": "Invalid Google token" }` for any other verification or sign-in failure.
curl -X POST "https://app.ummahspot.com/user/auth/google/mobile" \
-H "Content-Type: application/json" \
-d '{"idToken":"string"}'{
"token": "eyJhbGciOi...",
"user": {
"id": "665f1c2e9b1d4a0012ab34cd",
"username": "ibnrushd",
"email": "ibnrushd@example.com",
"stripeCustomerId": "cus_XXXXXXXXXXXXXX",
"roles": ["member"],
"authMethod": "google",
"profilePicture": "https://lh3.googleusercontent.com/a/..."
}
}