OAuth applications (sign in with WhatPulse)
To retrieve your own data, you can use a personal API key - which grants access to your own WhatPulse account. If you are building an application that other people sign in to, register an OAuth application instead. This lets any WhatPulse user authorize your app, and your app then calls the API on their behalf with an access token scoped to that user.
Choose the right approach for your integration:
- Personal API key: you are accessing your own data, for example a script, a personal dashboard, or a one-off integration.
- OAuth application: you are building a product where other WhatPulse users sign in and you access their data with their permission.
Registering an application
- Open the API keys page in your WhatPulse account.
- In the OAuth applications section, select New application.
- Fill in the details:
- Application name: shown to users on the authorization screen, so use a name they will recognize.
- Redirect URIs: one per line. After a user authorizes your app, WhatPulse redirects back to one of these. Custom schemes such as
myapp://oauth/callbackare supported for mobile and desktop apps. - Application type:
- Confidential, for server-side apps that can keep a secret. You receive a client secret, shown only once.
- Public (PKCE), for mobile, desktop, and single-page apps that cannot keep a secret. No client secret is issued, and the app uses PKCE instead.
- After creating the app you receive a client ID (and a client secret for confidential apps). Store the secret securely, because it is not shown again.
The authorization flow
WhatPulse uses the OAuth 2.0 authorization code flow. Public applications must also use PKCE.
Step 1: Send the user to the authorization page
Redirect the user to https://whatpulse.org/oauth/authorize with the following query parameters:
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code. |
client_id | Yes | Your application's client ID. |
redirect_uri | Yes | Must exactly match one of your registered redirect URIs. |
scope | Yes | Space separated list of scopes, for example user-public-api. |
state | Recommended | A random value you generate to protect against CSRF. WhatPulse returns it unchanged. |
code_challenge | Public apps | The Base64 URL encoded SHA-256 hash of your PKCE code verifier. |
code_challenge_method | Public apps | Must be S256. |
Example:
https://whatpulse.org/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback&scope=user-public-api&state=RANDOM_STATE&code_challenge=CODE_CHALLENGE&code_challenge_method=S256
The user signs in to WhatPulse and approves the requested access. WhatPulse then redirects to your redirect_uri with a code and the same state you sent.
Step 2: Exchange the code for an access token
Send a POST request to https://whatpulse.org/oauth/token with Content-Type: application/x-www-form-urlencoded.
For a public (PKCE) application:
grant_type=authorization_code
client_id=YOUR_CLIENT_ID
redirect_uri=https://yourapp.com/callback
code=AUTHORIZATION_CODE
code_verifier=YOUR_CODE_VERIFIER
For a confidential application, send your client secret instead of the code verifier:
grant_type=authorization_code
client_id=YOUR_CLIENT_ID
client_secret=YOUR_CLIENT_SECRET
redirect_uri=https://yourapp.com/callback
code=AUTHORIZATION_CODE
A successful response returns a Bearer access token and a refresh token:
{
"token_type": "Bearer",
"expires_in": 31536000,
"access_token": "eyJ0eXAiOiJKV1Qi...",
"refresh_token": "def50200a1b2c3..."
}
Step 3: Call the API as the user
Send the access token as a Bearer token, exactly like a personal API key:
curl https://whatpulse.org/api/v1/users/12345 \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Accept: application/json"
The token acts as the user who authorized your app, so it can read that user's private data wherever they have granted access. To identify the signed in user and read their profile, call:
curl https://whatpulse.org/api/user \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Accept: application/json"
This returns the authenticated user's profile, including their user ID, which you can then use with the /api/v1/users/{id} endpoints.
Scopes
Request only the scopes your application needs. Users see the requested scopes on the authorization screen.
| Scope | Grants |
|---|---|
user-public-api | View the user's profile, statistics, and pulse history. |
Token lifetime and refresh
Access tokens are valid for one year, and refresh tokens are valid for one year. When an access token expires, request a new one with the refresh token:
POST https://whatpulse.org/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
refresh_token=YOUR_REFRESH_TOKEN
client_id=YOUR_CLIENT_ID
scope=user-public-api
Confidential applications must also include their client_secret.
Redirect URI matching
The redirect_uri you send to the authorization endpoint must match one of your registered redirect URIs exactly, character for character. A trailing slash, a different scheme, or a different path will be rejected. This is a security requirement of OAuth 2.0, and it applies to custom schemes as well. If authorization fails before the sign-in screen appears, an exact redirect URI mismatch is the most common cause.
Revoking access
Users can revoke your application at any time from their WhatPulse account settings, which immediately invalidates every access and refresh token issued to it. You can also delete an application you registered from the OAuth applications section, which revokes all of its tokens.
Code examples
The examples below implement the full authorization code flow. The JavaScript and Swift examples use PKCE for a public client. The Python example uses a client secret for a confidential, server-side app. Replace the placeholder values with your own client ID, redirect URI, and (for confidential apps) client secret.
Generating the PKCE code verifier and challenge
Public applications generate a random code verifier, derive a code challenge from it with SHA-256, send the challenge to the authorization endpoint, and send the verifier to the token endpoint. Both values use Base64 URL encoding without padding.
JavaScript
function base64UrlEncode(bytes) {
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
async function createPkcePair() {
const verifier = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
const challenge = base64UrlEncode(new Uint8Array(digest));
return { verifier, challenge };
}
Python
import base64
import hashlib
import secrets
def create_pkce_pair() -> tuple[str, str]:
verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode()
digest = hashlib.sha256(verifier.encode()).digest()
challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
return verifier, challenge
Swift
import CryptoKit
import Foundation
func base64URL(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
func makePkcePair() -> (verifier: String, challenge: String) {
var bytes = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
let verifier = base64URL(Data(bytes))
let challenge = base64URL(Data(SHA256.hash(data: Data(verifier.utf8))))
return (verifier, challenge)
}
JavaScript (single-page app, public client)
const CLIENT_ID = 'YOUR_CLIENT_ID';
const REDIRECT_URI = 'https://yourapp.com/callback';
const AUTHORIZE_URL = 'https://whatpulse.org/oauth/authorize';
const TOKEN_URL = 'https://whatpulse.org/oauth/token';
// Step 1: start the flow and send the user to WhatPulse.
async function startLogin() {
const { verifier, challenge } = await createPkcePair();
const state = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)));
// Keep these until the user returns to your callback page.
sessionStorage.setItem('pkce_verifier', verifier);
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user-public-api',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
window.location.href = `${AUTHORIZE_URL}?${params}`;
}
// Step 2: on your redirect_uri page, exchange the code for tokens.
async function handleCallback() {
const url = new URL(window.location.href);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
if (!code || state !== sessionStorage.getItem('oauth_state')) {
throw new Error('Invalid or missing authorization response.');
}
const response = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
code,
code_verifier: sessionStorage.getItem('pkce_verifier'),
}),
});
const tokens = await response.json();
// tokens.access_token, tokens.refresh_token, tokens.expires_in
return tokens;
}
The token exchange is a cross-origin request from the browser. If your origin is not permitted by the token endpoint, run the exchange from a small backend endpoint on your own server instead, and keep the tokens there.
Python (server-side, confidential client)
import secrets
from urllib.parse import urlencode
import requests
CLIENT_ID = "YOUR_CLIENT_ID"
CLIENT_SECRET = "YOUR_CLIENT_SECRET"
REDIRECT_URI = "https://yourapp.com/callback"
AUTHORIZE_URL = "https://whatpulse.org/oauth/authorize"
TOKEN_URL = "https://whatpulse.org/oauth/token"
# Step 1: build the URL to send the user to, and store `state` in their session.
def build_authorize_url() -> tuple[str, str]:
state = secrets.token_urlsafe(16)
params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": "user-public-api",
"state": state,
}
return f"{AUTHORIZE_URL}?{urlencode(params)}", state
# Step 2: exchange the code your callback received for tokens.
def exchange_code(code: str) -> dict:
response = requests.post(TOKEN_URL, data={
"grant_type": "authorization_code",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": REDIRECT_URI,
"code": code,
}, headers={"Accept": "application/json"})
response.raise_for_status()
return response.json()
# Step 3: call the API as the user.
def get_current_user(access_token: str) -> dict:
response = requests.get("https://whatpulse.org/api/user", headers={
"Authorization": f"Bearer {access_token}",
"Accept": "application/json",
})
response.raise_for_status()
return response.json()
Swift (native iOS and macOS, public client)
Native apps register a custom scheme as their redirect URI and open the authorization page with ASWebAuthenticationSession, which returns the callback URL when WhatPulse redirects to your scheme.
import AuthenticationServices
import Foundation
let clientID = "YOUR_CLIENT_ID"
let redirectURI = "myapp://oauth/callback"
let callbackScheme = "myapp"
let authorizeURL = "https://whatpulse.org/oauth/authorize"
let tokenURL = "https://whatpulse.org/oauth/token"
func authorize(presentationContext: ASWebAuthenticationPresentationContextProviding) async throws -> [String: Any] {
let (verifier, challenge) = makePkcePair()
let state = base64URL(Data((0..<16).map { _ in UInt8.random(in: 0...255) }))
var components = URLComponents(string: authorizeURL)!
components.queryItems = [
.init(name: "response_type", value: "code"),
.init(name: "client_id", value: clientID),
.init(name: "redirect_uri", value: redirectURI),
.init(name: "scope", value: "user-public-api"),
.init(name: "state", value: state),
.init(name: "code_challenge", value: challenge),
.init(name: "code_challenge_method", value: "S256"),
]
// Open the authorization page and wait for the redirect back to your scheme.
let callbackURL: URL = try await withCheckedThrowingContinuation { continuation in
let session = ASWebAuthenticationSession(
url: components.url!,
callbackURLScheme: callbackScheme
) { url, error in
if let url { continuation.resume(returning: url) }
else { continuation.resume(throwing: error ?? URLError(.badServerResponse)) }
}
session.presentationContextProvider = presentationContext
session.prefersEphemeralWebBrowserSession = true
session.start()
}
let returned = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)
guard returned?.queryItems?.first(where: { $0.name == "state" })?.value == state,
let code = returned?.queryItems?.first(where: { $0.name == "code" })?.value else {
throw URLError(.userAuthenticationRequired)
}
// Exchange the code for tokens.
var request = URLRequest(url: URL(string: tokenURL)!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
var body = URLComponents()
body.queryItems = [
.init(name: "grant_type", value: "authorization_code"),
.init(name: "client_id", value: clientID),
.init(name: "redirect_uri", value: redirectURI),
.init(name: "code", value: code),
.init(name: "code_verifier", value: verifier),
]
request.httpBody = body.query?.data(using: .utf8)
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
}
Store the returned refresh token in the Keychain, and use it to obtain a new access token when the current one expires.