Back to 0fee
0fee

The Unified Payment Format: PAYIN_ORANGE_CI

How 0fee.dev unified 117 payment methods into the PAYIN_OPERATOR_COUNTRY format, simplifying API calls to a single field. By Juste A. Gnimavo and Claude.

Thales & Claude | March 25, 2026 9 min 0fee
payment-methodsapi-designroutingunified-formatdeveloper-experience

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:

  1. Redundancy. If you are paying with Orange Money in Ivory Coast, the currency is always XOF. Specifying it separately invites mismatches.
  2. Ambiguity. "orange_money" in payment_method_detail could mean Orange Money in Ivory Coast, Senegal, Mali, or Cameroon. The country field disambiguates, but the combination is fragile.
  3. 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.
  4. 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}
ComponentDescriptionExamples
PAYINFixed prefix indicating a pay-in operationAlways PAYIN
OPERATORThe payment operator or methodORANGE, MTN, WAVE, MOOV, MPESA, CARD, PAYPAL
COUNTRY_CODEISO 3166-1 alpha-2 code, or GLOBALCI, SN, GH, KE, GLOBAL

Naming Conventions

  • Operators use their common brand name, uppercased: ORANGE, MTN, WAVE, MOOV.
  • Compound names use underscores: AIRTELTIGO_GH, not AIRTEL_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 CodeProviderProvider Method Code
PAYIN_ORANGE_CIPaiementProOMCIV2
PAYIN_ORANGE_CIPawaPayORANGE_CIV
PAYIN_ORANGE_CIHub2Orange (with country=CI)

MTN in Ghana

Unified CodeProviderProvider Method Code
PAYIN_MTN_GHPawaPayMTN_GHA

Wave in Senegal

Unified CodeProviderProvider Method Code
PAYIN_WAVE_SNPawaPayWAVE_SEN
PAYIN_WAVE_SNHub2Wave (with country=SN)

M-Pesa in Kenya

Unified CodeProviderProvider Method Code
PAYIN_MPESA_KEPawaPayMPESA_KEN

Card (Global)

Unified CodeProviderProvider Method Code
PAYIN_CARD_GLOBALStripecard

The Full Routing Table

Here is the complete mapping of unified codes to providers, sorted by region:

Francophone West Africa (UEMOA -- XOF)

Payment MethodProviders (by priority)
PAYIN_ORANGE_CIPaiementPro (1), PawaPay (2), Hub2 (3)
PAYIN_MTN_CIPaiementPro (1), PawaPay (2), Hub2 (3)
PAYIN_WAVE_CIPaiementPro (1), PawaPay (2)
PAYIN_MOOV_CIPaiementPro (1), Hub2 (2)
PAYIN_ORANGE_SNPawaPay (1), Hub2 (2)
PAYIN_WAVE_SNPawaPay (1), Hub2 (2)
PAYIN_FREE_SNPaiementPro (1)
PAYIN_ORANGE_MLPawaPay (1), Hub2 (2)
PAYIN_MTN_BJPawaPay (1), Hub2 (2)
PAYIN_MOOV_BJPawaPay (1), Hub2 (2)
PAYIN_ORANGE_BFPawaPay (1), Hub2 (2)
PAYIN_MOOV_BFPawaPay (1)
PAYIN_MOOV_TGPawaPay (1), Hub2 (2)
PAYIN_TMONEY_TGPawaPay (1)
PAYIN_AIRTEL_NEPaiementPro (1)

Central Africa (CEMAC -- XAF)

Payment MethodProviders (by priority)
PAYIN_MTN_CMPawaPay (1), Hub2 (2)
PAYIN_ORANGE_CMPawaPay (1), Hub2 (2)

Anglophone West Africa

Payment MethodProviders (by priority)
PAYIN_MTN_GHPawaPay (1)
PAYIN_VODAFONE_GHPawaPay (1)
PAYIN_AIRTELTIGO_GHPawaPay (1)

East Africa

Payment MethodProviders (by priority)
PAYIN_MPESA_KEPawaPay (1)
PAYIN_AIRTEL_KEPawaPay (1)
PAYIN_MTN_UGPawaPay (1)
PAYIN_AIRTEL_UGPawaPay (1)
PAYIN_MTN_RWPawaPay (1)
PAYIN_AIRTEL_TZPawaPay (1)
PAYIN_VODACOM_TZPawaPay (1)
PAYIN_TIGO_TZPawaPay (1)
PAYIN_MPESA_TZPawaPay (1)
PAYIN_AIRTEL_ZMPawaPay (1)
PAYIN_MTN_ZMPawaPay (1)

Southern Africa

Payment MethodProviders (by priority)
PAYIN_AIRTEL_MWPawaPay (1)
PAYIN_MPESA_MZPawaPay (1)

Global Methods

Payment MethodProviders (by priority)
PAYIN_CARD_GLOBALStripe (1)
PAYIN_PAYPAL_GLOBALPayPal (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.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles