← Back to Developer Portal

API Documentation

Everything you need to integrate with the Tampa.dev platform.

Sign in with Tampa.dev

Add Tampa.dev authentication to your app and give your users single sign-on with the Tampa Bay tech community. Your app gets access to verified community member profiles, event data, and group memberships -- all through standard OAuth 2.1 with PKCE.

Demo

Want to see it in action? Check out the live demo app to try the OAuth flow yourself. The source code is available on GitHub as a reference implementation.

Quick Start

Step 1: Register Your Application

You need a client_id before your app can authenticate users. There are two ways to get one.

Developer Portal (Recommended)

  1. Sign in to tampa.dev and go to the Developer Portal
  2. Click Register New Application
  3. Choose your Client Type:
    • Confidential for server-side apps that can securely store a client secret
    • Public for SPAs or mobile apps that use PKCE only (no client secret)
  4. Fill in your app name and at least one redirect URI
  5. Save your client_id (and client_secret for confidential clients)

Dynamic Client Registration

For programmatic setups (CI pipelines, CLI tools, SPAs), register via the API:

curl -X POST https://tampa.dev/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My App",
    "redirect_uris": ["https://myapp.com/callback"],
    "grant_types": ["authorization_code"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "none"
  }'

The response includes your client_id:

{
  "client_id": "abc123...",
  "client_name": "My App",
  "redirect_uris": ["https://myapp.com/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

Set token_endpoint_auth_method to "none" for public clients (SPAs, mobile apps) that use PKCE without a client secret.

Step 2: Generate PKCE Parameters

PKCE (Proof Key for Code Exchange) is required for all OAuth flows. Generate a code_verifier and its SHA-256 code_challenge before redirecting the user:

function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64URLEncode(array);
}

async function generateCodeChallenge(verifier) {
  const data = new TextEncoder().encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return base64URLEncode(new Uint8Array(digest));
}

function base64URLEncode(buffer) {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

Store the code_verifier in the user's session -- you'll need it in Step 5.

Step 3: Redirect to Authorize

Send the user to the Tampa.dev authorization endpoint:

https://tampa.dev/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://myapp.com/callback
  &scope=read:user user:email
  &code_challenge=YOUR_CODE_CHALLENGE
  &code_challenge_method=S256
  &state=RANDOM_STATE_VALUE
ParameterRequiredDescription
response_typeYesMust be code
client_idYesYour app's client ID
redirect_uriYesMust match a registered redirect URI
scopeYesSpace-separated scopes
code_challengeYesPKCE code challenge (S256)
code_challenge_methodYesMust be S256
stateRecommendedRandom string to prevent CSRF attacks

The user will sign in (if needed) and see a consent screen listing the scopes your app is requesting. After they approve, Tampa.dev redirects back to your redirect_uri.

Step 4: Handle the Callback

Tampa.dev redirects the user back to your redirect_uri with an authorization code and your state value:

https://myapp.com/callback?code=AUTH_CODE&state=RANDOM_STATE_VALUE

Verify that state matches the value you sent in Step 3 to prevent CSRF attacks.

Step 5: Exchange Code for Tokens

Exchange the authorization code for an access token:

curl -X POST https://tampa.dev/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=https://myapp.com/callback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "code_verifier=YOUR_CODE_VERIFIER"

The response:

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBh...",
  "scope": "read:user user:email"
}

Store the refresh_token securely. Use it to get new access tokens without requiring the user to sign in again:

curl -X POST https://tampa.dev/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=YOUR_REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID"

Step 6: Fetch User Data

Use the access token to call the Tampa.dev API:

# Basic identity
curl -H "Authorization: Bearer ACCESS_TOKEN" \
  https://api.tampa.dev/v1/me

# Full profile
curl -H "Authorization: Bearer ACCESS_TOKEN" \
  https://api.tampa.dev/v1/profile

GET /v1/me returns:

{
  "data": {
    "id": "user-uuid",
    "name": "Jane Developer",
    "avatarUrl": "https://avatars.githubusercontent.com/...",
    "username": "janedev",
    "email": "dev@example.com"
  }
}

The email field is only included when the token has the user:email scope.


Complete Node.js Example

A minimal Express server implementing the full flow:

import express from 'express';
import crypto from 'crypto';

const app = express();

const CLIENT_ID = 'YOUR_CLIENT_ID';
const REDIRECT_URI = 'http://localhost:3000/callback';

// In production, use a proper session store
const sessions = new Map();

function base64URLEncode(buffer) {
  return buffer.toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

// Step 1: Start the OAuth flow
app.get('/login', async (req, res) => {
  const codeVerifier = base64URLEncode(crypto.randomBytes(32));
  const codeChallenge = base64URLEncode(
    crypto.createHash('sha256').update(codeVerifier).digest()
  );
  const state = base64URLEncode(crypto.randomBytes(16));

  // Store verifier and state in session
  const sessionId = base64URLEncode(crypto.randomBytes(16));
  sessions.set(sessionId, { codeVerifier, state });
  res.cookie('sid', sessionId, { httpOnly: true });

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    scope: 'read:user user:email',
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    state,
  });

  res.redirect(`https://tampa.dev/oauth/authorize?${params}`);
});

