Back to 0fee
0fee

The Developer Console: Tabs, Status Checker, and Widget Preview

How we built the 0fee.dev Developer Console with interactive status checker, widget preview, and downloadable templates. By Juste A. Gnimavo and Claude.

Thales & Claude | March 25, 2026 12 min 0fee
developer-consolecheckout-widgetdeveloper-experiencesolidjs

The Developer Console is the central hub for integration in the 0fee.dev dashboard. It is where developers go to learn how the API works, test their integration, preview the checkout widget, and download starter templates. Over Sessions 074 and 076, we consolidated the console from five tabs to four, added rich status endpoints, built an interactive status checker, created a live widget preview, and shipped a downloadable HTML checkout template.

This article covers the architecture of the Developer Console, the design decisions behind each tab, and the implementation details that make the integration experience smooth.

The Consolidation Problem

Before Session 074, the Developer Console had five tabs:

  1. Quick Start
  2. Hosted Checkout
  3. Widget
  4. Direct Integration
  5. API Reference

The problem: Quick Start and Hosted Checkout were nearly identical. Both explained how to create a checkout session via POST /v1/payments, both showed the same code examples, both linked to the same API endpoint. The only difference was the framing -- one said "start here" and the other said "redirect your customer." Developers were confused about which tab to use.

The consolidation merged these into a single Quick Start tab with a "How It Works" flow diagram, reducing the tab count to four:

TabPurpose
Quick StartCreate your first payment, status checking, how the flow works
Direct IntegrationServer-to-server integration, downloadable HTML template
WidgetCheckout.js widget configuration, live preview, framework code
API ReferenceLink to full documentation, key endpoint summaries

The final tab order was reordered in Session 076 to match the developer learning path: Quick Start, Direct Integration, Widget, API Reference.

Rich Status Endpoints

Session 074 enhanced the status endpoints to return full session and transaction data. Previously, the status endpoints returned minimal responses:

json// BEFORE: GET /v1/checkout/:id/status
{
  "status": "completed"
}

This was useless for debugging. A developer polling for status had no context about what completed, when, or for how much. The enhanced endpoints return everything:

json// AFTER: GET /v1/checkout/:id/status
{
  "status": "completed",
  "session_id": "cs_abc123def456",
  "amount": 5000,
  "currency": "XOF",
  "customer": {
    "phone": "+2250709757296",
    "email": null,
    "name": null
  },
  "payment_reference": "ORDER-42",
  "invoice_reference": "INV-2025-0001",
  "checkout_url": "https://pay.0fee.dev/checkout/cs_abc123def456",
  "success_url": "https://merchant.com/success",
  "cancel_url": "https://merchant.com/cancel",
  "expires_at": "2025-12-27T15:30:00Z",
  "created_at": "2025-12-27T14:30:00Z"
}

The transaction status endpoint received the same treatment:

json// AFTER: GET /v1/payments/public/:id/status
{
  "status": "completed",
  "source_amount": 5000,
  "source_currency": "XOF",
  "destination_amount": 5000,
  "destination_currency": "XOF",
  "payment_method": "PAYIN_ORANGE_CI",
  "payment_reference": "ORDER-42",
  "invoice_reference": "INV-2025-0001",
  "customer": {
    "phone": "+2250709757296"
  }
}

These endpoints are public (no authentication required) because they are called from the client-side checkout page. The data they expose is limited to what the customer would already know -- their own phone number, the amount they paid, and the references the merchant chose to share.

Backend Implementation

