Additional Information
Building an API Client
This guide will walk you through building a type-safe TypeScript client for the One Tribe API. We'll cover error handling, type definitions, and best practices.
Whilst this guide uses Typescript the concepts should all you to build one in any language.
Basic Client Setup
First, let's create the basic structure of our API client:
class OneTribeClient {
private baseUrl: string
private headers: HeadersInit
constructor(apiKey: string, options: { baseUrl?: string } = {}) {
this.baseUrl = options.baseUrl || 'https://api.onetribe.com/v1'
this.headers = {
Authorisation: `API-Key ${apiKey}`,
'Content-Type': 'application/json',
}
}
}
Type Definitions
Let's define the core types we'll use throughout the client:
// Project types
interface Project {
project_id: string
title: string
description: string
primary_image: string
location_name: string
country: string
category: string
registry_type: string
min_volume: number
pricing: {
base_price_per_ton: number
partner_margin_percentage: number
partner_profit_per_ton: number
end_customer_price_per_ton: number
vat_rate: number
total_price: number
}
project_status: string
}
// Transaction types
interface Transaction {
transaction_id: string
project_id: string
transaction_type: 'purchase'
quantity: number
status: 'ordered' | 'accepted' | 'retired' | 'completed'
description?: string
metadata?: Record<string, unknown>
createdAt: string
updatedAt: string
}
// API Response types
interface PaginatedResponse<T> {
data: T[]
meta: {
page: number
limit: number
total: number
}
}
// Error types
class OneTribeError extends Error {
constructor(
message: string,
public status: number,
public code: string,
) {
super(message)
this.name = 'OneTribeError'
}
}
Error Handling
Let's add error handling to our client:
class OneTribeClient {
// ... previous code ...
private async handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
let errorMessage = 'An unknown error occurred'
let errorCode = 'UnknownError'
try {
const errorData = await response.json()
errorMessage = errorData.error?.message || errorMessage
errorCode = errorData.error?.code || errorCode
} catch {
// Use default error message if response isn't JSON
}
throw new OneTribeError(errorMessage, response.status, errorCode)
}
return response.json()
}
private async request<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
...this.headers,
...options.headers,
},
})
return this.handleResponse<T>(response)
} catch (error) {
if (error instanceof OneTribeError) {
throw error
}
throw new OneTribeError('Failed to connect to the API', 0, 'NetworkError')
}
}
}
Project Methods
Now let's add methods for interacting with projects:
class OneTribeClient {
// ... previous code ...
async listProjects(
params: {
page?: number
limit?: number
sort?: string
filter?: Record<string, string>
} = {},
): Promise<PaginatedResponse<Project>> {
const searchParams = new URLSearchParams()
if (params.page) searchParams.append('page', params.page.toString())
if (params.limit) searchParams.append('limit', params.limit.toString())
if (params.sort) searchParams.append('sort', params.sort)
if (params.filter) {
Object.entries(params.filter).forEach(([key, value]) => {
searchParams.append(`filter[${key}]`, value)
})
}
return this.request<PaginatedResponse<Project>>(
`/projects/offset?${searchParams.toString()}`,
)
}
async getProject(projectId: string): Promise<Project> {
return this.request<Project>(`/projects/offset/${projectId}`)
}
}
Transaction Methods
Let's add methods for managing transactions:
interface CreateTransactionParams {
project_id: string
transaction_type: 'purchase' | 'sale'
quantity: number
description?: string
}
class OneTribeClient {
// ... previous code ...
async createTransaction(
params: CreateTransactionParams,
): Promise<Transaction> {
return this.request<Transaction>('/transactions', {
method: 'POST',
body: JSON.stringify(params),
})
}
async listTransactions(
params: {
page?: number
limit?: number
sort?: string
filter?: {
project_id?: string
transaction_type?: 'purchase' | 'sale'
}
} = {},
): Promise<PaginatedResponse<Transaction>> {
const searchParams = new URLSearchParams()
if (params.page) searchParams.append('page', params.page.toString())
if (params.limit) searchParams.append('limit', params.limit.toString())
if (params.sort) searchParams.append('sort', params.sort)
if (params.filter) {
Object.entries(params.filter).forEach(([key, value]) => {
if (value) searchParams.append(`filter[${key}]`, value)
})
}
return this.request<PaginatedResponse<Transaction>>(
`/transactions?${searchParams.toString()}`,
)
}
async getTransaction(transactionId: string): Promise<Transaction> {
return this.request<Transaction>(`/transactions/${transactionId}`)
}
async deleteTransaction(transactionId: string): Promise<void> {
await this.request(`/transactions/${transactionId}`, {
method: 'DELETE',
})
}
}
Rate Limiting
Let's add rate limit handling:
interface RateLimitInfo {
limit: number
remaining: number
reset: number
}
class OneTribeClient {
private lastRateLimit?: RateLimitInfo
// ... previous code ...
private updateRateLimits(response: Response) {
const limit = response.headers.get('X-RateLimit-Limit')
const remaining = response.headers.get('X-RateLimit-Remaining')
const reset = response.headers.get('X-RateLimit-Reset')
if (limit && remaining && reset) {
this.lastRateLimit = {
limit: parseInt(limit, 10),
remaining: parseInt(remaining, 10),
reset: parseInt(reset, 10),
}
}
}
getRateLimitInfo(): RateLimitInfo | undefined {
return this.lastRateLimit
}
// Update the request method to track rate limits
private async request<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
...this.headers,
...options.headers,
},
})
this.updateRateLimits(response)
return this.handleResponse<T>(response)
} catch (error) {
if (error instanceof OneTribeError) {
throw error
}
throw new OneTribeError('Failed to connect to the API', 0, 'NetworkError')
}
}
}
Usage Example
Here's how to use the client:
// Initialize the client
const client = new OneTribeClient('YOUR_API_KEY_HERE')
// List projects with filtering and pagination
try {
const projects = await client.listProjects({
page: 1,
limit: 10,
filter: {
country: 'Brazil',
project_status: 'active',
},
})
console.log('Projects:', projects.data)
console.log('Pagination:', projects.meta)
} catch (error) {
if (error instanceof OneTribeError) {
console.error(`${error.code} (${error.status}): ${error.message}`)
}
}
// Create a transaction
try {
const transaction = await client.createTransaction({
project_id: 'abc123',
transaction_type: 'purchase',
quantity: 100,
description: 'Monthly offset purchase',
})
console.log('Transaction created:', transaction)
} catch (error) {
if (error instanceof OneTribeError) {
console.error(`${error.code} (${error.status}): ${error.message}`)
}
}
// Check rate limits
const rateLimit = client.getRateLimitInfo()
if (rateLimit) {
console.log(`${rateLimit.remaining}/${rateLimit.limit} requests remaining`)
console.log(`Reset at: ${new Date(rateLimit.reset * 1000).toISOString()}`)
}
Best Practices
Error Handling
- Always catch
OneTribeErrorspecifically to handle API errors - Check error codes and status codes for specific error conditions
- Implement retry logic for rate limit errors if needed
- Always catch
Rate Limiting
- Monitor rate limits using
getRateLimitInfo() - Implement backoff strategies when approaching limits
- Consider using a queue for high-volume requests
- Monitor rate limits using
Configuration
- Store API keys securely (environment variables recommended)
- Consider implementing request timeouts
- Use TypeScript's strict mode for better type safety
Testing
- Mock API responses for unit tests
- Test error conditions and rate limit handling
- Validate request payloads before sending
Next Steps
- Review the authentication documentation for API key management
- Check the rate limiting guidelines
- Explore available endpoints in the Projects and Transactions sections