Before Session 010, creating a payment on 0fee.dev required multiple fields: payment_method (the operator), country (the country code), currency (the currency), and sometimes payment_method_detail (the provider-specific variant). After Session 010, it required one field: payment_method: "PAYIN_ORANGE_CI". One string encodes the operation type, the operator, and the country. The currency is derived automatically. The provider is selected by the routing engine. This single design change transformed the 0fee.dev API from workable to elegant.
The Problem: Too Many Fields
The original payment initiation request looked like this:
json{
"amount": 5000,
"currency": "XOF",
"country": "CI",
"payment_method": "mobile_money",
"payment_method_detail": "orange_money",
"customer": {
"phone": "+2250709757296",
"email": "[email protected]"
}
}Five parameters to describe what is conceptually a single choice: "Orange Money in Ivory Coast." The problems were:
- Redundancy. If you are paying with Orange Money in Ivory Coast, the currency is always XOF. Specifying it separately invites mismatches.
- Ambiguity. "orange_money" in
payment_method_detailcould mean Orange Money in Ivory Coast, Senegal, Mali, or Cameroon. Thecountryfield disambiguates, but the combination is fragile. - Inconsistency. Card payments do not have a country or a
payment_method_detail. PayPal has neither. The field requirements changed depending on the payment type. - Developer friction. Every SDK, every documentation page, and every integration guide had to explain all five fields and their interdependencies.
The Solution: One Field to Rule Them All
The unified payment format encodes everything into a single string:
PAYIN_ORANGE_CI
| | |
| | └── Country code (CI = Ivory Coast)
| └──────── Operator (ORANGE = Orange Money)
└────────────── Operation type (PAYIN = payment in)The new request:
json{
"amount": 5000,
"payment_method": "PAYIN_ORANGE_CI",
"customer": {
"phone": "+2250709757296"
}
}Three fields instead of six. The currency is derived from the country code. The country is extracted from the suffix. The email is auto-generated from a phone number hash (more on that later). The developer specifies what they want -- an Orange Money payment in Ivory Coast -- and the system handles the rest.
The Format Specification
PAYIN_{OPERATOR}_{COUNTRY_CODE}| Component | Description | Examples |
|---|---|---|
PAYIN | Fixed prefix indicating a pay-in operation | Always PAYIN |
OPERATOR | The payment operator or method | ORANGE, MTN, WAVE, MOOV, MPESA, CARD, PAYPAL |
COUNTRY_CODE | ISO 3166-1 alpha-2 code, or GLOBAL | CI, SN, GH, KE, GLOBAL |
Naming Conventions
- Operators use their common brand name, uppercased:
ORANGE,MTN,WAVE,MOOV. - Compound names use underscores:
AIRTELTIGO_GH, notAIRTEL_TIGO_GH. - Global methods (not country-specific) use
GLOBAL:PAYIN_CARD_GLOBAL,PAYIN_PAYPAL_GLOBAL. - The country code is always the last segment, making extraction trivial.
Country and Currency Auto-Detection
The country code embedded in the payment method drives automatic currency resolution:
python# services/routing.py
COUNTRY_CURRENCY_MAP = {
# UEMOA Zone (West African CFA Franc)
"CI": "XOF", # Ivory Coast
"SN": "XOF", # Senegal
"ML": "XOF", # Mali
"BF": "XOF", # Burkina Faso
"BJ": "XOF", # Benin
"TG": "XOF", # Togo
"NE": "XOF", # Niger
"GW": "XOF", # Guinea-Bissau
# CEMAC Zone (Central African CFA Franc)
"CM": "XAF", # Cameroon
"GA": "XAF", # Gabon
"CG": "XAF", # Congo
"TD": "XAF", # Chad
"CF": "XAF", # Central African Republic
"GQ": "XAF", # Equatorial Guinea
# East Africa
"KE": "KES", # Kenya
"TZ": "TZS", # Tanzania
"UG": "UGX", # Uganda
"RW": "RWF", # Rwanda
# Southern Africa
"ZA": "ZAR", # South Africa
"ZM": "ZMW", # Zambia
"MW": "MWK", # Malawi
"MZ": "MZN", # Mozambique
# West Africa (non-CFA)
"GH": "GHS", # Ghana
"NG": "NGN", # Nigeria
"SL": "SLE", # Sierra Leone
"GM": "GMD", # Gambia
"GN": "GNF", # Guinea
# Other
"MG": "MGA", # Madagascar
"CD": "CDF", # Democratic Republic of Congo
"EG": "EGP", # Egypt
# Global (multi-currency)
"GLOBAL": None,
}
def resolve_payment_method(payment_method: str) -> dict: """ Resolve a unified payment method code into its components. BLANK Args: payment_method: e.g., "PAYIN_ORANGE_CI" BLANK Returns: Dict with country, currency, operator, and full code. """ parts = payment_method.split("_") BLANK if len(parts) < 3 or parts[0] != "PAYIN": raise ValueError( f"Invalid payment method format: {payment_method}. " f"Expected PAYIN_OPERATOR_COUNTRY." ) BLANK country_code = parts[-1] operator = "_".join(parts[1:-1]) # Handle compound names currency = COUNTRY_CURRENCY_MAP.get(country_code) BLANK return { "code": payment_method, "operator": operator, "country": country_code, "currency": currency, } ```
Usage in the Payment Flow
python# In routes/payments.py
@router.post("/v1/payments")
async def initiate_payment(request: Request, data: PaymentInitiate, auth: dict = Depends(get_auth_context)):
# Resolve the unified payment method
method_info = resolve_payment_method(data.payment_method)
# method_info = {
# "code": "PAYIN_ORANGE_CI",
# "operator": "ORANGE",
# "country": "CI",
# "currency": "XOF",
# }
# Currency is auto-detected (merchant can override for GLOBAL methods)
currency = data.currency or method_info["currency"]
if not currency:
raise HTTPException(
400, "Currency required for global payment methods"
)
country = method_info["country"]
# Auto-generate customer email from phone hash
if not data.customer.get("email") and data.customer.get("phone"):
phone_hash = hashlib.md5(
data.customer["phone"].encode()
).hexdigest()[:8]
data.customer["email"] = f"{phone_hash}@mail.0fee.dev"
# Route to provider
route = await get_provider_for_unified_method(
data.payment_method, auth["app_id"], auth["environment"]
)
# route = {
# "provider_id": "paiementpro",
# "provider_method_code": "OMCIV2",
# "priority": 1,
# }
# ... initiate payment with provider ...Provider Method Code Mapping
Each provider uses its own internal codes for payment methods. The routing table maps the unified format to each provider's codes:
Orange Money in Ivory Coast
| Unified Code | Provider | Provider Method Code |
|---|---|---|
PAYIN_ORANGE_CI | PaiementPro | OMCIV2 |
PAYIN_ORANGE_CI | PawaPay | ORANGE_CIV |
PAYIN_ORANGE_CI | Hub2 | Orange (with country=CI) |
MTN in Ghana
| Unified Code | Provider | Provider Method Code |
|---|---|---|
PAYIN_MTN_GH | PawaPay | MTN_GHA |
Wave in Senegal
| Unified Code | Provider | Provider Method Code |
|---|---|---|
PAYIN_WAVE_SN | PawaPay | WAVE_SEN |
PAYIN_WAVE_SN | Hub2 | Wave (with country=SN) |
M-Pesa in Kenya
| Unified Code | Provider | Provider Method Code |
|---|---|---|
PAYIN_MPESA_KE | PawaPay | MPESA_KEN |
Card (Global)
| Unified Code | Provider | Provider Method Code |
|---|---|---|
PAYIN_CARD_GLOBAL | Stripe | card |
The Full Routing Table
Here is the complete mapping of unified codes to providers, sorted by region:
Francophone West Africa (UEMOA -- XOF)
| Payment Method | Providers (by priority) |
|---|---|
PAYIN_ORANGE_CI | PaiementPro (1), PawaPay (2), Hub2 (3) |
PAYIN_MTN_CI | PaiementPro (1), PawaPay (2), Hub2 (3) |
PAYIN_WAVE_CI | PaiementPro (1), PawaPay (2) |
PAYIN_MOOV_CI | PaiementPro (1), Hub2 (2) |
PAYIN_ORANGE_SN | PawaPay (1), Hub2 (2) |
PAYIN_WAVE_SN | PawaPay (1), Hub2 (2) |
PAYIN_FREE_SN | PaiementPro (1) |
PAYIN_ORANGE_ML | PawaPay (1), Hub2 (2) |
PAYIN_MTN_BJ | PawaPay (1), Hub2 (2) |
PAYIN_MOOV_BJ | PawaPay (1), Hub2 (2) |
PAYIN_ORANGE_BF | PawaPay (1), Hub2 (2) |
PAYIN_MOOV_BF | PawaPay (1) |
PAYIN_MOOV_TG | PawaPay (1), Hub2 (2) |
PAYIN_TMONEY_TG | PawaPay (1) |
PAYIN_AIRTEL_NE | PaiementPro (1) |
Central Africa (CEMAC -- XAF)
| Payment Method | Providers (by priority) |
|---|---|
PAYIN_MTN_CM | PawaPay (1), Hub2 (2) |
PAYIN_ORANGE_CM | PawaPay (1), Hub2 (2) |
Anglophone West Africa
| Payment Method | Providers (by priority) |
|---|---|
PAYIN_MTN_GH | PawaPay (1) |
PAYIN_VODAFONE_GH | PawaPay (1) |
PAYIN_AIRTELTIGO_GH | PawaPay (1) |
East Africa
| Payment Method | Providers (by priority) |
|---|---|
PAYIN_MPESA_KE | PawaPay (1) |
PAYIN_AIRTEL_KE | PawaPay (1) |
PAYIN_MTN_UG | PawaPay (1) |
PAYIN_AIRTEL_UG | PawaPay (1) |
PAYIN_MTN_RW | PawaPay (1) |
PAYIN_AIRTEL_TZ | PawaPay (1) |
PAYIN_VODACOM_TZ | PawaPay (1) |
PAYIN_TIGO_TZ | PawaPay (1) |
PAYIN_MPESA_TZ | PawaPay (1) |
PAYIN_AIRTEL_ZM | PawaPay (1) |
PAYIN_MTN_ZM | PawaPay (1) |
Southern Africa
| Payment Method | Providers (by priority) |
|---|---|
PAYIN_AIRTEL_MW | PawaPay (1) |
PAYIN_MPESA_MZ | PawaPay (1) |
Global Methods
| Payment Method | Providers (by priority) |
|---|---|
PAYIN_CARD_GLOBAL | Stripe (1) |
PAYIN_PAYPAL_GLOBAL | PayPal (1) |
The API Response
When a payment is initiated with the unified format, the response includes the resolved components:
bashcurl -X POST https://api.0fee.dev/v1/payments \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"payment_method": "PAYIN_ORANGE_CI",
"customer": {
"phone": "+2250709757296",
"firstName": "Amadou",
"lastName": "Diallo"
}
}'Response:
json{
"success": true,
"data": {
"id": "txn_749b8b1afbd846eaba95",
"status": "pending",
"provider": "paiementpro",
"amount": 5000,
"currency": "XOF",
"country": "CI",
"payment_method": "PAYIN_ORANGE_CI",
"customer": {
"phone": "+2250709757296",
"email": "[email protected]"
},
"created_at": "2025-12-12T10:15:30Z"
}
}The response confirms: amount 5,000 in XOF (auto-detected from CI), routed to PaiementPro (priority 1 for PAYIN_ORANGE_CI), customer email auto-generated from phone hash. The merchant specified three fields; the system resolved the rest.
Payment Method Discovery
Merchants need to know which unified codes are available for their customers. The discovery API returns methods grouped by country:
bash# Get all methods for Ivory Coast
curl https://api.0fee.dev/v1/countries/CI
{
"country": "CI",
"name": "Ivory Coast",
"currency": "XOF",
"payment_methods": [
{
"code": "PAYIN_ORANGE_CI",
"name": "Orange Money",
"type": "mobile_money",
"operator": "Orange"
},
{
"code": "PAYIN_MTN_CI",
"name": "MTN Mobile Money",
"type": "mobile_money",
"operator": "MTN"
},
{
"code": "PAYIN_WAVE_CI",
"name": "Wave",
"type": "mobile_money",
"operator": "Wave"
},
{
"code": "PAYIN_MOOV_CI",
"name": "Moov Money",
"type": "mobile_money",
"operator": "Moov"
},
{
"code": "PAYIN_CARD_GLOBAL",
"name": "Card Payment",
"type": "card",
"operator": null
}
]
}bash# Get details for a specific method
curl https://api.0fee.dev/v1/payin-methods/PAYIN_ORANGE_CI
{
"code": "PAYIN_ORANGE_CI",
"name": "Orange Money Ivory Coast",
"country": "CI",
"currency": "XOF",
"type": "mobile_money",
"operator": "Orange",
"min_amount": 100,
"max_amount": 1000000,
"providers": [
{"provider_id": "paiementpro", "priority": 1},
{"provider_id": "pawapay", "priority": 2},
{"provider_id": "hub2", "priority": 3}
]
}117 Payment Methods
At the time of the unified format implementation in Session 010, the routing table contained 117 payment methods across 30+ countries. This number grew as additional providers and countries were added in subsequent sessions. The format was designed to scale: adding a new payment method requires inserting a row in the payin_methods table and configuring provider routing -- no code changes, no API version bump, no SDK update.
The Design Principle
The unified payment format embodies a design principle that runs through the entire 0fee.dev API: encode decisions in data, not in parameters. When a merchant writes PAYIN_ORANGE_CI, they have made every decision: the operation (pay in), the operator (Orange Money), and the geography (Ivory Coast). The system derives everything else. Fewer parameters means fewer mistakes, simpler documentation, and cleaner SDKs. One string, one truth.
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.