DEV Community

Cover image for How to Collect Payments Using the v4 APIs

How to Collect Payments Using the v4 APIs

This guide covers everything you need to know about collecting payments with:

  • Cards
  • Mobile Money
  • Pay with Bank Transfers (PWBT)
  • Opay
  • USSD

By the end, you’ll understand how the v4 APIs work, the supported payment flows, authorization models, and how to charge using each payment method.

Before we dive in, let’s take a closer look at the v4 APIs, how they differ from v3, and why they’re a step up.

How v4 APIs Differ from v3

With the release of our v4 APIs, there are a few important changes to keep in mind. This applies whether you're integrating for the first time or coming from the v3 APIs. From updated authentication flows to more structured responses and better error handling, v4 introduces some meaningful improvements. Here are a few key upgrades to note:

OAuth2.0 Secure

Managing test and production API keys in your codebase is no longer necessary. Flutterwave now uses Open Authorization (OAuth 2.0), a secure authorization mechanism that connects your application to our servers without exposing static API keys. Unlike API keys, which can be easily exploited if leaked, OAuth 2.0 uses access tokens with expiration times, making it much harder for unauthorized users to maintain long-term access.

Dedicated Sandbox Environment

In v4, you can easily onboard as a developer, test our APIs, and simulate your payment gateway needs. The sandbox includes a redesigned UI, a consistent workflow, and a smoother experience as you build and test. Don’t have an account yet? Create one now.

Improved Workflow

Our v4 APIs follow true RESTful principles. Integration is predictable and scalable and follows industry best practices. For example, when initiating a card payment, you no longer need to encrypt the entire payload. Only the card information needs encryption. You also do not need to call the charge card endpoint multiple times to determine the appropriate authorization method.

Better Developer Experience

A major issue with the v3 API was its generic error messages, which did not clearly indicate what went wrong or how to resolve it. In v4, error responses are well-structured and provide clear insights:

{
    "status": "failed",
    "error": {
        "type": "REQUEST_NOT_VALID",
        "code": "10400",
        "message": "Invalid card number",
        "validation_errors": []
    }
}
Enter fullscreen mode Exit fullscreen mode

v4 also supports more test cases via scenario keys. This mechanism allows you to simulate different payment scenarios with ease.

Scalability

API requests in v4 follow a consistent flow across all payment methods. This reduces technical overhead. You can create reusable helper functions across your codebase, making integrations more efficient and scalable.

Collecting Payments with the v4 API

To collect payments using the v4 APIs, you can choose between the General Flow and the Orchestrator Flow.

The General Flow breaks the payment process into distinct steps, like setting up payment methods, creating customer details, and handling authorization. This gives you more control and lets you customize the payment experience based on your needs.

The Orchestrator Flow simplifies everything by letting you collect payment through a single request. You specify the payment method, the customer, and any other required details in one go.

Use Cases of General Flow and Orchestrator Flow
When building your payment experience, the choice between these two flows depends on how you want to manage customer data and handle repeat transactions.

  • General Flow: Best suited for subscription-based or recurring billing models where you need more control over customer data and payment methods. You can create and manage customers, store payment methods, and use that data to bill users over time. This works well for services that charge periodically.

    For example, think of platforms like Spotify or Notion. Once a customer signs up, you keep their details and bill them every month without asking again.

  • Orchestrator Flow: Great for one-time transactions where you don’t need to store customer or payment details. It’s quick, secure, and designed for fast payments, ideal for situations where speed matters more than long-term retention.

    For example, customers pay and go when buying a concert ticket or ordering food as a guest: no saved info, no accounts, just a smooth one-off transaction.

Use Case Type Recommended Flow Key Benefits
Monthly Subscriptions General Flow Store customer/payment info for recurring billing
One-Time Ticket Sales Orchestrator Flow Fast, anonymous transactions without data persistence
SaaS Tools General Flow Full control of billing cycles and customer management
Flash Sales Orchestrator Flow Speed and simplicity for high-velocity checkouts
Food Delivery (Guest) Orchestrator Flow No account needed; frictionless payments
Utility Services General Flow Long-term customer profiles and scheduled payments

This guide uses the General Flow to collect payments. Check out the Orchestrator Flow to see how it works and how to use it to collect payments.

How to Collect Payment with the General Flow

Collecting payment with the General Flow involves five key steps:

Collecting Payment With General Flow

  • Create a Customer
  • Create a Payment Method
  • Initiate a Charge
  • Authorize a Charge
  • Verify the Payment Status

Before you start, you need to generate a token. This token gives you access to the APIs for a limited period and is required for making authenticated requests.

To do this, log into your dashboard to obtain your Client ID and Client Secret. These are needed to generate the token and refresh it once it expires.

curl -X POST 'https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=<CLIENT_ID>' \
--data-urlencode 'client_secret=<CLIENT_SECRET>' \
--data-urlencode 'grant_type=client_credentials'
Enter fullscreen mode Exit fullscreen mode

Check out the authentication process to learn more.

As a rule of thumb, store sensitive information (like your Client ID) using environment variables or secrets management tools like AWS Secrets Manager.

Once the token is generated, you can use it to create a charge, as shown below:

Step 1: Create a Customer

To create a customer, send a request to the create customer endpoint with the customer’s details. Only the customer’s email is required; however, we recommend you collect as much information as you can.

curl --request POST \
--url 'https://developersandbox-api.flutterwave.com/customers' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'Content-Type: application/json' \
--header 'X-Trace-Id: {{YOUR_UNIQUE_TRACE_ID}}' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--data '{
    "address":{
        "city":"Gotham",
        "country":"US",
        "line1":"221B Baker Street",
        "line2":"",
        "postal_code":"94105",
        "state":"Colorado"
    },
    "name":{
        "first":"King",
        "middle":"Leo",
        "last":"James"
    },
    "phone":{
        "country_code":"1",
        "number":"6313958745"
    },
    "email":"james@example.com"
}'
Enter fullscreen mode Exit fullscreen mode

You'll receive a response containing the customer ID that you can use for future charges.

{
    "status": "success",
    "message": "Customer created",
    "data": {
        "id": "cus_X0yJv3ZMpL",
        "address": {
            "city": "Gotham",
            "country": "US",
            "line1": "221B Baker Street",
            "line2": "",
            "postal_code": "94105",
            "state": "Colorado"
        },
        "email": "james@example.com",
        "name": {
            "first": "King",
            "middle": "Leo",
            "last": "James"
        },
        "phone": {
            "country_code": "1",
            "number": "6313958745"
        },
        "meta": {},
        "created_datetime": "2025-01-29T12:44:53.049Z"
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices to Follow when Creating Customers

  1. Add a unique X-Trace-Id to each request. This helps with observability and makes debugging easier.
  2. Include a unique X-Idempotency-Key for every request. This prevents duplicate customer creation during retries or network issues.
  3. Save the id returned in the response. You’ll need it to reference the customer in future transactions. This helps avoid duplicate profiles and makes it easier to track customer activity.
  4. Validate all input on the client and server sides. For example, check for a valid email format or proper phone number structure.

Step 2: Create a Payment Method

Depending on the payment method your customer wants to use, follow the preferred approach below:

Cards

To collect payment with cards, send a request to the create a payment method endpoint. You must also encrypt card details before sending the requests.

curl --request POST \
--url 'https://developersandbox-api.flutterwave.com/payment-methods' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'Content-Type: application/json' \
--header 'X-Trace-Id: {{YOUR_UNIQUE_TRACE_ID}}' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--data '{
    "type": "card",
    "card": {
        "encrypted_card_number": "{{$encrypted_card_number}}",
        "encrypted_expiry_month": "{{$encrypted_expiry_month}}",
        "encrypted_expiry_year": "{{$encrypted_expiry_year}}",
        "encrypted_cvv": "{{$encrypted_cvv}}",
        "nonce": "{{$randomly_generated_nonce}}"
    }
}'
Enter fullscreen mode Exit fullscreen mode

A key parameter to note in the request body is type. It tells the general flow endpoint the type of payment to process. In this case, the payment method is set to card along with the corresponding card details.

Similar to the customer creation step, you’ll get a payment method ID.

{
    "status": "success",
    "message": "Payment method created",
    "data": {
        "type": "card",
        "card": {
            "expiry_month": 8,
            "expiry_year": 32,
            "first6": "123412",
            "last4": "2222",
            "network": "mastercard"
        },
        "id": "pmd_wlVhaYmkl2",
        "meta": {},
        "created_datetime": "2024-12-03T14:29:26.650973145Z"
    }
}
Enter fullscreen mode Exit fullscreen mode

Mobile Money

To use Mobile Money, send a request to the same create a payment method endpoint. Set the type as mobile_money and include the mobile number country code, network, and mobile number.

curl --request POST \
--url 'https://developersandbox-api.flutterwave.com/payment-methods' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'Content-Type: application/json' \
--header 'X-Trace-Id: {{YOUR_UNIQUE_TRACE_ID}}' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--data '{
    "type": "mobile_money",
    "mobile_money": {
        "country_code": "233",
        "network": "MTN",
        "phone_number": "9012345678"
    }
}'
Enter fullscreen mode Exit fullscreen mode

