Skip to main content

Authentication

Billbora uses a hybrid authentication system designed for both server-to-server integrations and frontend applications. This guide covers all authentication methods and best practices.

Authentication Methods

API Keys

API keys are the simplest way to authenticate with the Billbora API, ideal for server-to-server integrations.

Getting API Keys

1

Log into Billbora

Navigate to app.billbora.com and log in.
2

Go to API Settings

Navigate to SettingsAPICredentials.
3

Generate API Key

Click Generate New API Key and copy the generated key.
Store this key securely - it won’t be shown again!

Using API Keys

Include your API key in the Authorization header with the Bearer scheme:
curl -X GET \
  https://api.billbora.com/api/v1/invoices/ \
  -H "Authorization: Bearer YOUR_API_KEY"

API Key Security

  • Store API keys in environment variables, never in code
  • Use different keys for different environments (development, staging, production)
  • Rotate keys regularly (every 90 days recommended)
  • Monitor API key usage in your dashboard
  • Revoke compromised keys immediately
  • Each organization can have multiple API keys
  • Keys can be scoped to specific permissions
  • Usage analytics are available per key
  • Keys can be temporarily disabled without deletion

JWT Authentication

JWT authentication is designed for frontend applications and provides organization context switching capabilities.

Authentication Flow

The JWT authentication flow involves multiple steps:

Step 1: Supabase Authentication

First, authenticate with Supabase to get the initial JWT token:
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-supabase-anon-key'
)

const { data, error } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'secure-password'
})

const supabaseToken = data.session?.access_token

Step 2: Token Exchange

Exchange the Supabase token for a Billbora token with organization context:
const response = await fetch('https://api.billbora.com/api/v1/users/auth/exchange/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    supabase_token: supabaseToken,
    organization_id: 'org_1234567890' // Optional
  })
})

const tokens = await response.json()
// tokens.access, tokens.refresh, tokens.organization

Step 3: Using JWT Tokens

Use the Billbora JWT token for API requests:
const response = await fetch('https://api.billbora.com/api/v1/invoices/', {
  headers: {
    'Authorization': `Bearer ${tokens.access}`,
    'Content-Type': 'application/json'
  }
})

Token Refresh

JWT tokens expire after 1 hour. Use the refresh token to get a new access token:
const refreshResponse = await fetch('https://api.billbora.com/api/v1/users/auth/refresh/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    refresh: refreshToken
  })
})

const newTokens = await refreshResponse.json()
// Update stored tokens

Organization Context

One of the key features of Billbora’s authentication system is organization context switching.

Switching Organizations

Users can belong to multiple organizations and switch between them:
const switchOrganization = async (organizationId) => {
  // Get fresh Supabase token
  const { data } = await supabase.auth.getSession()
  const supabaseToken = data.session?.access_token
  
  // Exchange for new organization context
  const newTokens = await exchangeTokens(supabaseToken, organizationId)
  
  // Update stored tokens
  localStorage.setItem('billbora_auth', JSON.stringify(newTokens))
  
  return newTokens
}

Getting Available Organizations

Fetch organizations the current user has access to:
const organizations = await fetch('https://api.billbora.com/api/v1/users/me/organizations/', {
  headers: {
    'Authorization': `Bearer ${accessToken}`
  }
})

const orgList = await organizations.json()

Error Handling

Handle authentication errors gracefully:
  • invalid_credentials: Wrong email/password combination
  • token_expired: JWT token has expired, refresh needed
  • invalid_token: Malformed or invalid token
  • organization_access_denied: User doesn’t have access to requested organization
  • rate_limited: Too many authentication attempts
{
  "error": {
    "code": "token_expired",
    "message": "The provided token has expired",
    "details": {
      "expired_at": "2025-01-15T10:30:00Z"
    }
  }
}

Security Best Practices

Token Storage

  • Store tokens securely (not in localStorage for sensitive apps)
  • Use httpOnly cookies for web applications when possible
  • Clear tokens on logout
  • Implement token expiration checking

Network Security

  • Always use HTTPS in production
  • Implement proper CORS policies
  • Validate SSL certificates
  • Monitor for unusual authentication patterns

Application Security

  • Implement proper session management
  • Use secure password requirements
  • Enable two-factor authentication
  • Implement account lockout policies

Monitoring

  • Monitor authentication failures
  • Set up alerts for suspicious activity
  • Regularly audit API key usage
  • Implement request rate limiting

Complete Authentication Example

Here’s a complete example of implementing authentication in a React application:
import { useState, useEffect, useCallback, createContext, useContext } from 'react'
import { createClient } from '@supabase/supabase-js'

interface AuthState {
  isAuthenticated: boolean
  isLoading: boolean
  user: any
  organization: any
  error: string | null
}

const AuthContext = createContext<any>(null)

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [state, setState] = useState<AuthState>({
    isAuthenticated: false,
    isLoading: true,
    user: null,
    organization: null,
    error: null
  })

  const supabase = createClient(
    process.env.REACT_APP_SUPABASE_URL!,
    process.env.REACT_APP_SUPABASE_ANON_KEY!
  )

  const login = useCallback(async (email: string, password: string) => {
    setState(prev => ({ ...prev, isLoading: true, error: null }))
    
    try {
      // Step 1: Supabase authentication
      const { data, error } = await supabase.auth.signInWithPassword({ email, password })
      if (error) throw error

      // Step 2: Token exchange
      const tokens = await exchangeTokens(data.session!.access_token)
      
      // Step 3: Get user info
      const user = await fetchUser(tokens.access)
      
      setState({
        isAuthenticated: true,
        isLoading: false,
        user,
        organization: tokens.organization,
        error: null
      })
      
      // Store tokens
      localStorage.setItem('billbora_auth', JSON.stringify(tokens))
      
    } catch (error: any) {
      setState(prev => ({
        ...prev,
        isLoading: false,
        error: error.message
      }))
    }
  }, [])

  const logout = useCallback(async () => {
    await supabase.auth.signOut()
    localStorage.removeItem('billbora_auth')
    
    setState({
      isAuthenticated: false,
      isLoading: false,
      user: null,
      organization: null,
      error: null
    })
  }, [])

  return (
    <AuthContext.Provider value={{ ...state, login, logout }}>
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = () => {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

Next Steps