Back to 0fee
0fee

API Documentation: 90+ Endpoints Fully Documented

How we documented 90+ API endpoints across 18 route modules with scroll-spy navigation and multi-language code examples. By Juste A. Gnimavo and Claude.

Thales & Claude | March 25, 2026 13 min 0fee
documentationapi-referencedeveloper-experiencesolidjs

API documentation is not a feature. It is the product. For a payment orchestration platform, the API is the primary interface. Every merchant, every developer, every integration partner interacts with 0fee.dev through HTTP endpoints. If the documentation is incomplete, ambiguous, or hard to navigate, the platform is unusable -- regardless of how well the backend works.

In Session 019 (December 12, 2025), we transformed the documentation page from a sparse 400-line placeholder into a comprehensive 1,006-line interactive reference covering 90+ endpoints across 18 route modules. This article breaks down the architecture, the content strategy, and the technical details of building documentation that developers actually want to read.

The Before State

Before Session 019, Docs.tsx was a minimal page. It listed maybe a dozen endpoints with basic descriptions and a few code snippets. No sidebar navigation. No language tabs. No syntax highlighting. No error codes reference. No search. A developer trying to integrate the Currency API or the Credits API would find nothing.

The backend had grown to 18 route modules with 90+ endpoints, but the documentation had not kept pace. This is the classic documentation debt problem -- every new feature ships without updated docs because the docs page is hard to maintain.

The After State

MetricBeforeAfter
Lines of code~4001,006
Endpoints documented~1290+
Route modules covered418
Language tabs03 (TypeScript, Python, cURL)
NavigationNoneScroll-spy sidebar
Error codes referenceNoYes
Interactive examplesNoYes, with copy buttons

Architecture

The documentation page is a single SolidJS component with three main sections:

Docs.tsx (1,006 lines)
├── Sidebar (scroll-spy navigation)
│   ├── Section links (18 sections)
│   └── Active section indicator
├── Content (scrollable main area)
│   ├── Introduction
│   ├── Authentication
│   ├── 18 API sections
│   │   ├── Section header
│   │   ├── Endpoint cards
│   │   │   ├── Method badge
│   │   │   ├── Path
│   │   │   ├── Description
│   │   │   ├── Language tabs (TS / Python / cURL)
│   │   │   ├── Request/response examples
│   │   │   └── Copy button
│   │   └── Parameter tables
│   └── Error codes reference
└── State management
    ├── Active section (scroll position)
    └── Selected language (per-section)

The 18 Route Modules

The documentation covers every route module in the backend:

ModuleEndpointsDescription
auth.py5Registration, login, OTP verification, token refresh
payments.py8Create, get, list, cancel, refund, status, search
webhooks.py6CRUD operations, event listing, test delivery
checkout.py5Session creation, retrieval, status, hosted page
analytics.py5Overview, volume charts, transaction stats, customer stats
billing.py4Summary, invoices, account status, usage
credits.py6Balance, history, top-up, fee estimation, tiers, usage
currency.py5List currencies, exchange rates, convert, regions, supported
invoices.py6Create, get, list, update, delete, HTML export
payment_links.py5Create, get, list, update, deactivate
customers.py5Create, get, list, update, payment history
profile.py4Get profile, update, sessions, API usage
payin_methods.py3List methods, get method details, methods by country
countries.py3List countries, get country, providers by country
apps.py5Create app, get, list, update, rotate keys
oauth.py4Authorize, callback, token, revoke
pay.py3Render payment link page, process, status
health.py3Health check, version, status

Total: 90+ endpoints, each documented with method, path, description, parameters, request body, and response example.

Scroll-Spy Sidebar

The sidebar tracks the user's scroll position and highlights the currently visible section. This is essential for a long documentation page -- without it, developers lose their place.

typescriptconst [activeSection, setActiveSection] = createSignal("introduction");

