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:
- Creates an authorization request via API
- Opens a popup window for user authentication
- Polls for completion while user completes verification in popup
- Automatically closes popup and returns identity data
- 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
- Always handle errors gracefully - network issues, popup blocking, etc.
- Implement proper loading states - show progress during authorization
- Use exponential backoff - for status polling to avoid rate limits
- Keep API keys secure - never expose them client-side
- Test thoroughly - across different browsers and devices
- Monitor usage - track authorization success rates and failure modes
Need help? Check out our examples repository or reach out in the Hack Club Slack.