Back to 0fee
0fee

Seven SDKs in Seven Languages

How we built 7 SDKs for 0fee.dev in TypeScript, Python, Go, Ruby, PHP, Java, and C# across two sessions. By Juste A. Gnimavo and Claude.

Thales & Claude | March 25, 2026 8 min 0fee
sdksdeveloper-experienceopen-source

A payment API without SDKs is a payment API that developers avoid. Nobody wants to hand-craft HTTP requests, parse JSON responses, and handle error codes when a well-typed client library can do it for them. In Session 002, we built the TypeScript and Python SDKs. In Session 003, we added Go, Ruby, PHP, Java, and C#. Seven SDKs in two sessions.

The Pattern

All seven SDKs follow the same architectural pattern:

Client (configuration, HTTP, auth)
  |-- Types (models, enums, errors)
  |-- Resources
       |-- Payments (create, get, list, cancel)
       |-- Apps (create, get, list, update)
       |-- Checkout (create session, get session)
       |-- Webhooks (verify signature, parse event)

This consistency means a developer who knows the TypeScript SDK can pick up the Go SDK and find everything exactly where they expect it. The method names are identical, the parameter structures mirror each other, and the error types correspond one-to-one.

TypeScript SDK

The TypeScript SDK was first because our primary audience is web developers. It provides full type safety with TypeScript generics and discriminated unions for error handling.

Installation

bashnpm install @zerofee/sdk

Usage

typescriptimport { ZeroFee } from '@zerofee/sdk';

const client = new ZeroFee({
  apiKey: 'sk_live_...',
  baseUrl: 'https://api.0fee.dev/v1', // optional, defaults to production
});

// Create a payment (3-field API)
const payment = await client.payments.create({
  amount: 5000,
  sourceCurrency: 'XOF',
  paymentReference: 'ORDER-42',
});

console.log(payment.checkoutUrl);
// https://pay.0fee.dev/checkout/txn_abc123

// Get payment status
const status = await client.payments.get('txn_abc123');
console.log(status.status); // "completed" | "pending" | "failed" | ...

// List payments with filters
const payments = await client.payments.list({
  status: 'completed',
  currency: 'XOF',
  limit: 50,
});

Type Definitions

typescriptexport interface PaymentCreateParams {
  amount: number;
  sourceCurrency: string;
  paymentReference: string;
  paymentMethod?: string;
  provider?: string;
  customerEmail?: string;
  customerPhone?: string;
  customerFirstName?: string;
  customerLastName?: string;
  successUrl?: string;
  cancelUrl?: string;
  webhookUrl?: string;
  metadata?: Record<string, string>;
}

export interface Payment {
  id: string;
  status: PaymentStatus;
  amount: number;
  sourceCurrency: string;
  paymentReference: string;
  invoiceReference: string;
  provider?: string;
  checkoutUrl?: string;
  providerReference?: string;
  redirectUrl?: string;
  createdAt: string;
  completedAt?: string;
}

export type PaymentStatus =
  | 'pending'
  | 'processing'
  | 'completed'
  | 'failed'
  | 'cancelled'
  | 'refunded'
  | 'expired';

export class ZeroFeeError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public errorCode: string,
    public requestId?: string,
  ) {
    super(message);
    this.name = 'ZeroFeeError';
  }
}

Error Handling

typescripttry {
  const payment = await client.payments.create({
    amount: 5000,
    sourceCurrency: 'XOF',
    paymentReference: 'ORDER-42',
  });
} catch (error) {
  if (error instanceof ZeroFeeError) {
    switch (error.errorCode) {
      case 'invalid_currency':
        console.error('Currency not supported');
        break;
      case 'payment_required':
        console.error('Account suspended -- pay invoice');
        break;
      case 'rate_limited':
        console.error('Too many requests, retry after cooldown');
        break;
      default:
        console.error(`API error: ${error.message}`);
    }
  }
}

Python SDK

The Python SDK uses Pydantic models for request/response validation and httpx for async HTTP.

Installation

bashpip install zerofee

Usage

pythonfrom zerofee import ZeroFee, PaymentCreate

client = ZeroFee(api_key="sk_live_...")

# Async usage
import asyncio

async def main():
    payment = await client.payments.create(PaymentCreate(
        amount=5000,
        source_currency="XOF",
        payment_reference="ORDER-42",
    ))
    print(payment.checkout_url)

    # Get payment
    status = await client.payments.get("txn_abc123")
    print(status.status)

    # List with filters
    payments = await client.payments.list(status="completed", limit=50)
    for p in payments.data:
        print(f"{p.payment_reference}: {p.amount} {p.source_currency}")