// Section definitions for the sidebar
const sections = [
  { id: "introduction", label: "Introduction" },
  { id: "authentication", label: "Authentication" },
  { id: "payments", label: "Payments" },
  { id: "checkout", label: "Checkout" },
  { id: "webhooks", label: "Webhooks" },
  { id: "analytics", label: "Analytics" },
  { id: "billing", label: "Billing" },
  { id: "credits", label: "Credits" },
  { id: "currency", label: "Currency" },
  { id: "invoices", label: "Invoices" },
  { id: "payment-links", label: "Payment Links" },
  { id: "customers", label: "Customers" },
  { id: "profile", label: "Profile" },
  { id: "payin-methods", label: "Payment Methods" },
  { id: "countries", label: "Countries" },
  { id: "apps", label: "Apps" },
  { id: "oauth", label: "OAuth" },
  { id: "errors", label: "Error Codes" },
];

onMount(() => {
  const observer = new IntersectionObserver(
    (entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting) {
          setActiveSection(entry.target.id);
        }
      }
    },
    {
      rootMargin: "-20% 0px -80% 0px",
      threshold: 0,
    }
  );

  sections.forEach(({ id }) => {
    const element = document.getElementById(id);
    if (element) observer.observe(element);
  });

  onCleanup(() => observer.disconnect());
});

The rootMargin values are chosen to trigger the active state when a section header is in the upper portion of the viewport. The -20% 0px -80% 0px means the intersection is detected when the element is in the top 20% of the visible area. This creates natural "snapping" behavior as the user scrolls.

The sidebar renders as a fixed column:

tsx<nav class="sidebar">
  <For each={sections}>
    {(section) => (
      <a
        href={`#${section.id}`}
        class={activeSection() === section.id ? "active" : ""}
        onClick={(e) => {
          e.preventDefault();
          document.getElementById(section.id)?.scrollIntoView({
            behavior: "smooth",
          });
        }}
      >
        {section.label}
      </a>
    )}
  </For>
</nav>

Language Tabs

Every endpoint includes code examples in three languages: TypeScript (using the 0fee SDK), Python (using the 0fee SDK), and cURL (raw HTTP). Developers choose their preferred language, and the selection persists across sections.

typescriptconst [selectedLang, setSelectedLang] = createSignal<"typescript" | "python" | "curl">("typescript");

A single endpoint documented in all three languages:

TypeScript:

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

const zf = new ZeroFee({ apiKey: "sk_test_..." });

const payment = await zf.payments.create({
  amount: 5000,
  sourceCurrency: "XOF",
  paymentMethod: "PAYIN_ORANGE_CI",
  customer: { phone: "+2250709757296" },
});

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

Python:

pythonfrom zerofee import ZeroFee

zf = ZeroFee(api_key="sk_test_...")

payment = zf.payments.create(
    amount=5000,
    source_currency="XOF",
    payment_method="PAYIN_ORANGE_CI",
    customer={"phone": "+2250709757296"},
)

print(payment.id)           # txn_abc123
print(payment.checkout_url) # https://pay.0fee.dev/...

cURL:

bashcurl -X POST https://api.0fee.dev/v1/payments \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 5000,
    "sourceCurrency": "XOF",
    "paymentMethod": "PAYIN_ORANGE_CI",
    "customer": {"phone": "+2250709757296"}
  }'

Syntax Highlighting

Code examples use a custom syntax highlighter that applies colors based on the language:

