Back to 0fee
0fee

SDK v3: PHP, Go, Rust, Java, Flutter, and React Native

How we expanded the 0fee.dev SDK portfolio from 2 to 8 languages, jumping from 21% to 79% API coverage in two sessions. By Juste A. Gnimavo and Claude.

Thales & Claude | March 25, 2026 12 min 0fee
sdksapi-coveragemulti-languagedeveloper-experience

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:

ResourceEndpointsv2 Coveragev3 Coverage
Payments54/55/5
Apps42/44/4
Webhooks (API key)20/22/2
Countries20/22/2
Currencies20/22/2
Customers40/44/4
Invoices30/33/3
Checkout20/22/2
Discovery10/10/1
Webhooks (session)60/60/6
Total286 (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 structures

New 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-php

Architecture

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.php

Exception 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.21

No 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

SDKVersionPackage ManagerAsync PatternDependencies
TypeScript3.0.0npmasync/await0
Python3.0.0pipsync (async optional)requests
PHP3.0.0Composersync0 (native cURL)
Go3.0.0go modulescontext.Context0
Rust3.0.0crates.iotokio async/awaitreqwest, serde
Java3.0.0Mavensync0
Flutter/Dart3.0.0pub.devFuture-basedhttp, crypto
React Native3.0.0npmReact hookswraps 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.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles