When we wrote Article 040 about the original seven SDKs, we had solid coverage of the basics: create a payment, get a payment, list payments, verify a webhook. But a gap analysis in Session 079 revealed an uncomfortable truth -- our SDKs only covered 6 of 28 API endpoints. That is 21% coverage. A developer using our SDK to list countries, manage customers, or create invoices would hit a wall and fall back to raw HTTP requests.
Sessions 079 and 080 fixed this. We upgraded the existing Python and Node.js SDKs from v2 to v3, created four entirely new SDKs (PHP, Go, Rust, Java), built two mobile SDKs (Flutter/Dart, React Native), and pushed API coverage from 21% to 79%. Eight SDKs across seven languages, all at version 3.0.0.
The Gap Analysis
Before writing any code, we mapped every SDK method against the complete API surface documented in docs/curl-rest-api-test.md:
| Resource | Endpoints | v2 Coverage | v3 Coverage |
|---|---|---|---|
| Payments | 5 | 4/5 | 5/5 |
| Apps | 4 | 2/4 | 4/4 |
| Webhooks (API key) | 2 | 0/2 | 2/2 |
| Countries | 2 | 0/2 | 2/2 |
| Currencies | 2 | 0/2 | 2/2 |
| Customers | 4 | 0/4 | 4/4 |
| Invoices | 3 | 0/3 | 3/3 |
| Checkout | 2 | 0/2 | 2/2 |
| Discovery | 1 | 0/1 | 0/1 |
| Webhooks (session) | 6 | 0/6 | 0/6 |
| Total | 28 | 6 (21%) | 22 (79%) |
The six webhook management endpoints that require session authentication (not API keys) were intentionally excluded. Those endpoints are used in the dashboard, not in server-to-server integrations.
Python SDK: v2 to v3
The Python SDK gained 16 new dataclass types and 6 new resource modules.
New Types
python# sdks/python/zerofee/types.py (v3 additions)
@dataclass
class Country:
code: str
name: str
currency_code: str
phone_code: str
supported_methods: list[str]
@dataclass
class Currency:
code: str
name: str
symbol: str
decimal_places: int
is_zero_decimal: bool
@dataclass
class Customer:
id: str
email: str
name: str | None
phone: str | None
metadata: dict | None
created_at: str
@dataclass
class Invoice:
id: str
customer_id: str
amount: float
currency: str
status: str # draft, sent, paid, cancelled
due_date: str | None
line_items: list[dict]
created_at: str
@dataclass
class CheckoutSession:
id: str
url: str
payment_id: str
expires_at: str
status: str
# ... 11 more types for request/response structuresNew Resources
Each resource follows the same pattern: a class that receives the HTTP client and exposes methods that map one-to-one with API endpoints.
python# sdks/python/zerofee/resources/customers.py
class Customers:
def __init__(self, client):
self._client = client
def create(self, email: str, name: str = None,
phone: str = None, metadata: dict = None) -> Customer:
payload = {"email": email}
if name: payload["name"] = name
if phone: payload["phone"] = phone
if metadata: payload["metadata"] = metadata
return self._client.post("/customers", payload, Customer)
def get(self, customer_id: str) -> Customer:
return self._client.get(f"/customers/{customer_id}", Customer)
def list(self, page: int = 1, limit: int = 20) -> list[Customer]:
return self._client.get(
f"/customers?page={page}&limit={limit}",
list[Customer]
)
def update(self, customer_id: str, **kwargs) -> Customer:
return self._client.patch(
f"/customers/{customer_id}", kwargs, Customer
)The client's __init__.py exports grew from 12 to 43 symbols.
Node.js SDK: v2 to v3
The TypeScript SDK mirrors the Python SDK structure but with language-appropriate conventions.
snake_case to camelCase Mapping
The 0fee.dev API uses snake_case (Python convention). The TypeScript SDK translates to camelCase:
typescript// sdks/typescript/src/types.ts
export interface Customer {
id: string;
email: string;
name?: string;
phone?: string;
metadata?: Record<string, string>;
createdAt: string; // API returns: created_at
}
export interface Invoice {
id: string;
customerId: string; // API returns: customer_id
amount: number;
currency: string;
status: 'draft' | 'sent' | 'paid' | 'cancelled';
dueDate?: string; // API returns: due_date
lineItems: LineItem[]; // API returns: line_items
createdAt: string;
}The translation happens in the HTTP client layer, so resource classes work with camelCase natively:
typescript// sdks/typescript/src/resources/invoices.ts
export class Invoices {
constructor(private client: HttpClient) {}
async create(params: CreateInvoiceParams): Promise<Invoice> {
return this.client.post<Invoice>('/invoices', params);
}
async get(invoiceId: string): Promise<Invoice> {
return this.client.get<Invoice>(`/invoices/${invoiceId}`);
}
async list(params?: ListParams): Promise<PaginatedResponse<Invoice>> {
return this.client.get<PaginatedResponse<Invoice>>('/invoices', params);
}
}Build output: 31 KB ESM, 33 KB CJS. Both formats ship in the npm package for maximum compatibility.
PHP SDK: New
PHP was the most requested SDK, driven by the WordPress and Laravel ecosystems. The SDK uses native cURL (no Guzzle dependency) and PSR-4 autoloading via Composer.
Installation
bashcomposer require zerofee/zerofee-phpArchitecture
sdks/php/
composer.json
src/
ZeroFee.php # Main client
Exceptions/
ApiException.php # Base API exception
AuthenticationException.php # Invalid API key
InvalidRequestException.php # Malformed request
NotFoundExcepion.php # Resource not found
RateLimitException.php # 429 Too Many Requests
ServerException.php # 5xx errors
Resources/
Payments.php
Apps.php
Countries.php
Currencies.php
Customers.php
Invoices.php
Checkout.php
Webhooks.phpException Hierarchy
Six exception classes provide granular error handling:
phpuse ZeroFee\Exceptions\AuthenticationException;
use ZeroFee\Exceptions\RateLimitException;
use ZeroFee\Exceptions\InvalidRequestException;
try {
$payment = $zerofee->payments->create([
'amount' => 10.00,
'source_currency' => 'USD',
]);
} catch (AuthenticationException $e) {
// Invalid or expired API key
error_log('Auth failed: ' . $e->getMessage());
} catch (RateLimitException $e) {
// Back off and retry
sleep($e->getRetryAfter());
} catch (InvalidRequestException $e) {
// Validation error - check the request parameters
error_log('Invalid: ' . $e->getMessage());
}Usage
phpuse ZeroFee\ZeroFee;
$zerofee = new ZeroFee('sk_live_...');
// Create a payment
$payment = $zerofee->payments->create([
'amount' => 25.00,
'source_currency' => 'EUR',
'payment_reference' => 'ORDER-123',
]);
// List supported countries
$countries = $zerofee->countries->list();
// Create a customer
$customer = $zerofee->customers->create([
'email' => '[email protected]',
'name' => 'Jane Doe',
]);
// Create an invoice
$invoice = $zerofee->invoices->create([
'customer_id' => $customer['id'],
'amount' => 100.00,
'currency' => 'USD',
'line_items' => [
['description' => 'Consulting (2 hours)', 'amount' => 100.00]
],
]);Go SDK: New
The Go SDK was designed around three Go-specific principles: zero external dependencies, functional options for configuration, and context.Context support for cancellation and timeouts.
Zero External Dependencies
go// go.mod
module github.com/zerofee/zerofee-go
go 1.21No require block. The SDK uses only the standard library: net/http, encoding/json, crypto/hmac, crypto/sha256. This means zero dependency conflicts and zero supply chain risk.
Functional Options Pattern
gopackage zerofee
type ClientOption func(*Client)
func WithBaseURL(url string) ClientOption {
return func(c *Client) {
c.baseURL = url
}
}
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = httpClient
}
}
func WithTimeout(d time.Duration) ClientOption {
return func(c *Client) {
c.httpClient.Timeout = d
}
}
// Usage
client := zerofee.NewClient("sk_live_...",
zerofee.WithTimeout(30 * time.Second),
zerofee.WithBaseURL("https://api.0fee.dev/v1"),
)Context Support
Every method accepts a context.Context as its first argument, following Go conventions:
goctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
payment, err := client.Payments.Create(ctx, &zerofee.CreatePaymentParams{
Amount: 10.00,
SourceCurrency: "USD",
PaymentReference: "GO-ORDER-001",
})
if err != nil {
var apiErr *zerofee.APIError
if errors.As(err, &apiErr) {
log.Printf("API error %d: %s", apiErr.StatusCode, apiErr.Message)
}
return err
}
fmt.Printf("Checkout URL: %s\n", payment.CheckoutURL)Webhook Verification
gofunc VerifyWebhookSignature(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}Rust SDK: New
The Rust SDK leverages the async ecosystem with tokio, reqwest, and serde. It provides compile-time type safety that catches integration errors before runtime.
Dependencies
toml# Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
hmac = "0.12"
sha2 = "0.10"
thiserror = "1"Usage
rustuse zerofee::ZeroFee;
#[tokio::main]
async fn main() -> Result<(), zerofee::Error> {
let client = ZeroFee::new("sk_live_...");
let payment = client.payments().create(
zerofee::CreatePaymentParams {
amount: 10.0,
source_currency: "USD".to_string(),
payment_reference: Some("RUST-ORDER-001".to_string()),
..Default::default()
}
).await?;
println!("Checkout URL: {}", payment.checkout_url);
// List countries
let countries = client.countries().list().await?;
for country in countries {
println!("{}: {}", country.code, country.name);
}
Ok(())
}Error Handling with thiserror
rust#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Authentication failed: {0}")]
Authentication(String),
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Rate limit exceeded, retry after {retry_after} seconds")]
RateLimit { retry_after: u64 },
#[error("Server error: {0}")]
Server(String),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
}Java SDK: New
The Java SDK targets Java 11+ and uses the built-in java.net.http.HttpClient -- zero external dependencies, just like the Go SDK.
Why Zero Dependencies
Most Java HTTP clients pull in dozens of transitive dependencies. Jackson alone brings jackson-core, jackson-databind, jackson-annotations, and their transitive trees. For a payment SDK that should be lightweight, we chose manual JSON parsing over convenience:
java// src/main/java/dev/zerofee/util/JsonUtil.java
public class JsonUtil {
public static Map<String, Object> parse(String json) {
// Recursive descent parser for JSON objects
// Handles: strings, numbers, booleans, null, arrays, objects
// No external dependencies
}
public static String toJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder("{");
// Manual serialization
// Handles nested objects and arrays
return sb.append("}").toString();
}
}Is this more code than adding a Jackson dependency? Yes. Is it worth it for a library that developers add to projects with complex dependency trees? Absolutely.
Usage
javaimport dev.zerofee.ZeroFee;
import dev.zerofee.model.Payment;
import dev.zerofee.model.params.CreatePaymentParams;
ZeroFee client = new ZeroFee("sk_live_...");
Payment payment = client.payments().create(
new CreatePaymentParams.Builder()
.amount(10.00)
.sourceCurrency("USD")
.paymentReference("JAVA-ORDER-001")
.build()
);
System.out.println("Checkout URL: " + payment.getCheckoutUrl());The builder pattern for request parameters provides a fluent API that is idiomatic Java while enforcing required fields at compile time.
Flutter/Dart SDK: New
The Flutter SDK supports both Flutter mobile apps and pure Dart server applications.
Null Safety
The SDK uses Dart's sound null safety throughout:
dartclass Payment {
final String id;
final double amount;
final String sourceCurrency;
final String status;
final String? checkoutUrl; // Nullable
final String? paymentReference; // Nullable
final Map<String, dynamic>? metadata;
final DateTime createdAt;
Payment({
required this.id,
required this.amount,
required this.sourceCurrency,
required this.status,
this.checkoutUrl,
this.paymentReference,
this.metadata,
required this.createdAt,
});
}Usage
dartimport 'package:zerofee/zerofee.dart';
final client = ZeroFee(apiKey: 'sk_live_...');
// Create a payment
final payment = await client.payments.create(
amount: 10.00,
sourceCurrency: 'USD',
paymentReference: 'FLUTTER-001',
);
// Open checkout in browser
if (payment.checkoutUrl != null) {
await launchUrl(Uri.parse(payment.checkoutUrl!));
}
// List currencies
final currencies = await client.currencies.list();
for (final currency in currencies) {
print('${currency.code}: ${currency.name}');
}React Native SDK: New
The React Native SDK wraps the TypeScript SDK and adds React-specific patterns: Context/Provider for configuration, hooks for state management, and deep linking for payment flow returns.
Provider Pattern
tsximport { ZeroFeeProvider, useZeroFee } from '@zerofee/react-native';
function App() {
return (
<ZeroFeeProvider apiKey="sk_live_...">
<PaymentScreen />
</ZeroFeeProvider>
);
}
function PaymentScreen() {
const { payments } = useZeroFee();
const { createPayment, loading, error } = usePayment();
const handlePay = async () => {
const payment = await createPayment({
amount: 10.00,
sourceCurrency: 'USD',
paymentReference: 'RN-ORDER-001',
});
// Deep link handling returns user to app after checkout
};
return (
<View>
<CheckoutButton
amount={10.00}
currency="USD"
onSuccess={(payment) => console.log('Paid:', payment.id)}
onError={(err) => console.error(err)}
/>
</View>
);
}Deep Linking
The SDK configures URL scheme handling so that after a customer completes payment on the 0fee.dev hosted checkout page, they are returned to the mobile app:
tsx// Automatic deep link configuration
const config = {
successUrl: 'myapp://payment/success',
cancelUrl: 'myapp://payment/cancel',
};The Complete Portfolio
| SDK | Version | Package Manager | Async Pattern | Dependencies |
|---|---|---|---|---|
| TypeScript | 3.0.0 | npm | async/await | 0 |
| Python | 3.0.0 | pip | sync (async optional) | requests |
| PHP | 3.0.0 | Composer | sync | 0 (native cURL) |
| Go | 3.0.0 | go modules | context.Context | 0 |
| Rust | 3.0.0 | crates.io | tokio async/await | reqwest, serde |
| Java | 3.0.0 | Maven | sync | 0 |
| Flutter/Dart | 3.0.0 | pub.dev | Future-based | http, crypto |
| React Native | 3.0.0 | npm | React hooks | wraps TS SDK |
Eight SDKs. Seven languages. Two sessions. 79% API coverage.
The remaining 21% (6 endpoints) are webhook management endpoints that require session authentication -- they are dashboard features, not SDK features. For the server-to-server integration use case, 0fee.dev now has complete SDK coverage in every major programming language.
Design Principles Across All SDKs
1. Consistent Resource Names
Every SDK exposes the same resources: payments, webhooks, countries, currencies, customers, invoices, checkout, discovery. The names are identical regardless of language.
2. Native Error Handling
Each SDK uses the language's native error handling mechanism: exceptions in Python/PHP/Java, Result<T, E> in Rust, error returns in Go, thrown errors in TypeScript/Dart. No SDK invents its own error pattern.
3. Minimal Dependencies
Four of eight SDKs have zero external dependencies (TypeScript, PHP, Go, Java). The others use only essential libraries: requests for Python, reqwest/serde for Rust, http/crypto for Dart. The React Native SDK wraps the TypeScript SDK rather than duplicating HTTP logic.
4. Webhook Verification Included
Every SDK includes HMAC-SHA256 webhook signature verification as a first-class method. This is not optional -- webhook verification is a security requirement, and making it easy is our job.
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.