// Step 2: Handle the callback
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
  const session = sessions.get(req.cookies?.sid);

  if (!session || session.state !== state) {
    return res.status(403).send('Invalid state');
  }

  // Exchange code for tokens
  const tokenRes = await fetch('https://tampa.dev/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      code_verifier: session.codeVerifier,
    }),
  });

  const tokens = await tokenRes.json();

  // Fetch user profile
  const userRes = await fetch('https://api.tampa.dev/v1/me', {
    headers: { Authorization: `Bearer ${tokens.access_token}` },
  });

  const { data: user } = await userRes.json();
  res.json({ user, tokens: { scope: tokens.scope } });
});

app.listen(3000, () => console.log('http://localhost:3000/login'));

Button Assets and Style Guide

Use the official "Sign in with Tampa.dev" button for a consistent, recognizable experience across applications.

Hosted SVG Button

The easiest approach -- link directly to the hosted button image:

Sign in with Tampa.dev
Sign in with Tampa.dev (dark)
<!-- Light theme (default) -->
<a href="https://tampa.dev/oauth/authorize?...">
  <img src="https://api.tampa.dev/assets/signin-button.svg"
       alt="Sign in with Tampa.dev"
       height="40" />
</a>

<!-- Dark theme -->
<a href="https://tampa.dev/oauth/authorize?...">
  <img src="https://api.tampa.dev/assets/signin-button.svg?theme=dark"
       alt="Sign in with Tampa.dev"
       height="40" />
</a>

Brand Guidelines

Do

  • Use the official coral icon mark (#F97066 background, white "T." text)
  • Keep the full text "Sign in with Tampa.dev" -- the period and lowercase "dev" are part of the brand
  • Maintain at least 8px padding around the button
  • Use the button at a minimum height of 32px
  • Provide sufficient contrast between the button and its background

Don't

  • Change the icon mark colors or proportions
  • Abbreviate the text (not "Sign in with TD" or "Tampa.dev Login")
  • Place the button on a coral or red background where the icon loses contrast
  • Stretch or distort the button -- it should maintain its natural aspect ratio
  • Use the button for purposes other than Tampa.dev authentication

Brand Colors

ElementHexUsage
Icon background#F97066Coral fill on the "T." icon mark
Icon text#FFFFFFWhite "T." on the icon mark
Wordmark "Tampa"#000000Black in the full Tampa.dev logo
Wordmark ".dev"#E85A4FCoral in the full Tampa.dev logo
Light button background#FFFFFFWhite button on light backgrounds
Light button border#DADCE0Subtle gray border
Light button text#1A1A1ANear-black text
Dark button background#1A1A1ADark button on dark backgrounds
Dark button border#3A3A3ASubtle dark border
Dark button text#FFFFFFWhite text

Next Steps

  • Authentication -- Full OAuth parameter reference and token details
  • Scopes -- Available scopes and scope hierarchy
  • API Reference -- Endpoints available with your access token
  • Examples -- Common API usage patterns with cURL