OAuth quickstart
Ten minutes from a new app to a verified email on a connected account. First with curl so you see every request, then with the Node.js SDK.
Tokens are always live
OAuth tokens spend real credits. There is no OAuth sandbox; connect your own RocketVerifier account while you build. A single verification costs 1 credit.
1. Create an app
- Open Dashboard > Developer apps and click New app.
- Enter a name, a homepage URL and a redirect URI. For this guide use
http://localhost:3000/callback. Redirect URIs must match exactly, character for character. - Copy the client ID (
rvapp_...) and the client secret (rv_cs_...). The secret is shown once. If you lose it, rotate it from the same page.
export RV_CLIENT_ID=rvapp_xxxxxxxxxxxx
export RV_CLIENT_SECRET=rv_cs_xxxxxxxxxxxxxxxxxxxxxxxx
export RV_REDIRECT_URI=http://localhost:3000/callback2. Make a PKCE pair and a state value
PKCE ties the code to the browser session that asked for it. The verifier stays with you; the challenge goes in the URL.
export VERIFIER=$(openssl rand -base64 48 | tr -d '=+/\n' | cut -c1-64)
export CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
export STATE=$(openssl rand -hex 16)Or in one line of Node:
node -e "const c=require('crypto');const v=c.randomBytes(32).toString('base64url');console.log(v, c.createHash('sha256').update(v).digest('base64url'))"3. Send the user to the consent screen
Build the authorize URL and open it in a browser.
echo "https://app.rocketverifier.com/oauth/authorize?response_type=code\
&client_id=$RV_CLIENT_ID\
&redirect_uri=$(node -p 'encodeURIComponent(process.env.RV_REDIRECT_URI)')\
&scope=verify:single%20account:read\
&state=$STATE\
&code_challenge=$CHALLENGE\
&code_challenge_method=S256"Sign in, pick an account, and click Allow. The browser lands on your redirect URI. Nothing is listening there yet, which is fine: copy the query string from the address bar.
http://localhost:3000/callback?code=CODE_FROM_REDIRECT&state=3f9c2a...Check that state equals the value you generated. The code is single-use and expires in 60 seconds, so move to the next step quickly. If the user clicks Cancel you get error=access_denied instead.
4. Exchange the code for tokens
curl https://app.rocketverifier.com/api/oauth/token \
-u "$RV_CLIENT_ID:$RV_CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code=CODE_FROM_REDIRECT \
-d redirect_uri=$RV_REDIRECT_URI \
-d code_verifier=$VERIFIER{
"access_token": "rv_at_xxxxxxxxxxxxxxxxxxxxxxxx",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rv_rt_xxxxxxxxxxxxxxxxxxxxxxxx",
"scope": "verify:single account:read"
}-u sends the client credentials with HTTP Basic (client_secret_basic). Read scope from the response: it lists what the user actually approved.
5. Call the API as the user
export RV_TOKEN=rv_at_xxxxxxxxxxxxxxxxxxxxxxxx
curl https://api.rocketverifier.com/v1/me \
-H "Authorization: Bearer $RV_TOKEN"{
"email": "jane@acme.com",
"name": "Jane Doe",
"account_type": "personal",
"organization": null,
"credits": 12400,
"auth_type": "oauth",
"scopes": ["verify:single", "account:read"],
"app": { "client_id": "rvapp_xxxxxxxxxxxx", "name": "My App" }
}Show this in your UI ("Connected as jane@acme.com, 12,400 credits"). Now verify an address:
curl https://api.rocketverifier.com/v1/verify \
-H "Authorization: Bearer $RV_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "someone@example.com"}'The response is the same as with an API key. See Verify Email.
6. Refresh the access token
Access tokens last one hour. When one expires the API returns 401 with WWW-Authenticate: Bearer error="invalid_token". Trade the refresh token for a new pair:
curl https://app.rocketverifier.com/api/oauth/token \
-u "$RV_CLIENT_ID:$RV_CLIENT_SECRET" \
-d grant_type=refresh_token \
-d refresh_token=rv_rt_xxxxxxxxxxxxxxxxxxxxxxxxSave the new refresh token every time
Refresh tokens rotate on every use. The response contains a new refresh_token and the old one stops working. Sending an old refresh token again is treated as theft and revokes the whole connection.
The same flow in Node.js
The Node.js SDK generates PKCE, builds the URL, exchanges the code, and refreshes expired tokens for you.
npm install @rocketverifier/sdk expressserver.js
import express from 'express';
import RocketVerifier, { OAuth, OAuthError } from '@rocketverifier/sdk';
const oauth = new OAuth({
clientId: process.env.RV_CLIENT_ID, // rvapp_...
clientSecret: process.env.RV_CLIENT_SECRET, // rv_cs_..., server only
redirectUri: 'http://localhost:3000/callback',
});
const pending = new Map(); // state -> codeVerifier. Use your session store in production.
let saved = null; // Use your database in production, encrypted.
const app = express();
// 1. The Connect button links here.
app.get('/connect', (req, res) => {
const { url, state, codeVerifier } = oauth.getAuthorizeUrl({
scopes: ['verify:single', 'account:read'],
});
pending.set(state, codeVerifier);
res.redirect(url);
});
// 2. RocketVerifier redirects back here.
app.get('/callback', async (req, res) => {
const { code, state, error } = req.query;
if (error) return res.send('Not connected: ' + error); // access_denied when the user cancels
const codeVerifier = pending.get(state);
if (!codeVerifier) return res.status(400).send('Unknown state');
pending.delete(state);
saved = await oauth.exchangeCode({ code, codeVerifier });
res.redirect('/me');
});
// 3. Call the API as the user.
app.get('/me', async (req, res) => {
const rv = new RocketVerifier({
accessToken: saved.accessToken,
refreshToken: saved.refreshToken,
oauth,
onTokenRefresh: (next) => { saved = next; }, // refresh tokens rotate: always save
});
try {
const me = await rv.getMe();
const result = await rv.verify('someone@example.com');
res.json({ connectedAs: me.email, credits: me.credits, result });
} catch (err) {
if (err instanceof OAuthError && err.error === 'invalid_grant') {
return res.redirect('/connect'); // the user disconnected: ask them to connect again
}
throw err;
}
});
app.listen(3000, () => console.log('Open http://localhost:3000/connect'));Without refreshToken and oauth, an expired token throws InvalidTokenError. oauth.revoke(token) disconnects from your side.
The same flow in Python
from rocketverifier import RocketVerifier, OAuth, OAuthError
oauth = OAuth(
client_id="rvapp_...",
client_secret="rv_cs_...",
redirect_uri="http://localhost:3000/callback",
)
# Connect route: redirect to auth.url, keep auth.state and auth.code_verifier in the session
auth = oauth.get_authorize_url(scopes=["verify:single", "account:read"])
# Callback route: check state first, then
tokens = oauth.exchange_code(code=code, code_verifier=auth.code_verifier)
rv = RocketVerifier(
access_token=tokens.access_token,
refresh_token=tokens.refresh_token,
oauth=oauth,
on_token_refresh=save_tokens, # refresh tokens rotate: always save
)
print(rv.get_me())Next steps
- Pick the smallest set of scopes on the Scopes page.
- Set a webhook URL on your app to receive
grant.revoked, and go through the security checklist. - Submit for review to remove the Unverified badge.
- Let an AI assistant do the wiring: paste https://docs.rocketverifier.com/build-an-integration.md into it.