You’ll also get a response with a payment method ID.

{
    "status": "success",
    "message": "Payment method created",
    "data": {
        "type": "mobile_money",
        "mobile_money": {
            "network": "MTN",
            "country_code": "233",
            "phone_number": "9012345678"
        },
        "id": "pmd_kwU1jeHpBC",
        "meta": {},
        "created_datetime": "2025-01-30T12:25:52.596970660Z"
    }
}
Enter fullscreen mode Exit fullscreen mode

Pay with Bank Transfers (PWBT)

With PWBT, your customers pay into a generated bank account that’s linked to your Flutterwave settlement account. You can choose to generate either a static account or a dynamic one, depending on your use case.

  • Use a static account if you plan to reuse the same account for multiple transactions.
  • Use a dynamic account for one-off payments.

To create a payment method with PWBT, send a request to the create a virtual account endpoint. You’ll need to include a unique reference for the customer, the customer ID generated earlier, whether the account type is static or dynamic, and other important information, as shown below:

curl --request POST \
--url 'https://developersandbox-api.flutterwave.com/virtual-accounts' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--header 'Content-Type: application/json' \
--data '{
    "reference": "a4d5f6b8-a785-4d41-8932-50fd8288aec8",
    "customer_id": "cus_WWVaC0InrN",
    "amount": 1500,
    "currency": "NGN",
    "account_type": "dynamic",
    "narration": "John Doe"
}'
Enter fullscreen mode Exit fullscreen mode

If you don’t set an expiry time for a dynamic bank account, it will expire after 3600 seconds (60 minutes) by default.

You’ll get a response with a payment method ID.

{
    "status": "success",
    "message": "Virtual account created",
    "data": {
        "id": "van_fRiLt0WNsj",
        "amount": 1500,
        "account_number": "4032866864",
        "reference": "9d961ebf-6e51-4970-a334-af6a39325930",
        "account_bank_name": "WEMA BANK",
        "account_type": "dynamic",
        "status": "active",
        "account_expiration_datetime": "2025-06-02T08:03:21.369640550Z",
        "note": "Please make a bank transfer to John Doe",
        "customer_id": "cus_WWVaC0InrN",
        "created_datetime": "2025-06-02T08:02:21.383710209Z",
        "meta": {}
    }
}
Enter fullscreen mode Exit fullscreen mode

OPay

To use OPay, send a request to the same payment method endpoint and set the type to opay.

curl --request POST \
--url 'https://developersandbox-api.flutterwave.com/payment-methods' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'Content-Type: application/json' \
--header 'X-Trace-Id: {{YOUR_UNIQUE_TRACE_ID}}' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--data '{
    "type":"opay"
}'
Enter fullscreen mode Exit fullscreen mode

You’ll get a response with a payment method ID.

{
    "status": "success",
    "message": "Payment method created",
    "data": {
        "type": "opay",
        "opay": {},
        "id": "pmd_uF9ADr9LvH",
        "meta": {},
        "created_datetime": "2024-12-26T14:38:25.179075400Z"
    }
}
Enter fullscreen mode Exit fullscreen mode

USSD

To collect payments with USSD, start by sending a request to the get bank endpoint. Add the country as a query parameter to fetch a list of banks along with their unique bank codes.

curl --request GET \
--url 'https://developersandbox-api.flutterwave.com/banks?country=NG' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'Content-Type: application/json' \
--header 'X-Trace-Id: {{YOUR_UNIQUE_TRACE_ID}}' \
Enter fullscreen mode Exit fullscreen mode

Bank codes are unique identifiers used to determine which bank will handle the USSD payment.

After the customer selects a bank, create a payment method using that bank’s code and set the type to ussd.

curl --location 'https://developersandbox-api.flutterwave.com/payment-methods' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'Content-Type: application/json' \
--header 'X-Trace-Id: {{YOUR_UNIQUE_TRACE_ID}}' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--data '{
    "type": "ussd",
    "ussd": {
    "account_bank": "050"
    }
}'
Enter fullscreen mode Exit fullscreen mode

Best Practices to Follow when Creating Payment Methods

  1. Use the encryption utility to encrypt fields like card_number, expiry_month, expiry_year, and cvv.
  2. Always match the type field with the correct nested object (card, mobile_money, etc.) in your request body.
  3. Always call the /banks?country={<REPLACE WITH COUNTRY CODE>} endpoint to get the latest list of supported banks and their codes for USSD. Present this list to users to reduce friction and increase conversion.
  4. Every successful request returns a unique id (e.g., pmd_wlVhaYmkl2) for that payment method. You’ll need this ID for initiating charges later, so store it securely in your system.
  5. If your use case involves recurring or customer-linked payments via bank transfers, use static account instead of a dynamic one.

