This API is currently in closed beta and not publicly available

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

  1. Error Handling

    • Always catch OneTribeError specifically to handle API errors
    • Check error codes and status codes for specific error conditions
    • Implement retry logic for rate limit errors if needed
  2. Rate Limiting

    • Monitor rate limits using getRateLimitInfo()
    • Implement backoff strategies when approaching limits
    • Consider using a queue for high-volume requests
  3. Configuration

    • Store API keys securely (environment variables recommended)
    • Consider implementing request timeouts
    • Use TypeScript's strict mode for better type safety
  4. Testing

    • Mock API responses for unit tests
    • Test error conditions and rate limit handling
    • Validate request payloads before sending

Next Steps

Previous
Transactions (soon)