Skip to main content

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

  1. Open the API keys page in your WhatPulse account.
  2. In the OAuth applications section, select New application.
  3. 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/callback are 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.
  4. 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:

ParameterRequiredDescription
response_typeYesMust be code.
client_idYesYour application's client ID.
redirect_uriYesMust exactly match one of your registered redirect URIs.
scopeYesSpace separated list of scopes, for example user-public-api.
stateRecommendedA random value you generate to protect against CSRF. WhatPulse returns it unchanged.
code_challengePublic appsThe Base64 URL encoded SHA-256 hash of your PKCE code verifier.
code_challenge_methodPublic appsMust 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.

ScopeGrants
user-public-apiView 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.