Step 3: Putting it Together — Initiate a Charge

To initiate the charge, use the customer and payment method IDs you got from the previous steps to send a request to the charge endpoint. You’ll need to pass in the amount, currency, and a unique reference.

curl --request POST \
--url 'https://developersandbox-api.flutterwave.com/charges' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'Content-Type: application/json' \
--header 'X-Trace-Id: {{YOUR_UNIQUE_TRACE_ID}}' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--data '{
    "reference": "cedfa85a-a803-4a06-a586-0f81fb9b9f22",
    "currency": "USD",
    "customer_id": "cus_IpH7CKCUtD",
    "payment_method_id": "pmd_wlVhaYmkl2",
    "redirect_url":"https://google.com",
    "amount": 250,
    "meta":{
        "person_name": "King James",
        "role": "Developer"
    }
    }
'
Enter fullscreen mode Exit fullscreen mode

Check out the API reference to see other supported parameters.

Note that for PWBT, you don’t need to initiate the charge as shown above, but instead display the account details generated for the customer to complete payment.

Step 4: Authorize the Charge

After initiating the charge, the customer needs to authenticate the transaction. Flutterwave returns a next_action object to inform you of the type of authentication required to complete the payment.

Cards

In cards, a sample next_action is the address verification (AVS) that requires the customer to provide their address information to authorize the charge.

"next_action": {
    "type": "requires_additional_fields",
    "requires_additional_fields":{
            "fields":[
                "authorization.avs.address.postal_code",
                "authorization.avs.address.line1",
                "authorization.avs.address.state",
                "authorization.avs.address.country",
                "authorization.avs.address.city"
            ]
            }
},
Enter fullscreen mode Exit fullscreen mode

To authorize a charge using AVS, update the charge by sending a request to the update charge endpoint with the required address information and charge ID.

curl --request PUT 'https://developersandbox-api.flutterwave.com/charges/{id}' \
--header 'Authorization: Bearer {{YOUR_ACCESS_TOKEN}}' \
--header 'Content-Type: application/json' \
--header 'X-Trace-Id: {{YOUR_UNIQUE_TRACE_ID}}' \
--header 'X-Idempotency-Key: {{YOUR_UNIQUE_INDEMPOTENCY_KEY}}' \
--data '{
    "authorization": {
        "type": "avs",
        "avs": {
        "city":"Gotham",
        "country":"US",
        "line1":"221B Baker Street",
        "line2":"",
        "postal_code":"94105",
        "state":"Colorado"
    }
    }
}'
Enter fullscreen mode Exit fullscreen mode

Possible next_actions you should cater to in v4 include:

Authorization Method Response Type
3DS/VBVSECURECODE redirect_url
External 3DS redirect_url
AVS requires_additional_fields
PIN requires_pin
USSD payment_instructions
Bank Transfer requires_bank_transfer

Mobile Money

In Mobile Money, the next_action object contains a type of payment_instruction. This means the customer must complete the charge externally on their mobile phone. They’ll receive a push notification from their mobile money platform and authorize the payment by entering their PIN. Once the payment is completed and funds are received, Flutterwave sends you a webhook with the final transaction status. See the Verify the Payment Status section for more details.

PWBT

In PWBT, the next_action object contains a type of requires_bank_transfer. This means the customer must complete the charge by transferring funds to the generated bank account details. Once the payment is completed and funds are received, Flutterwave sends you a webhook with the final transaction status.

OPay

In OPay, the next_action object contains a type of redirect_url. Redirect the customer to the OPay interface to authorize and complete the payment. Once the payment is completed and funds are received, Flutterwave sends you a webhook with the final transaction status.

"next_action":{
        "type":"redirect_url",
        "redirect_url":{
        "url":"https://developer-sandbox-ui-sit.flutterwave.cloud/redirects?opay&token=eyJhbGciOiJIUzI1NiJ9.eyJjbGllbnRJZCI6ImNiYThhMTkwLTE2OGUtNGNmZS05NmY5LTE2NDZhYjFhOWNkYiIsImNoYXJnZUlkIjoiY2hnX25PTmdlQUdZOTciLCJzdWIiOiJjYmE4YTE5MC0xNjhlLTRjZmUtOTZmOS0xNjQ2YWIxYTljZGIiLCJpYXQiOjE3MzgzMTc3MTAsImV4cCI6MTczODMxODAxMH0.QgLoZYfNhHJOJJvOsLA9eLoxOjGF0qnuehPMgMP4zD4"
        }
},
Enter fullscreen mode Exit fullscreen mode

