Frontend Integration Guide
This comprehensive guide covers integrating the Billbora API with modern frontend applications, including React, Vue, and vanilla JavaScript implementations.Overview
Billbora provides multiple integration options for frontend applications:TypeScript SDK
Fully typed SDK with automatic error handling and retry logic
React Hooks
Pre-built hooks for common operations with React Query integration
Vue Composables
Vue 3 composables for reactive API integration
REST API
Direct REST API integration for any framework
TypeScript SDK
The official Billbora TypeScript SDK provides the easiest way to integrate with our API.Installation
Copy
npm install @billbora/sdk
Basic Setup
Copy
import { BillboraClient } from '@billbora/sdk'
const client = new BillboraClient({
apiKey: 'your-api-key',
environment: 'production' // or 'sandbox'
})
// Create an invoice
const invoice = await client.invoices.create({
client: 'client-id',
line_items: [{
description: 'Consulting Services',
quantity: 10,
unit_price: '150.00'
}]
})
console.log('Invoice created:', invoice.id)
Configuration Options
Copy
interface BillboraClientConfig {
apiKey: string
environment?: 'production' | 'sandbox'
baseURL?: string
timeout?: number
retries?: number
onError?: (error: BillboraError) => void
onRequest?: (config: RequestConfig) => void
onResponse?: (response: Response) => void
}
const client = new BillboraClient({
apiKey: process.env.BILLBORA_API_KEY!,
environment: 'production',
timeout: 30000,
retries: 3,
onError: (error) => {
console.error('API Error:', error)
// Send to error tracking service
}
})
React Hooks
For React applications, we provide a comprehensive set of hooks built on React Query.Installation
Copy
npm install @billbora/react @tanstack/react-query
Provider Setup
Copy
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BillboraProvider } from '@billbora/react'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<BillboraProvider apiKey="your-api-key">
<YourApp />
</BillboraProvider>
</QueryClientProvider>
)
}
Invoice Management
Copy
import { useInvoices, useCreateInvoice } from '@billbora/react'
function InvoiceList() {
const { data: invoices, isLoading } = useInvoices({
status: 'draft',
limit: 20
})
const createInvoice = useCreateInvoice()
const handleCreateInvoice = async () => {
try {
const newInvoice = await createInvoice.mutateAsync({
client: 'client-id',
line_items: [{
description: 'New Service',
quantity: 1,
unit_price: '100.00'
}]
})
console.log('Created:', newInvoice.id)
} catch (error) {
console.error('Failed to create invoice:', error)
}
}
if (isLoading) return <div>Loading...</div>
return (
<div>
<button onClick={handleCreateInvoice}>
Create Invoice
</button>
{invoices?.results.map(invoice => (
<div key={invoice.id}>
{invoice.invoice_number} - ${invoice.total_amount}
</div>
))}
</div>
)
}
Real-time Updates
Copy
import { useInvoiceSubscription } from '@billbora/react'
function InvoiceDetail({ invoiceId }: { invoiceId: string }) {
const { data: invoice } = useInvoiceSubscription(invoiceId)
// Automatically updates when invoice status changes
return (
<div>
<h1>Invoice {invoice?.invoice_number}</h1>
<p>Status: {invoice?.status}</p>
<p>Amount: ${invoice?.total_amount}</p>
</div>
)
}
Form Integration
Copy
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { createInvoiceSchema } from '@billbora/react/schemas'
function CreateInvoiceForm() {
const createInvoice = useCreateInvoice()
const form = useForm({
resolver: zodResolver(createInvoiceSchema),
defaultValues: {
client: '',
line_items: [{
description: '',
quantity: 1,
unit_price: '0.00'
}]
}
})
const onSubmit = async (data) => {
try {
await createInvoice.mutateAsync(data)
form.reset()
} catch (error) {
// Error handling is automatic with the hook
}
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<input {...form.register('client')} placeholder="Client ID" />
<input {...form.register('line_items.0.description')} placeholder="Description" />
<input {...form.register('line_items.0.quantity', { valueAsNumber: true })} type="number" />
<input {...form.register('line_items.0.unit_price')} placeholder="Price" />
<button type="submit" disabled={form.formState.isSubmitting}>
Create Invoice
</button>
</form>
)
}
Vue Composables
For Vue 3 applications, we provide reactive composables.Installation
Copy
npm install @billbora/vue @tanstack/vue-query
Setup
Copy
// main.ts
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import { createBillboraPlugin } from '@billbora/vue'
const app = createApp(App)
app.use(VueQueryPlugin)
app.use(createBillboraPlugin({
apiKey: 'your-api-key'
}))
app.mount('#app')
Usage in Components
Copy
<template>
<div>
<div v-if="isLoading">Loading invoices...</div>
<div v-else>
<div v-for="invoice in invoices?.results" :key="invoice.id">
{{ invoice.invoice_number }} - ${{ invoice.total_amount }}
</div>
</div>
<button @click="createNewInvoice" :disabled="isCreating">
Create Invoice
</button>
</div>
</template>
<script setup lang="ts">
import { useInvoices, useCreateInvoice } from '@billbora/vue'
const { data: invoices, isLoading } = useInvoices({
status: 'draft'
})
const { mutateAsync: createInvoice, isLoading: isCreating } = useCreateInvoice()
const createNewInvoice = async () => {
try {
await createInvoice({
client: 'client-id',
line_items: [{
description: 'Vue Service',
quantity: 1,
unit_price: '200.00'
}]
})
} catch (error) {
console.error('Failed:', error)
}
}
</script>
Direct API Integration
For other frameworks or vanilla JavaScript, you can integrate directly with our REST API.Fetch API Example
Copy
class BillboraAPI {
constructor(apiKey, baseURL = 'https://api.billbora.com/api/v1') {
this.apiKey = apiKey
this.baseURL = baseURL
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`
const config = {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers
},
...options
}
const response = await fetch(url, config)
if (!response.ok) {
const error = await response.json()
throw new Error(`API Error: ${error.error.message}`)
}
return response.json()
}
// Invoice methods
async getInvoices(params = {}) {
const query = new URLSearchParams(params).toString()
return this.request(`/invoices/?${query}`)
}
async createInvoice(data) {
return this.request('/invoices/', {
method: 'POST',
body: JSON.stringify(data)
})
}
async getInvoice(id) {
return this.request(`/invoices/${id}/`)
}
// Client methods
async getClients(params = {}) {
const query = new URLSearchParams(params).toString()
return this.request(`/clients/?${query}`)
}
async createClient(data) {
return this.request('/clients/', {
method: 'POST',
body: JSON.stringify(data)
})
}
}
// Usage
const api = new BillboraAPI('your-api-key')
// Create a client
const client = await api.createClient({
name: 'Acme Corp',
email: '[email protected]'
})
// Create an invoice
const invoice = await api.createInvoice({
client: client.id,
line_items: [{
description: 'Consulting',
quantity: 10,
unit_price: '150.00'
}]
})
Axios Example
Copy
import axios from 'axios'
// Create axios instance
const billboraAPI = axios.create({
baseURL: 'https://api.billbora.com/api/v1',
headers: {
'Authorization': `Bearer ${process.env.BILLBORA_API_KEY}`,
'Content-Type': 'application/json'
}
})
// Add response interceptor for error handling
billboraAPI.interceptors.response.use(
response => response,
error => {
if (error.response?.data?.error) {
throw new Error(error.response.data.error.message)
}
throw error
}
)
// Usage
async function createInvoice(invoiceData) {
try {
const response = await billboraAPI.post('/invoices/', invoiceData)
return response.data
} catch (error) {
console.error('Failed to create invoice:', error.message)
throw error
}
}
Authentication Integration
JWT Authentication for Frontend Apps
For frontend applications that need user authentication:Copy
import { createClient } from '@supabase/supabase-js'
class BillboraAuth {
private supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
)
async login(email: string, password: string) {
// Step 1: Authenticate with Supabase
const { data, error } = await this.supabase.auth.signInWithPassword({
email, password
})
if (error) throw error
// Step 2: Exchange for Billbora token
const response = await fetch('/api/v1/users/auth/exchange/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
supabase_token: data.session.access_token
})
})
const tokens = await response.json()
// Store tokens securely
localStorage.setItem('billbora_tokens', JSON.stringify(tokens))
return tokens
}
async logout() {
await this.supabase.auth.signOut()
localStorage.removeItem('billbora_tokens')
}
getAccessToken() {
const tokens = JSON.parse(localStorage.getItem('billbora_tokens') || '{}')
return tokens.access
}
}
// Usage with API client
const auth = new BillboraAuth()
const tokens = await auth.login('[email protected]', 'password')
const client = new BillboraClient({
accessToken: tokens.access
})
Error Handling
Global Error Handler
Copy
interface ErrorHandler {
onValidationError: (errors: Record<string, string[]>) => void
onNotFound: (resource: string) => void
onRateLimit: (retryAfter: number) => void
onUnauthorized: () => void
onServerError: (error: Error) => void
}
const errorHandler: ErrorHandler = {
onValidationError: (errors) => {
// Show field-specific errors in your form
Object.entries(errors).forEach(([field, messages]) => {
showFieldError(field, messages[0])
})
},
onNotFound: (resource) => {
showNotification(`${resource} not found`, 'error')
},
onRateLimit: (retryAfter) => {
showNotification(`Rate limited. Try again in ${retryAfter} seconds`, 'warning')
},
onUnauthorized: () => {
// Redirect to login
window.location.href = '/login'
},
onServerError: (error) => {
showNotification('Something went wrong. Please try again.', 'error')
console.error('Server error:', error)
}
}
// Configure client with error handler
const client = new BillboraClient({
apiKey: 'your-api-key',
errorHandler
})
Performance Optimization
Caching Strategies
Copy
// React Query with custom cache configuration
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: (failureCount, error) => {
// Don't retry on 4xx errors
if (error.status >= 400 && error.status < 500) {
return false
}
return failureCount < 3
}
}
}
})
// Custom cache keys for better invalidation
const invoiceKeys = {
all: ['invoices'] as const,
lists: () => [...invoiceKeys.all, 'list'] as const,
list: (filters: string) => [...invoiceKeys.lists(), filters] as const,
details: () => [...invoiceKeys.all, 'detail'] as const,
detail: (id: string) => [...invoiceKeys.details(), id] as const,
}
// Usage
const { data: invoices } = useQuery({
queryKey: invoiceKeys.list(JSON.stringify(filters)),
queryFn: () => getInvoices(filters)
})
Request Batching
Copy
class BatchedBillboraClient {
private batchQueue: Array<{
resolve: Function
reject: Function
request: any
}> = []
private batchTimeout: NodeJS.Timeout | null = null
async batchRequest(request: any) {
return new Promise((resolve, reject) => {
this.batchQueue.push({ resolve, reject, request })
if (!this.batchTimeout) {
this.batchTimeout = setTimeout(() => {
this.processBatch()
}, 50) // Batch requests over 50ms
}
})
}
private async processBatch() {
const batch = this.batchQueue.splice(0)
this.batchTimeout = null
try {
const response = await fetch('/api/v1/batch/', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
requests: batch.map(item => item.request)
})
})
const results = await response.json()
batch.forEach((item, index) => {
const result = results[index]
if (result.error) {
item.reject(new Error(result.error.message))
} else {
item.resolve(result.data)
}
})
} catch (error) {
batch.forEach(item => item.reject(error))
}
}
}
Testing
Mock API Responses
Copy
// Create mock client for testing
export class MockBillboraClient extends BillboraClient {
private mockResponses = new Map()
setMockResponse(endpoint: string, response: any) {
this.mockResponses.set(endpoint, response)
}
async request(endpoint: string, options?: any) {
const mockResponse = this.mockResponses.get(endpoint)
if (mockResponse) {
if (mockResponse instanceof Error) {
throw mockResponse
}
return mockResponse
}
return super.request(endpoint, options)
}
}
// Usage in tests
const mockClient = new MockBillboraClient({ apiKey: 'test' })
mockClient.setMockResponse('/invoices/', {
results: [
{ id: 'inv_1', invoice_number: 'INV-001', total_amount: '100.00' }
],
count: 1
})
// Test your components with mock data
Integration Tests
Copy
import { render, screen, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BillboraProvider } from '@billbora/react'
function createTestWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false }
}
})
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
<BillboraProvider apiKey="test-key">
{children}
</BillboraProvider>
</QueryClientProvider>
)
}
test('displays invoices', async () => {
render(<InvoiceList />, { wrapper: createTestWrapper() })
await waitFor(() => {
expect(screen.getByText('INV-001')).toBeInTheDocument()
})
})
Next Steps
Authentication Setup
Configure authentication for your frontend application
Webhook Integration
Set up real-time updates with webhooks
Payment Processing
Accept payments directly in your application
API Reference
Explore all available endpoints and methods