asyncio.run(main())

Pydantic Models

pythonfrom pydantic import BaseModel, Field
from typing import Optional, Literal
from decimal import Decimal
from datetime import datetime

class PaymentCreate(BaseModel):
    amount: Decimal = Field(..., gt=0)
    source_currency: str = Field(..., min_length=3, max_length=3)
    payment_reference: str = Field(..., min_length=1, max_length=100)
    payment_method: Optional[str] = None
    provider: Optional[str] = None
    customer_email: Optional[str] = None
    customer_phone: Optional[str] = None
    customer_first_name: Optional[str] = None
    customer_last_name: Optional[str] = None
    success_url: Optional[str] = None
    cancel_url: Optional[str] = None
    webhook_url: Optional[str] = None
    metadata: Optional[dict[str, str]] = None

class Payment(BaseModel):
    id: str
    status: Literal[
        "pending", "processing", "completed",
        "failed", "cancelled", "refunded", "expired"
    ]
    amount: Decimal
    source_currency: str
    payment_reference: str
    invoice_reference: str
    provider: Optional[str] = None
    checkout_url: Optional[str] = None
    provider_reference: Optional[str] = None
    created_at: datetime
    completed_at: Optional[datetime] = None

class ZeroFeeError(Exception):
    def __init__(self, message: str, status_code: int,
                 error_code: str, request_id: Optional[str] = None):
        super().__init__(message)
        self.status_code = status_code
        self.error_code = error_code
        self.request_id = request_id

Go SDK

The Go SDK uses native net/http, context.Context for cancellation, and zero external dependencies.

Installation

bashgo get github.com/zerofee/zerofee-go

Usage

gopackage main

import (
    "context"
    "fmt"
    "log"

    zerofee "github.com/zerofee/zerofee-go"
)

func main() {
    client := zerofee.NewClient("sk_live_...")

    ctx := context.Background()

    // Create payment
    payment, err := client.Payments.Create(ctx, &zerofee.PaymentCreateParams{
        Amount:           5000,
        SourceCurrency:   "XOF",
        PaymentReference: "ORDER-42",
    })
    if err != nil {
        var apiErr *zerofee.Error
        if errors.As(err, &apiErr) {
            log.Fatalf("API error %s: %s", apiErr.Code, apiErr.Message)
        }
        log.Fatal(err)
    }

    fmt.Printf("Checkout URL: %s\n", payment.CheckoutURL)

    // Get payment
    status, err := client.Payments.Get(ctx, payment.ID)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Status: %s\n", status.Status)

    // List payments
    payments, err := client.Payments.List(ctx, &zerofee.PaymentListParams{
        Status:  zerofee.String("completed"),
        Limit:   zerofee.Int(50),
    })
    if err != nil {
        log.Fatal(err)
    }
    for _, p := range payments.Data {
        fmt.Printf("%s: %d %s\n", p.PaymentReference, p.Amount, p.SourceCurrency)
    }
}

The Go SDK is intentionally dependency-free. No external HTTP client, no JSON library beyond encoding/json, no logging framework. Go developers value minimal dependency trees, and we respect that.

Ruby SDK

bashgem install zerofee
rubyrequire 'zerofee'

client = ZeroFee::Client.new(api_key: 'sk_live_...')

# Create payment
payment = client.payments.create(
  amount: 5000,
  source_currency: 'XOF',
  payment_reference: 'ORDER-42'
)

puts payment.checkout_url

# Get payment
status = client.payments.get('txn_abc123')
puts status.status

# List payments
payments = client.payments.list(status: 'completed', limit: 50)
payments.data.each do |p|
  puts "#{p.payment_reference}: #{p.amount} #{p.source_currency}"
end

PHP SDK

bashcomposer require zerofee/zerofee-php
php<?php
use ZeroFee\ZeroFeeClient;

$client = new ZeroFeeClient('sk_live_...');

// Create payment
$payment = $client->payments->create([
    'amount' => 5000,
    'source_currency' => 'XOF',
    'payment_reference' => 'ORDER-42',
]);

echo $payment->checkout_url;

// Get payment
$status = $client->payments->get('txn_abc123');
echo $status->status;

