Developer onboarding is the single biggest lever in payment API adoption. If a developer cannot process a test payment within ten minutes of signing up, they leave. They have alternatives. Stripe nails this. PayPal does not. We wanted 0fee.dev to be in the first category.
Across Sessions 029 and 039, we built a guided onboarding stepper that walks developers from account creation to their first live payment -- with auto-verification, visual progress tracking, and a sandbox environment that requires zero configuration.
The Problem We Were Solving
Before the onboarding stepper, new developers landed on the dashboard and faced a blank canvas. There were sidebar links to Apps, API Keys, Payments, Webhooks, SDKs -- but no guidance on where to start. The documentation existed, but forcing developers to read docs before touching the product is a known conversion killer.
We needed a step-by-step flow that would:
- Guide developers through the minimum viable integration path.
- Auto-detect when each step was completed (no manual checkboxes).
- Work for developers targeting any country, not just one payment ecosystem.
- Provide a clean sandbox reset for repeated testing.
The Six Steps
After several iterations, we settled on five required steps and one optional step:
| Step | Name | Type | Auto-Verify |
|---|---|---|---|
| 1 | Create Your First App | Required | Yes |
| 2 | Get Your API Keys | Required | Yes |
| 3 | Make a Test Payment | Required | Yes |
| 4 | Configure Providers | Required | Yes |
| 5 | Go Live | Required | No |
| 6 | Configure Webhooks | Optional | Yes |
The optional webhook step appears at the end with a gray background and an "Optional" badge. Webhooks are important for production integrations, but they are not necessary to validate that the platform works.
The Stripe Step That Got Removed
In Session 029, the original implementation included a dedicated "Add Stripe Test Keys" step. This made sense early on -- Stripe was the first provider we integrated, and our initial audience was Francophone Africa where we were personally testing with Stripe sandbox credentials.
By Session 039, we realized this was wrong for a platform targeting developers worldwide. An Indonesian developer using Xendit or a Nigerian developer using Paystack has no use for a Stripe-specific onboarding step. The test provider (covered in Article 021) already supports all 115+ payment methods in sandbox mode, making Stripe sandbox keys unnecessary for onboarding.
We removed the Stripe step and replaced it with a sandbox info box at the API Keys step, explaining that the built-in test provider handles all payment methods automatically.
Code Snippet Internationalization
The original test payment code snippet used XOF (West African CFA Franc) and PAYIN_ORANGE_CI (Orange Money, Cote d'Ivoire) as the example:
python# Before: Session 029 (Africa-specific)
response = requests.post(
"https://api.0fee.dev/v1/payments",
json={
"amount": 5000,
"source_currency": "XOF",
"payment_method": "PAYIN_ORANGE_CI"
}
)In Session 039, we changed it to USD and a generic card payment, which resonates with a global developer audience:
python# After: Session 039 (international)
import requests
response = requests.post(
"https://api.0fee.dev/v1/payments",
headers={"Authorization": "Bearer sk_test_..."},
json={
"amount": 1000,
"source_currency": "USD",
"payment_reference": "test-001"
}
)
print(response.json()["checkout_url"])This is a small change that signals a large shift in positioning. 0fee.dev is Africa-first, but it is not Africa-only.
The Progress Bar
The visual progress indicator was a Session 039 addition that transformed the page from a task list into a journey.
Architecture
The progress bar is a horizontal layout of step indicators connected by lines. Each indicator is a circle with a number, a label below it, and a connecting line to the next step.
tsx// Step indicator states
type StepState = "completed" | "current" | "pending";
// Visual mapping
const stepStyles: Record<StepState, string> = {
completed: "bg-emerald-500 text-white border-emerald-500",
current: "bg-white text-emerald-500 border-emerald-500 ring-2 ring-emerald-200",
pending: "bg-gray-100 text-gray-400 border-gray-300"
};
// Connecting line colors
const lineStyles: Record<string, string> = {
completed: "bg-emerald-500", // solid green
pending: "bg-gray-200" // light gray
};Step Labels
Each step has a short label that fits beneath its circle indicator. We defined these in a mapping to keep them concise:
tsxconst stepLabels: Record<string, string> = {
create_app: "Create App",
get_api_keys: "API Keys",
test_payment: "Test Payment",
configure_providers: "Providers",
go_live: "Go Live",
configure_webhooks: "Webhooks"
};The progress percentage is calculated from completed steps divided by total required steps (not including the optional webhook step in the denominator).
Auto-Verification Polling
The most technically interesting part of the onboarding stepper is the auto-verification system. Instead of asking developers to click a "I did this" button, we poll the backend every five seconds to check whether each step's completion condition has been met.
Backend Endpoint
python# backend/routes/onboarding.py
@router.get("/status")
async def get_onboarding_status(
app: App = Depends(get_current_app),
db: AsyncSession = Depends(get_db)
):
"""Check completion status of all onboarding steps."""
steps = []
# Step 1: Create App (always true if they reach this endpoint)
steps.append({
"id": "create_app",
"completed": True,
"auto_verify": True
})
# Step 2: API Keys (check if keys have been viewed/copied)
api_key_viewed = await check_api_key_viewed(db, app.id)
steps.append({
"id": "get_api_keys",
"completed": api_key_viewed,
"auto_verify": True,
"sandbox_info": True # Show built-in sandbox provider info
})
# Step 3: Test Payment (check if any sandbox transaction exists)
has_test_payment = await check_sandbox_payment(db, app.id)
steps.append({
"id": "test_payment",
"completed": has_test_payment,
"auto_verify": True
})
# Step 4: Configure Providers (check if any provider is configured)
has_provider = await check_provider_configured(db, app.id)
steps.append({
"id": "configure_providers",
"completed": has_provider,
"auto_verify": True
})
# Step 5: Go Live (manual - developer must switch to live mode)
is_live = app.mode == "live"
steps.append({
"id": "go_live",
"completed": is_live,
"auto_verify": False
})
# Step 6: Webhooks (optional)
has_webhook = await check_webhook_configured(db, app.id)
steps.append({
"id": "configure_webhooks",
"completed": has_webhook,
"auto_verify": True,
"optional": True
})
completed_required = sum(
1 for s in steps if s["completed"] and not s.get("optional")
)
total_required = sum(1 for s in steps if not s.get("optional"))
return {
"steps": steps,
"progress": completed_required / total_required * 100,
"completed": completed_required == total_required
}Frontend Polling
tsx// GetStarted.tsx - Auto-verification polling
const [steps, setSteps] = createSignal<OnboardingStep[]>([]);
const [progress, setProgress] = createSignal(0);
let pollInterval: ReturnType<typeof setInterval>;
onMount(() => {
fetchStatus();
pollInterval = setInterval(fetchStatus, 5000); // Poll every 5 seconds
});
onCleanup(() => {
clearInterval(pollInterval);
});
async function fetchStatus() {
const response = await api.get("/onboarding/status");
if (response.ok) {
const data = await response.json();
setSteps(data.steps);
setProgress(data.progress);
// Stop polling when all required steps are complete
if (data.completed) {
clearInterval(pollInterval);
}
}
}The polling stops automatically once all required steps are marked complete. This prevents unnecessary network traffic for developers who have already finished onboarding.
Persistence
Step completion state is persisted server-side in the database, not in localStorage. This means if a developer starts onboarding on their laptop and continues on their desktop, progress is preserved. The backend checks actual platform state (does an app exist? are there sandbox transactions? is a provider configured?) rather than storing a separate "onboarding progress" record.
This approach eliminates state synchronization bugs entirely. If a developer deletes their app and creates a new one, the onboarding stepper reflects reality.
The Sandbox Info Box
When a developer reaches the API Keys step, they see an informational box explaining 0fee.dev's built-in sandbox system:
tsx{step.sandbox_info && (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-semibold text-blue-800">
Built-in Sandbox Provider
</h4>
<p className="text-blue-700 text-sm mt-1">
0fee.dev includes a built-in test provider that supports all 115+
payment methods. No external sandbox credentials needed. Use your
test API key (sk_test_...) and transactions are automatically
routed to the sandbox.
</p>
<div className="mt-2 text-sm text-blue-600">
<strong>Magic amounts for testing:</strong>
<ul className="list-disc list-inside mt-1">
<li>Any amount → Successful payment</li>
<li>Amount ending in .99 → Failed payment</li>
<li>Amount ending in .50 → Pending payment</li>
</ul>
</div>
</div>
)}This eliminates the most common support question we anticipated: "Where do I get test credentials?" The answer is: you do not need any. The platform handles it.
The Reset Button
Testing often means starting over. A developer might want to test the complete onboarding flow again, or clear out test data before inviting a colleague to try the platform. The reset button provides this with a single click.
Frontend
tsxasync function handleReset() {
if (!confirm("This will delete all sandbox transactions and reset your onboarding progress. Continue?")) {
return;
}
const response = await api.post("/onboarding/reset", {
full_reset: true
});
if (response.ok) {
fetchStatus(); // Refresh the stepper
}
}Backend
python@router.post("/reset")
async def reset_onboarding(
request: ResetRequest,
app: App = Depends(get_current_app),
db: AsyncSession = Depends(get_db)
):
"""Reset onboarding progress. With full_reset=true, deletes sandbox transactions."""
if request.full_reset:
# Delete all sandbox transactions for this app
await db.execute(
delete(Transaction).where(
Transaction.app_id == app.id,
Transaction.mode == "sandbox"
)
)
# Reset API key viewed flag
await reset_api_key_viewed(db, app.id)
await db.commit()
return {"status": "reset", "full_reset": request.full_reset}The full_reset=true flag is important. A basic reset only clears the "viewed" flags, but a full reset deletes sandbox transactions. This distinction matters because sandbox transactions may contain test data that the developer wants to preserve for debugging, even if they want to restart the onboarding flow.
Sidebar Integration
The Get Started page is the first item in the dashboard sidebar, positioned above all other navigation items. It uses a rocket icon to convey forward momentum.
tsx// Sidebar.tsx - Get Started menu item
{
label: "Get Started",
path: "/get-started",
icon: "rocket",
section: "developer"
}The icon is a custom SVG added to the getIcon utility function, matching the existing icon style in the dashboard. Once a developer completes all required onboarding steps, the sidebar item displays a green checkmark badge.
What We Learned
1. Remove Provider-Specific Steps Early
The Stripe-specific step survived one session before we realized it was wrong. Every provider-specific onboarding step you add is a developer who bounces because they do not use that provider. The built-in test provider made all provider-specific steps unnecessary.
2. Auto-Verification Beats Checkboxes
Manual "I did this" checkboxes invite two failure modes: developers check them without doing the work (making the onboarding meaningless) or developers do the work and forget to check the box (making the onboarding frustrating). Auto-verification eliminates both.
3. Poll Responsibly
Five seconds is the sweet spot for polling interval. One second feels aggressive and generates unnecessary load. Ten seconds feels sluggish when a developer is actively working through steps. Five seconds provides near-real-time feedback without hammering the backend.
4. State Should Reflect Reality
By checking actual platform state (apps, transactions, providers) rather than maintaining a separate progress record, we avoided an entire class of bugs. The onboarding stepper is a view over existing data, not its own data model.
5. International by Default
Starting with XOF/Orange Money and later switching to USD/Stripe was a lesson in audience awareness. For a platform that covers 200+ countries, the first code example a developer sees should be the most universally understood: USD, generic card payment, standard HTTP request. Region-specific examples belong in the documentation, not in the onboarding flow.
The Result
The onboarding stepper reduced the time from signup to first test payment to under five minutes. The auto-verification provides immediate feedback that keeps developers engaged. The sandbox info box eliminates the credential configuration step that other platforms require. And the reset button lets developers experiment freely without accumulating test debris.
It is not the most technically complex feature in 0fee.dev, but it may be the most commercially important. A developer who processes a test payment in five minutes is a developer who stays. A developer who stares at a blank dashboard for five minutes is a developer who leaves.
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.