Skip to main content

Headless Integration

The headless integration allows you to perform identity verification programmatically without requiring users to be redirected away from your application. This is perfect for single-page applications, mobile apps, or any scenario where you want to maintain full control over the user experience.

Overview

Instead of redirecting users to complete OAuth flow, the headless integration:
  1. Creates an authorization request via API
  2. Opens a popup window for user authentication
  3. Polls for completion while user completes verification in popup
  4. Automatically closes popup and returns identity data
  5. Provides idv_rec for subsequent verification calls

Implementation

You’ll need your program API key from the admin dashboard to use the headless flow.
class SubmitAuthorizer {
  constructor(apiKey, baseUrl = 'https://submit.hackclub.com') {
    this.apiKey = apiKey;
    this.baseUrl = baseUrl;
  }

  async authorize() {
    // Step 1: Create authorization request
    const response = await fetch(`${this.baseUrl}/api/authorize`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error('Failed to create authorization request');
    }

    const { auth_id, popup_url } = await response.json();
    
    // Step 2: Open popup window
    const popup = window.open(
      popup_url,
      'authorization',
      'width=500,height=700,scrollbars=yes,resizable=yes'
    );

    // Step 3: Poll for completion
    return new Promise((resolve, reject) => {
      const pollInterval = setInterval(async () => {
        try {
          const statusResponse = await fetch(`${this.baseUrl}/api/authorize/${auth_id}/status`, {
            headers: { 'Authorization': `Bearer ${this.apiKey}` }
          });
          
          const status = await statusResponse.json();
          
          if (status.status === 'completed') {
            clearInterval(pollInterval);
            popup.close();
            resolve({
              authId: auth_id,
              idvRec: status.idv_rec,
              completedAt: status.completed_at
            });
          }
        } catch (error) {
          clearInterval(pollInterval);
          popup.close();
          reject(error);
        }
      }, 2000); // Poll every 2 seconds

      // Handle popup closed by user
      const checkClosed = setInterval(() => {
        if (popup.closed) {
          clearInterval(checkClosed);
          clearInterval(pollInterval);
          reject(new Error('Authorization cancelled by user'));
        }
      }, 1000);
    });
  }
}

Framework Examples

React Hook

import { useState, useCallback } from 'react';

interface UseSubmitAuthProps {
  apiKey: string;
  baseUrl?: string;
}

interface AuthResult {
  idvRec: string;
  completedAt: string;
}

export function useSubmitAuth({ apiKey, baseUrl = 'https://submit.hackclub.com' }: UseSubmitAuthProps) {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const authorize = useCallback(async (): Promise<AuthResult> => {
    setIsLoading(true);
    setError(null);

    try {
      const authorizer = new SubmitAuthorizer(apiKey, baseUrl);
      const result = await authorizer.authorize();
      return result;
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : 'Authorization failed';
      setError(errorMessage);
      throw err;
    } finally {
      setIsLoading(false);
    }
  }, [apiKey, baseUrl]);

  return { authorize, isLoading, error };
}

// Usage in component
function VerificationForm() {
  const { authorize, isLoading, error } = useSubmitAuth({
    apiKey: process.env.REACT_APP_SUBMIT_API_KEY!
  });

  const handleVerify = async () => {
    try {
      const result = await authorize();
      // Use result.idvRec for verification
      console.log('Got idv_rec:', result.idvRec);
    } catch (error) {
      console.error('Authorization failed:', error);
    }
  };

  return (
    <button onClick={handleVerify} disabled={isLoading}>
      {isLoading ? 'Verifying...' : 'Verify Identity'}
    </button>
  );
}

Vue 3 Composable

import { ref } from 'vue';

export function useSubmitAuth(apiKey: string, baseUrl = 'https://submit.hackclub.com') {
  const isLoading = ref(false);
  const error = ref<string | null>(null);

  const authorize = async () => {
    isLoading.value = true;
    error.value = null;

    try {
      const authorizer = new SubmitAuthorizer(apiKey, baseUrl);
      return await authorizer.authorize();
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Authorization failed';
      throw err;
    } finally {
      isLoading.value = false;
    }
  };

  return {
    authorize,
    isLoading: readonly(isLoading),
    error: readonly(error)
  };
}

Next.js App Router

'use client';

import { useState } from 'react';

export default function VerificationPage() {
  const [result, setResult] = useState<any>(null);
  const [loading, setLoading] = useState(false);

  const handleVerify = async () => {
    setLoading(true);
    
    try {
      // Note: In production, call your API route that has the secret API key
      const response = await fetch('/api/submit/authorize', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' }
      });
      
      const data = await response.json();
      
      if (data.popup_url) {
        // Open popup and wait for completion
        const popup = window.open(data.popup_url, 'auth', 'width=500,height=700');
        
        // Poll for completion (implement polling logic here)
        const pollForCompletion = setInterval(async () => {
          const statusResponse = await fetch(`/api/submit/status/${data.auth_id}`);
          const status = await statusResponse.json();
          
          if (status.status === 'completed') {
            clearInterval(pollForCompletion);
            popup.close();
            setResult(status);
          }
        }, 2000);
      }
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <button onClick={handleVerify} disabled={loading}>
        {loading ? 'Processing...' : 'Verify Identity'}
      </button>
      {result && <p>Identity verified: {result.idv_rec}</p>}
    </div>
  );
}

Security Considerations

Never expose your API key in client-side code! Always use server-side routes to make API calls with your secret key.

Client-Side Implementation

For client-side applications, create API routes that proxy requests:
// pages/api/submit/authorize.js (Next.js)
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    const response = await fetch('https://submit.hackclub.com/api/authorize', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.SUBMIT_API_KEY}`,
        'Content-Type': 'application/json'
      }
    });

    const data = await response.json();
    res.status(200).json(data);
  } catch (error) {
    res.status(500).json({ error: 'Failed to create authorization request' });
  }
}

Rate Limiting

The API includes rate limiting to prevent abuse:
  • Authorization requests: 10 per minute per API key
  • Status checks: 30 per minute per API key
Implement exponential backoff in your polling logic:
async function pollWithBackoff(authId, maxAttempts = 30) {
  let delay = 2000; // Start with 2 seconds
  
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const status = await checkStatus(authId);
    
    if (status.status === 'completed') {
      return status;
    }
    
    if (status.status === 'expired' || status.status === 'failed') {
      throw new Error(`Authorization ${status.status}`);
    }
    
    await new Promise(resolve => setTimeout(resolve, delay));
    delay = Math.min(delay * 1.2, 10000); // Max 10 seconds
  }
  
  throw new Error('Polling timeout');
}

Troubleshooting

Common Issues

Popup Blocked
  • Ensure the authorization is triggered by a user action (click)
  • Check browser popup settings
  • Consider showing a message about allowing popups
Authorization Timeout
  • Authorization requests expire after 15 minutes
  • Implement proper error handling for expired requests
  • Consider shortening your polling timeout
API Key Issues
  • Verify your API key is correct and from the right environment
  • Ensure the associated program is active
  • Check that you’re using Bearer token format: Bearer pk_...

Debug Mode

Enable debug logging in development:
const authorizer = new SubmitAuthorizer(apiKey, baseUrl, { debug: true });
This will log all API requests and responses to help diagnose issues.

Migration from Redirect Flow

If you’re migrating from the traditional redirect-based flow:

Before (Redirect)

// User clicks link -> redirected to Submit -> completes OAuth -> redirected back
window.location.href = 'https://submit.hackclub.com/your-program';

After (Headless)

// User clicks button -> popup opens -> OAuth completes in popup -> popup closes
const result = await authorizer.authorize();
// Continue with verification using result.idvRec
The main benefits of the headless approach:
  • No page reload - maintain application state
  • Better UX - users stay on your site
  • Mobile friendly - works well on all devices
  • Customizable - full control over UI and error handling

Best Practices

  1. Always handle errors gracefully - network issues, popup blocking, etc.
  2. Implement proper loading states - show progress during authorization
  3. Use exponential backoff - for status polling to avoid rate limits
  4. Keep API keys secure - never expose them client-side
  5. Test thoroughly - across different browsers and devices
  6. Monitor usage - track authorization success rates and failure modes
Need help? Check out our examples repository or reach out in the Hack Club Slack.