// List payments
$payments = $client->payments->list(['status' => 'completed', 'limit' => 50]);
foreach ($payments->data as $p) {
    echo "{$p->payment_reference}: {$p->amount} {$p->source_currency}\n";
}

Java SDK

xml<dependency>
    <groupId>dev.zerofee</groupId>
    <artifactId>zerofee-java</artifactId>
    <version>1.0.0</version>
</dependency>
javaimport dev.zerofee.ZeroFee;
import dev.zerofee.model.Payment;
import dev.zerofee.model.PaymentCreateParams;

public class Main {
    public static void main(String[] args) {
        ZeroFee client = new ZeroFee("sk_live_...");

        // Create payment
        PaymentCreateParams params = PaymentCreateParams.builder()
            .amount(5000)
            .sourceCurrency("XOF")
            .paymentReference("ORDER-42")
            .build();

        Payment payment = client.payments().create(params);
        System.out.println(payment.getCheckoutUrl());

        // Get payment
        Payment status = client.payments().get("txn_abc123");
        System.out.println(status.getStatus());
    }
}

C# SDK

bashdotnet add package ZeroFee
csharpusing ZeroFee;

var client = new ZeroFeeClient("sk_live_...");

// Create payment
var payment = await client.Payments.CreateAsync(new PaymentCreateParams
{
    Amount = 5000,
    SourceCurrency = "XOF",
    PaymentReference = "ORDER-42",
});

Console.WriteLine(payment.CheckoutUrl);

// Get payment
var status = await client.Payments.GetAsync("txn_abc123");
Console.WriteLine(status.Status);

// List payments
var payments = await client.Payments.ListAsync(new PaymentListParams
{
    Status = "completed",
    Limit = 50,
});

foreach (var p in payments.Data)
{
    Console.WriteLine($"{p.PaymentReference}: {p.Amount} {p.SourceCurrency}");
}

Webhook Verification

Every SDK includes webhook signature verification, which is critical for security:

typescript// TypeScript
const event = client.webhooks.verify(
  requestBody,
  headers['x-zerofee-signature'],
  webhookSecret,
);

// Python
event = client.webhooks.verify(
    request.body,
    request.headers["x-zerofee-signature"],
    webhook_secret,
)

// Go
event, err := client.Webhooks.Verify(
    body,
    r.Header.Get("X-ZeroFee-Signature"),
    webhookSecret,
)

The verification logic is identical across all SDKs:

  1. Extract timestamp and signature from the header.
  2. Construct the signed payload: {timestamp}.{body}.
  3. Compute HMAC-SHA256 with the webhook secret.
  4. Compare signatures using constant-time comparison.
  5. Check that the timestamp is within 5 minutes (replay protection).

How We Built Seven SDKs in Two Sessions

The process was systematic:

Session 002 (TypeScript + Python): 1. Defined the interface contract -- method names, parameters, return types. 2. Built the TypeScript SDK first as the reference implementation. 3. Built the Python SDK, translating patterns to Pythonic equivalents. 4. Created test suites for both.

Session 003 (Go + Ruby + PHP + Java + C#): 1. Used the TypeScript SDK as the template. 2. For each language, translated the pattern following that language's idioms. 3. Go: zero deps, context.Context, explicit error handling. 4. Ruby: method_missing for fluent API, blocks for iteration. 5. PHP: PSR-4 autoloading, array-based params. 6. Java: builder pattern, checked exceptions. 7. C#: async/await, LINQ-friendly collections.

The key insight: once you have a well-designed reference SDK, translating to other languages is mechanical. The API contract does not change; only the syntax and idioms shift.

Consistency Guarantees

We maintain cross-SDK consistency through several practices:

AspectRule
Method namesIdentical across all SDKs (create, get, list, cancel)
Parameter namesSnake_case in Python/Ruby, camelCase in TS/Java/C#, PascalCase in Go
Error codesSame string codes across all SDKs
Response shapesSame JSON structure, language-appropriate types
Webhook verificationSame algorithm, same header names
Timeout defaults30 seconds across all SDKs
Retry behavior3 retries with exponential backoff, all SDKs

A developer switching from the TypeScript SDK to the Go SDK should feel like they are using the same product in a different language, not learning a new product.


This article is part of the "How We Built 0fee.dev" series. 0fee.dev is a payment orchestrator covering 53+ providers across 200+ countries, built by Juste A. GNIMAVO and Claude from Abidjan with zero human engineers. Follow the series for the complete build story.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles