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/sdkUsage
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 zerofeeUsage
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_idGo SDK
The Go SDK uses native net/http, context.Context for cancellation, and zero external dependencies.
Installation
bashgo get github.com/zerofee/zerofee-goUsage
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 zerofeerubyrequire '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}"
endPHP SDK
bashcomposer require zerofee/zerofee-phpphp<?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 ZeroFeecsharpusing 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:
- Extract timestamp and signature from the header.
- Construct the signed payload:
{timestamp}.{body}. - Compute HMAC-SHA256 with the webhook secret.
- Compare signatures using constant-time comparison.
- 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:
| Aspect | Rule |
|---|---|
| Method names | Identical across all SDKs (create, get, list, cancel) |
| Parameter names | Snake_case in Python/Ruby, camelCase in TS/Java/C#, PascalCase in Go |
| Error codes | Same string codes across all SDKs |
| Response shapes | Same JSON structure, language-appropriate types |
| Webhook verification | Same algorithm, same header names |
| Timeout defaults | 30 seconds across all SDKs |
| Retry behavior | 3 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.