Sign in with FML
With “Sign in with FML”, your app gets a member's token without ever seeing their password: the member signs in on fmylife.com and agrees to let your app act on their behalf. It's OAuth 2 (authorization code with PKCE), like “Sign in with Google”. For an app meant for people other than you, use it instead of signing in with a password.
1. Turn it on for your key
In My keys, turn on “Sign in with FML” on your app's key and enter its redirect URIs (redirect_uri): https, http://localhost for development, or a mobile app scheme like com.myapp:/oauth. You get a client_id and, for an app with a server, a client_secret, shown once. A mobile or JavaScript app can't keep a secret: check the no-secret option, PKCE is enough.
2. Send the member to FML
Create a random code_verifier (43 to 128 characters) and a state, keep them for the next step, then open this URL in the browser. The code_challenge is the verifier's SHA-256, base64url-encoded. The redirect_uri must be exactly one of the registered URIs.
https://www.fmylife.com/oauth/authorize
?response_type=code
&client_id=$CLIENT_ID
&redirect_uri=https%3A%2F%2Fmyapp.com%2Fauth%2Fcallback
&state=$STATE
&code_challenge=$CODE_CHALLENGE
&code_challenge_method=S256On mobile, open it in the system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android), never in a WebView: the member must see they're really on fmylife.com.
3. Get the code
The member signs in if needed, then accepts or declines. FML sends them back to your redirect_uri with the same state: check it first.
https://myapp.com/auth/callback?code=…&state=$STATE
https://myapp.com/auth/callback?error=access_denied&state=$STATE4. Exchange the code for a token
The code is valid for 10 minutes and works once. Send it as a form with the verifier. client_secret is required for an app with a server, and left out for an app without secret (you can also send it with HTTP Basic).
curl "https://www.fmylife.com/oauth/token" \
-d grant_type=authorization_code \
-d code=$CODE \
-d redirect_uri=https%3A%2F%2Fmyapp.com%2Fauth%2Fcallback \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET \
-d code_verifier=$CODE_VERIFIERThe response has the member's token and identity:
{
"access_token": "3f1c9b0e8d7a6f5e4d3c2b1a09f8e7d6",
"token_type": "Bearer",
"scope": "api",
"user": { "id": 123456, "username": "Name" }
}5. Call the API on the member's behalf
Send the token in Authorization: Bearer (or X-VDM-Api-Token) along with your key: it only works with it. Every endpoint of the reference marked “Member token required” is then available.
curl "https://www.fmylife.com/api/v2/user/123456/favorites" \
-H "X-VDM-Api-Key: $API_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN"Lifetime and revocation
- The token doesn't expire: store it safely (device keychain, encrypted database), like a password.
- It stops working if the member signs out of your app (GET /user/logout), changes their password, or if you revoke your key.
- A 401 on an endpoint that needs a token: send the member back to step 2.
Errors
/oauth/token answers in the OAuth 2 format: {"error": "…", "error_description": "…"}. invalid_client (401): wrong client_id or client_secret, or revoked key. invalid_grant (400): expired or used code, or one that doesn't match the verifier or the redirect_uri. On /oauth/authorize, an unknown client_id or redirect_uri is shown to the member instead of sending them back to you.
Full example (Node.js)
import { createHash, randomBytes } from "node:crypto";
// 1. Before redirecting the member: a verifier and a state, kept in the session
const verifier = randomBytes(32).toString("base64url");
const state = randomBytes(16).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
const url = new URL("https://www.fmylife.com/oauth/authorize");
url.search = new URLSearchParams({
response_type: "code",
client_id: process.env.CLIENT_ID,
redirect_uri: "https://myapp.com/auth/callback",
state,
code_challenge: challenge,
code_challenge_method: "S256",
}).toString();
// redirect the browser to url
// 2. On redirect_uri: check the state, then exchange the code
const response = await fetch("https://www.fmylife.com/oauth/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: "https://myapp.com/auth/callback",
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET, // not for an app without secret
code_verifier: verifier,
}),
});
const { access_token, user } = await response.json();