python# backend/routes/checkout_hosted.py
@router.get("/v1/checkout/{session_id}/status")
async def get_checkout_status(session_id: str, db: AsyncSession = Depends(get_db)):
    """Return rich status data for a checkout session."""
    session = await db.execute(
        select(CheckoutSession).where(CheckoutSession.id == session_id)
    )
    session = session.scalar_one_or_none()

    if not session:
        raise HTTPException(status_code=404, detail="Session not found")

    return {
        "status": session.status,
        "session_id": session.id,
        "amount": session.amount,
        "currency": session.currency,
        "customer": {
            "phone": session.customer_phone,
            "email": session.customer_email,
            "name": session.customer_name,
        },
        "payment_reference": session.payment_reference,
        "invoice_reference": session.invoice_reference,
        "checkout_url": f"https://pay.0fee.dev/checkout/{session.id}",
        "success_url": session.success_url,
        "cancel_url": session.cancel_url,
        "expires_at": session.expires_at.isoformat() if session.expires_at else None,
        "created_at": session.created_at.isoformat(),
    }

The Interactive Status Checker

Instead of showing static code examples for status checking, the Developer Console provides live input fields that make actual API calls:

typescript// State for the status checker
const [sessionIdInput, setSessionIdInput] = createSignal("");
const [transactionIdInput, setTransactionIdInput] = createSignal("");
const [sessionStatus, setSessionStatus] = createSignal<any>(null);
const [transactionStatus, setTransactionStatus] = createSignal<any>(null);
const [statusLoading, setStatusLoading] = createSignal(false);

async function checkSessionStatus() {
  const id = sessionIdInput();
  if (!id) return;

  setStatusLoading(true);
  try {
    const response = await fetch(`/api/v1/checkout/${id}/status`);
    const data = await response.json();
    setSessionStatus(data);
  } catch (error) {
    setSessionStatus({ error: "Failed to fetch status" });
  } finally {
    setStatusLoading(false);
  }
}

async function checkTransactionStatus() {
  const id = transactionIdInput();
  if (!id) return;

  setStatusLoading(true);
  try {
    const response = await fetch(`/api/v1/payments/public/${id}/status`);
    const data = await response.json();
    setTransactionStatus(data);
  } catch (error) {
    setTransactionStatus({ error: "Failed to fetch status" });
  } finally {
    setStatusLoading(false);
  }
}

The UI renders two checker panels side by side:

+----------------------------------+  +----------------------------------+
| Session Status Checker           |  | Transaction Status Checker       |
|                                  |  |                                  |
| Session ID: [cs_____________]    |  | Transaction ID: [txn__________]  |
|                [Check Status]    |  |                   [Check Status] |
|                                  |  |                                  |
| {                                |  | {                                |
|   "status": "completed",        |  |   "status": "completed",        |
|   "session_id": "cs_abc123",    |  |   "source_amount": 5000,        |
|   "amount": 5000,               |  |   "source_currency": "XOF",     |
|   "currency": "XOF",            |  |   "payment_method": "ORANGE_CI",|
|   ...                            |  |   ...                            |
| }                                |  | }                                |
+----------------------------------+  +----------------------------------+

The JSON response is displayed with the same recursive syntax highlighter from the API Playground. Developers can paste a session or transaction ID from their logs, click Check Status, and see the full response instantly. No cURL required.

The "How It Works" Flow Diagram

The Quick Start tab opens with a 3-step flow diagram that explains the payment lifecycle:

+-------------------+     +-------------------+     +-------------------+
|                   |     |                   |     |                   |
|   1. Create       |     |   2. Customer     |     |   3. Webhook      |
|   Payment         | --> |   Pays            | --> |   Notification    |
|                   |     |                   |     |                   |
|   POST /v1/       |     |   Redirect to     |     |   POST to your    |
|   payments        |     |   checkout URL    |     |   webhook URL     |
|                   |     |                   |     |                   |
+-------------------+     +-------------------+     +-------------------+

Each step includes a brief description and a link to the relevant documentation section. The diagram replaced a wall of text that explained the same flow in prose. Visual communication wins.

The Widget Tab

The Widget tab is where the Developer Console becomes interactive. It does not just describe the checkout widget -- it lets developers configure and launch it.

Widget Configuration

Three input fields control the widget parameters:

typescriptconst [widgetAmount, setWidgetAmount] = createSignal(5000);
const [widgetCurrency, setWidgetCurrency] = createSignal("XOF");
const [widgetReference, setWidgetReference] = createSignal("TEST-001");
const [widgetScriptLoaded, setWidgetScriptLoaded] = createSignal(false);
const [activeFramework, setActiveFramework] = createSignal("vanilla");
const [widgetResult, setWidgetResult] = createSignal<any>(null);

The configuration panel:

Widget Configuration
+--------------------------------------------------+
| Amount:     [5000        ]                       |
| Currency:   [XOF  v]                             |
| Reference:  [TEST-001    ]                       |
|                                                  |
| [Try Widget Live]                                |
+--------------------------------------------------+

"Try Widget Live"

Clicking the button loads the checkout.js script on-demand and opens the actual checkout modal:

typescriptasync function loadWidgetScript(): Promise<void> {
  if (widgetScriptLoaded()) return;

  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = "https://pay.0fee.dev/checkout.js";
    script.onload = () => {
      setWidgetScriptLoaded(true);
      resolve();
    };
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

async function tryWidgetLive(): Promise<void> {
  await loadWidgetScript();

  // @ts-ignore -- ZeroFeeCheckout is loaded dynamically
  const checkout = new ZeroFeeCheckout({
    apiKey: sandboxKey(),
    amount: widgetAmount(),
    currency: widgetCurrency(),
    reference: widgetReference(),
    onSuccess: (result: any) => {
      setWidgetResult({ type: "success", data: result });
    },
    onError: (error: any) => {
      setWidgetResult({ type: "error", data: error });
    },
    onClose: () => {
      setWidgetResult({ type: "closed" });
    },
  });

  checkout.open();
}

This is not a mockup. Clicking "Try Widget Live" opens the real checkout modal, connected to the sandbox environment, processing test payments. Developers see exactly what their customers will see -- the payment method selector, the phone number input, the processing spinner, the success confirmation.

Framework Tabs

Below the live preview, framework-specific integration code is displayed in tabs:

Vanilla JS:

html<script src="https://pay.0fee.dev/checkout.js"></script>
<script>
  const checkout = new ZeroFeeCheckout({
    apiKey: "sk_test_your_key_here",
    amount: 5000,
    currency: "XOF",
    reference: "ORDER-42",
    onSuccess: function(result) {
      console.log("Payment succeeded:", result);
      window.location.href = "/success";
    },
    onError: function(error) {
      console.error("Payment failed:", error);
    },
    onClose: function() {
      console.log("Widget closed");
    }
  });

  document.getElementById("pay-btn").onclick = function() {
    checkout.open();
  };
</script>

React:

jsximport { useEffect, useRef } from "react";

function CheckoutButton({ amount, currency, reference }) {
  const checkoutRef = useRef(null);

  useEffect(() => {
    const script = document.createElement("script");
    script.src = "https://pay.0fee.dev/checkout.js";
    script.onload = () => {
      checkoutRef.current = new ZeroFeeCheckout({
        apiKey: "sk_test_your_key_here",
        amount,
        currency,
        reference,
        onSuccess: (result) => console.log("Success:", result),
        onError: (error) => console.error("Error:", error),
      });
    };
    document.head.appendChild(script);
  }, []);

  return (
    <button onClick={() => checkoutRef.current?.open()}>
      Pay {amount} {currency}
    </button>
  );
}

The code examples are not static strings. They are generated dynamically from the widget configuration state:

typescriptfunction getVanillaCode(): string {
  return `<script src="https://pay.0fee.dev/checkout.js"></script>
<script>
  const checkout = new ZeroFeeCheckout({
    apiKey: "${sandboxKey()}",
    amount: ${widgetAmount()},
    currency: "${widgetCurrency()}",
    reference: "${widgetReference()}",
    onSuccess: function(result) {
      console.log("Payment succeeded:", result);
    },
    onError: function(error) {
      console.error("Payment failed:", error);
    }
  });
</script>`;
}

If a developer changes the amount to 10000, the code examples update to show amount: 10000. If they change the currency to EUR, the code shows currency: "EUR". Copy-paste-ready.

The Downloadable HTML Template

The Direct Integration tab includes a "Download Template" button that generates a complete, self-contained HTML file (~920 lines) implementing the full checkout flow. This is the fastest path from "I signed up" to "I have a working checkout page."

The Template Generator

typescript// frontend/src/utils/checkoutTemplate.ts
export function generateCheckoutTemplate(apiKey: string): string {
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Checkout - Powered by 0fee.dev</title>
  <style>
    /* Complete embedded CSS -- no external dependencies */
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
    /* ... ~200 lines of styles */
  </style>
</head>
<body>
  <div id="checkout">
    <!-- Step 1: Amount and method selection -->
    <!-- Step 2: Customer details -->
    <!-- Step 3: Processing -->
    <!-- Step 4: Result -->
  </div>
  <script>
    const API_KEY = "${apiKey}";
    const API_URL = "https://api.0fee.dev/v1";
    /* ... ~400 lines of checkout logic */
  </script>
</body>
</html>`;
}

The template includes:

FeatureImplementation
Payment method selectorGrid of available methods, fetched from the API
Phone number inputWith country code prefix
Card input fieldsAuto-formatting for number, expiry, CVV
Processing spinnerAnimated, with status polling
Success/failure displayWith transaction ID and reference
Responsive designWorks on mobile and desktop
Dark modeRespects prefers-color-scheme
Error handlingNetwork errors, validation errors, timeout handling

All ~920 lines in a single HTML file. No build tools, no npm, no framework. A developer can download it, replace the API key, and host it anywhere -- static hosting, S3, even a local file.

The Demo Key Endpoint

To make the download work in sandbox mode without requiring the developer to find their API key, Session 076 added a dedicated endpoint:

python# backend/routes/checkout.py
@router.get("/v1/checkout/demo-key")
async def get_demo_key():
    """Return a sandbox API key for demo purposes."""
    return {"key": config.PLATFORM_DEMO_SANDBOX_KEY}

The template download fetches this key and embeds it in the HTML file. The developer gets a working checkout page immediately, without any configuration.

Widget Callbacks Reference

The Widget tab documents the three callback functions that drive the integration:

typescriptinterface WidgetCallbacks {
  onSuccess: (result: {
    transactionId: string;
    status: "completed";
    amount: number;
    currency: string;
    reference: string;
  }) => void;

  onError: (error: {
    code: string;
    message: string;
    transactionId?: string;
  }) => void;

  onClose: () => void;
}
CallbackWhen it firesWhat to do
onSuccessPayment completed successfullyRedirect to success page, update UI, verify server-side
onErrorPayment failed or was declinedShow error message, offer retry
onCloseCustomer closed the widget without completingResume shopping, offer alternative

The documentation emphasizes that onSuccess should always be verified server-side via the webhook or status endpoint. Client-side callbacks can be spoofed.

Widget Parameters Reference

ParameterTypeRequiredDescription
apiKeystringYesYour sandbox or live API key
amountnumberYesAmount in smallest currency unit
currencystringYesISO 4217 currency code
referencestringNoYour internal reference for the payment
customer.phonestringNoPre-fill customer phone number
customer.emailstringNoPre-fill customer email
customer.namestringNoPre-fill customer name
localestringNoLanguage code (en, fr). Defaults to browser language
themestringNo"light" or "dark". Defaults to system preference

Lessons Learned

Consolidate duplicate tabs early. The five-tab Developer Console confused developers who could not tell the difference between Quick Start and Hosted Checkout. The consolidation to four tabs was obvious in hindsight.

Rich status endpoints cost nothing but save hours. Adding 10 fields to a status response took 15 minutes of backend work. It saves every developer integrating the API from making additional calls to get context they need for debugging.

Live previews beat documentation. A developer who can click "Try Widget Live" and see the actual checkout modal understands the widget in 10 seconds. The same understanding from reading documentation takes 10 minutes.

Downloadable templates are the fastest onboarding. The ~920-line HTML template is the most direct path from signup to working checkout. No framework, no build tools, no dependencies. Download, open, pay.


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