USSD

In USSD, the next_action object contains a type of payment_instruction. This requires the customer to complete the charge externally on their mobile phone. They'll receive a push notification from their mobile money provider and authorize the payment by entering their PIN after dialing the USSD code. Once the payment is completed and funds are received, Flutterwave sends you a webhook with the final transaction status.

"next_action": {
        "type": "payment_instruction",
        "payment_instruction": {
            "note": "Please dial *1414# to complete this transaction"
        }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices to Follow when Authorizing a Charge

  1. Build a flexible handler or router in your code to dynamically process next_action.type and validate values before use, especially if dynamically rendering fields or redirecting.
  2. For sensitive information like addresses, PINs, or card details, never store or log full payment details, and sanitize user input before including it in payloads.
  3. For better UX, pre-fill known user address data if available, especially when dealing with AVS requests.
  4. For payment types with external steps like USSD, display instructions clearly and show a countdown timer or prompt to indicate that the payment is still pending.

Step 5: Verify the Payment Status

The last step is to verify transaction details like status and amount before providing value to the customer. You can verify the transaction using either of the following methods:

  1. Webhook (Recommended): Flutterwave sends a webhook with the transaction status if you have webhooks enabled on your dashboard.
{
    "webhook_id": "wbk_yXvsB4LzWSwhUCpAPCBR",
    "timestamp": 1739456704200,
    "type": "charge.completed",
    "data": {
        "id": "chg_zam88NgLb7",
        "amount": 2000,
        "currency": "NGN",
        "customer": {
            "id": "cus_dc0FUyBpd0",
            "address": {
                "city": "Gotham",
                "country": "US",
                "line1": "221B Baker Street",
                "line2": "",
                "postal_code": "94105",
                "state": "Colorado"
            },
            "email": "james@example.com",
            "name": {
                "first": "King",
                "middle": "Leo",
                "last": "James"
            },
            "phone": {
                "country_code": "1",
                "number": "6313958745"
            },
            "meta": {},
            "created_datetime": "2024-12-25T20:16:38.246Z"
        },
        "description": null,
        "meta": {},
        "payment_method": {
            "type": "card",
            "card": {
                "expiry_month": 9,
                "expiry_year": 32,
                "first6": "553188",
                "last4": "2950",
                "network": "MASTERCARD",
                "billing_address": null,
                "cof": null,
                "card_holder_name": null
            },
            "id": "pmd_ujxuBcf4cJ",
            "customer_id": null,
            "meta": {},
            "device_fingerprint": null,
            "client_ip": null,
            "created_datetime": "2025-02-05T14:06:10.344Z"
        },
        "redirect_url": null,
        "reference": "ex61m23ja3feykheoidho8ilrri",
        "status": "succeeded",
        "processor_response": {
            "type": "approved",
            "code": "00"
        },
        "created_datetime": "2025-02-13T14:24:43.133Z"
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Charge Endpoint: You can manually check the charge status by sending a request to retrieve a charge endpoint at a specified interval.
curl --request GET \
--url 'https://api-sit.flutterwave.cloud/developersandbox/charges/{id}' \
--header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--header 'X-Trace-Id: <YOUR_UNIQUE_TRACE_ID>' \
--header 'X-Idempotency-Key: <YOUR_UNIQUE_INDEMPOTENCY_KEY>' \
Enter fullscreen mode Exit fullscreen mode

You’ll get a response similar to this:

{
    "status": "success",
    "message": "Charge updated",
    "data": {
        "id": "chg_zam88NgLb7",
        "amount": 2000,
        "currency": "NGN",
        "customer": "cus_EFE4TQhBSf",
        "meta": {},
        "payment_method_details": {
            "type": "card",
            "card": {
                "expiry_month": 8,
                "expiry_year": 2024,
                "first6": "123412",
                "last4": "4444",
                "network": "mastercard"
            },
            "id": "pmd_NkWibrRJIy",
            "customer": "cus_dc0FUyBpd0",
            "meta": {}
        },
        "redirect_url": null,
        "reference": "ex61m23ja3feykheoidho8ilrri",
        "status": "succeeded",
        "processor_response": {
            "type": "approved",
            "code": "00"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

The v4 API supports multiple payment methods, like cards, mobile money, and PWBT (Pay With Bank Transfer), making it easier to accept payments from your customers. It also works with the General and Orchestrator flows, so you can handle recurring or one-time payments based on what your use case needs.

To get started:

Top comments (0)