typescriptfunction highlightCode(code: string, language: string): string {
  if (language === "typescript" || language === "python") {
    return code
      .replace(
        /\b(import|from|const|let|var|await|async|function|return|if|else|print)\b/g,
        '<span class="keyword">$1</span>'
      )
      .replace(
        /(["'`])(?:(?!\1).)*\1/g,
        '<span class="string">$&</span>'
      )
      .replace(
        /\/\/.*/g,
        '<span class="comment">$&</span>'
      )
      .replace(
        /#.*/g,
        '<span class="comment">$&</span>'
      );
  }
  if (language === "curl") {
    return code
      .replace(
        /\b(curl)\b/g,
        '<span class="keyword">$1</span>'
      )
      .replace(
        /(-[A-Za-z]+)/g,
        '<span class="flag">$1</span>'
      )
      .replace(
        /(["'])(?:(?!\1).)*\1/g,
        '<span class="string">$&</span>'
      );
  }
  return code;
}

This is a simple regex-based highlighter -- not a full parser. It handles the common cases (keywords, strings, comments, flags) well enough for documentation purposes. The API Playground's JSON highlighter uses the recursive approach because JSON has nested structures that regex cannot handle correctly. For code snippets in documentation, regex is sufficient.

Endpoint Method Badges

Each endpoint displays a colored badge indicating the HTTP method:

tsxfunction MethodBadge(props: { method: string }) {
  const colors: Record<string, string> = {
    GET: "bg-blue-500",
    POST: "bg-green-500",
    PATCH: "bg-yellow-500",
    DELETE: "bg-red-500",
    PUT: "bg-orange-500",
  };

  return (
    <span class={`method-badge ${colors[props.method] || "bg-gray-500"}`}>
      {props.method}
    </span>
  );
}

The color coding is standard across the industry:

MethodColorSemantic
GETBlueRead data
POSTGreenCreate data
PATCHYellowUpdate data
DELETERedRemove data

Developers scan the badge color before reading the path, which accelerates navigation through the endpoint list.

Interactive Copy Buttons

Every code example has a copy button that copies the code to the clipboard:

typescriptfunction CopyButton(props: { text: string }) {
  const [copied, setCopied] = createSignal(false);

  async function handleCopy() {
    await navigator.clipboard.writeText(props.text);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  }

  return (
    <button class="copy-btn" onClick={handleCopy}>
      {copied() ? "Copied!" : "Copy"}
    </button>
  );
}

The "Copied!" feedback disappears after 2 seconds. Small detail, large usability impact. Without the feedback, developers click the button and are not sure if anything happened.

The 7 New Sections

Session 019 added 7 API sections that were missing entirely from the documentation:

Analytics API

GET  /v1/analytics/overview         Dashboard overview stats
GET  /v1/analytics/volume           Transaction volume chart data
GET  /v1/analytics/transactions     Transaction statistics (by status, method, provider)
GET  /v1/analytics/customers        Customer acquisition and retention stats
GET  /v1/analytics/revenue          Revenue breakdown by currency and method

Billing API

GET  /v1/billing/summary            Billing summary (current period charges, credits used)
GET  /v1/billing/invoices           List billing invoices
GET  /v1/billing/status             Account billing status (active, suspended, grace period)
GET  /v1/billing/usage              Detailed usage breakdown

Credits API

GET  /v1/credits/balance            Current credit balance
GET  /v1/credits/history            Credit transaction history (top-ups, deductions)
POST /v1/credits/topup              Add credits to account
GET  /v1/credits/fee-estimate       Estimate fee for a given amount and method
GET  /v1/credits/tiers              Credit tier pricing and thresholds
GET  /v1/credits/usage              Credit usage statistics

Currency API

GET  /v1/currencies                 List supported currencies
GET  /v1/currencies/rates           Current exchange rates
POST /v1/currencies/convert         Convert amount between currencies
GET  /v1/currencies/regions         Currencies grouped by region
GET  /v1/currencies/:code           Currency details (symbol, decimals, countries)

Invoices API

POST   /v1/invoices                 Create an invoice
GET    /v1/invoices/:id             Get invoice by ID
GET    /v1/invoices                 List invoices (with filters)
PATCH  /v1/invoices/:id             Update invoice
DELETE /v1/invoices/:id             Delete invoice
GET    /v1/invoices/:id/html        Export invoice as HTML (for PDF generation)

Payment Links API

POST  /v1/payment-links             Create a payment link
GET   /v1/payment-links/:id         Get payment link
GET   /v1/payment-links             List payment links
PATCH /v1/payment-links/:id         Update payment link
POST  /v1/payment-links/:id/deactivate  Deactivate a payment link

Profile API

GET   /v1/profile                   Get current user profile
PATCH /v1/profile                   Update profile (name, email, company)
GET   /v1/profile/sessions          List active sessions
GET   /v1/profile/usage             API usage statistics

Each section includes request and response examples in all three languages, parameter tables with types and descriptions, and notes on pagination, filtering, and error handling.

Error Codes Reference

The documentation ends with a comprehensive error codes table:

CodeHTTP StatusDescription
auth_required401Missing or invalid API key
auth_expired401API key or session has expired
forbidden403Insufficient permissions for this operation
not_found404Resource does not exist
validation_error400Request body failed validation
duplicate409Resource already exists (e.g., duplicate idempotency key)
insufficient_credits402Not enough credits to process the payment
provider_error502Payment provider returned an error
provider_timeout504Payment provider did not respond in time
rate_limited429Too many requests. Retry after the indicated delay
suspended403Account is suspended due to unpaid billing
currency_unsupported400The requested currency is not supported
method_unavailable400The payment method is not available in the target country
amount_too_low400Amount is below the minimum for the payment method
amount_too_high400Amount exceeds the maximum for the payment method

Each error code includes the HTTP status, a machine-readable code string, and a human-readable description. The machine-readable codes are stable and can be used in error handling logic:

typescripttry {
  const payment = await zf.payments.create({ amount: 10, sourceCurrency: "XOF" });
} catch (error) {
  switch (error.code) {
    case "insufficient_credits":
      // Redirect to top-up page
      break;
    case "amount_too_low":
      // Show minimum amount to user
      break;
    case "provider_error":
      // Retry with a different provider
      break;
    default:
      // Generic error handling
      break;
  }
}

The Content Strategy

Writing documentation for 90+ endpoints is a volume problem. The strategy was:

  1. Start with the backend routes. Every FastAPI route has a docstring, parameter types, and response models. These are the source of truth.
  1. Group by domain, not by module. Developers think in terms of "I want to create a payment" or "I want to check analytics," not "I need the payments.py module." The documentation groups by business domain.
  1. Show the happy path first. Every endpoint example shows a successful request and response. Error cases are documented separately in the error codes reference.
  1. Three languages, same structure. TypeScript and Python examples use the SDK. cURL examples show raw HTTP. All three produce identical results. A developer can switch languages and see the same operation expressed differently.
  1. Parameter tables are mandatory. Every endpoint lists its parameters with type, required/optional, and description. No "see the source code for details."

The SDK Reference Connection

The documentation page links to SDK-specific methods for each endpoint. When viewing the Payments section, developers see:

SDK Methods:
  TypeScript: zf.payments.create(params)
  Python:     zf.payments.create(**params)
  Go:         client.Payments.Create(ctx, params)
  Ruby:       client.payments.create(params)
  PHP:        $client->payments->create($params)
  Java:       client.payments().create(params)
  C#:         client.Payments.CreateAsync(params)

This bridges the gap between "I see the REST endpoint" and "How do I call it from my language."

Build Verification

After the rewrite, the frontend build completed successfully:

65 modules compiled
Build time: 5.86s
JS output: 427.70 kB (107.28 kB gzip)
CSS output: 101.92 kB (14.68 kB gzip)

No TypeScript errors. The 1,006-line component compiles cleanly despite its size because SolidJS components are just functions -- there is no class hierarchy or lifecycle complexity that would cause type issues at scale.

Lessons Learned

Documentation is the first thing developers judge. Before trying the API, before reading the pricing page, developers open the docs. If the docs look incomplete, they assume the API is incomplete. The transformation from 400 to 1,006 lines was not just a documentation improvement -- it was a credibility improvement.

Scroll-spy is not optional for long pages. A 90+ endpoint reference page without navigation is a wall of text. The scroll-spy sidebar turns it into a browsable reference where developers can jump to any section in one click.

Three-language examples triple the maintenance cost but are worth it. When an endpoint changes, three code examples need updating. But developers overwhelmingly prefer seeing code in their language over mentally translating from a language they do not use.

A single-component architecture works at 1,000 lines. The documentation page is one SolidJS component with one file. No sub-components, no separate data files, no abstractions. For a page that is essentially a structured document with interactive elements, a single component is easier to maintain than a component tree.

Error codes are an API feature. Documenting error codes is not a documentation task -- it is an API design task. Every error code is a contract. insufficient_credits tells the developer exactly what happened and what to do about it. internal_server_error tells them nothing.


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