Direct card charge

Hey👋. We recommend checking out the overview to understand the basics of direct charge first. This guide assumes you've read that.

Direct card charge allows you to charge both local cards (issued in your country of operation) and international cards. This is useful if your customers are predominantly credit/debit card users, and you'd prefer for them to manage payments via your app.

Your country of operation is the country you selected when you created your Flutterwave account. If you accept payments from cards issued in other countries, or you charge in currencies apart from your local currency, the payment will be considered "international".

Compliance required

Using direct card charge involves handling some very sensitive customer data, so a PCI DSS compliance certificate is required. When you've got one, contact your Relationship Manager or reach out to hi@flutterwavego.com to enable this feature on your account.

Process

Card charge involves four main steps:

  1. Initiate the payment: You send the transaction details and the customer's payment details to the charge card endpoint.
  2. Authorize the charge: We tell you the details needed to authorize the charge, you get those from the customer, and you send to the same charge card endpoint.
  3. Validate the charge: Think of this as a second layer of authentication. The customer provides some info to validate the charge such as an OTP), and you send that to us (the validate charge endpoint). This completes the payment.
  4. Verify the payment: As a failsafe, you'll call our API to verify that the payment was successful before giving value (the verify transaction endpoint).

In some scenarios, steps 2 and/or 3 might be skipped. For example, cards which use 3DS authentication won't have a validate stage. We'll explain the different scenarios in this guide.

Recurring payments

You can also collect recurring payments from customers with direct card charge. See our guide to card tokenization.

Card authorization models

When you make a card charge request, Flutterwave will evaluate the card to determine its authorization model—a one-time pin (OTP), a card internet PIN (i-Pin), or an address verification system (AVS). Then we'll send you a prompt telling you what parameters are needed for that authorization model, so the cardholder can authenticate their transaction.

Step 1: Collect the customer's payment details

First, you'll need the customer's card details. You can collect these with a simple HTML form like this:

See the Pen Credit Card Payment Form by Shalvah (@shalvah) on CodePen.

After collecting the card details, you'll need to add some other details to make up the full payload, including:

  • tx_ref: A unique reference code that you'll generate for each transaction.
  • amount and currency: The amount to be charged, and the currency to charge in.
  • email: The customer's email address.
  • redirect_url (optional): The url we should redirect to after the customer completes payment. For 3DSecure payments, we'll append the tx_ref and status to the URL as query parameters.
  • meta (optional): Any extra details you wish to attach to the transaction. For instance, you could include the flight_id or product_url.

See the charge card endpoint for a full list of available options.

{
  "card_number": "4556052704172643",
  "cvv": "899",
  "expiry_month": "01",
  "expiry_year": "23",
  "currency": "NGN",
  "amount": "7500",
  "email": "developers@flutterwavego.com",
  "fullname": "Flutterwave Developers",
  "tx_ref": "MC-3243e",
  "redirect_url":"https://your-awesome.app/payment-redirect"
}

Preauthorization

Preauthorizing a card is a way to place a "hold" on the amount you plan to charge a customer. This is helpful if you intend to bill the customer after they use your service, but you want to ensure they have enough funds to pay before providing them the service.

For instance, if you run a ride-hailing business or hotel, you'd typically charge the customer after their trip or stay. However, you can estimate how much the trip or stay will cost and preauthorize the charge. This will "lock" the requested amount for a maximum of 7 working days. That way, you can be sure the customer will be able to pay their bill after rendering the service.

Approval required

Using preauthorization requires approval. Contact us at compliance@flutterwavego.com so we can enable it on your account.

To preauthorize a card, pass a preauthorize parameter in your card charge payload.

{
+   "preauthorize": true,
    "card_number": "4556052704172643",
    "cvv": "899",
    "expiry_month": "01",
    "expiry_year": "23",
    "currency": "NGN",
    "amount": "7500",
    "email": "developers@flutterwavego.com",
    "fullname": "Flutterwave Developers",
    "tx_ref": "MC-3243e",
    "redirect_url":"https://your-awesome.app/payment-redirect"
}

You can also specify the usesecureauth boolean parameter if you'd like to authorise the preauth charge via 3DS.

This will place a hold on the amount, but won't actually charge the card. When the service has been rendered and you're ready to charge the customer, you can use the capture preauth charge endpoint. You can also pass in the amount you want to charge:

const got = require("got");
await got.post(
    `https://api.flutterwave.com/v3/charges/${orderRef}/capture`,
    {
        json: {amount: 1200},
        headers: {authorization: `Bearer ${process.env.FLW_SECRET_KEY}`}
    }
);

The amount must be less than or equal to the preauthorized amount. This means you can't charge more than you preauthorized, so you should estimate adequately.

You can also release the hold on the funds (if the customer does not eventually use the service) or refund part of the held amount. Any holds that you don't capture, void or refund will automatically be voided after 7 working days.

Step 2: Encrypt the payload

If you're using one of our backend SDKs, you should skip this step, as they'll automatically encrypt the payload for you when sending the request. Head on to Step 3.

Next, you'll encrypt the payload you've built up. You'll need your encryption key (from the Settings > API section of your dashboard), and you'll use the 3DES algorithm for encryption. You can see examples of this in our encryption guide.

Now, you'll wrap the encrypted payload inside a JSON body like this:

{
  "client": "Of8p6iJUVUezgvjUkjjJsP8aPd6CjHR3f9ptHiH5Q0+2h/FzHA/X1zPlDmRmH5v+GoLWWB4TqEojrKhZI38MSjbGm3DC8UPf385zBYEHZdgvQDsacDYZtFEruJqEWXmbvw9sUz+YwUHegTSogQdnXp7OGdUxPngiv6592YoL0YXa4eHcH1fRGjAimdqucGJPurFVu4sE5gJIEmBCXdESVqNPG72PwdRPfAINT9x1bXemI1M3bBdydtWvAx58ZE4fcOtWkD/IDi+o8K7qpmzgUR8YUbgZ71yi0pg5UmrT4YpcY2eq5i46Gg3L+fxFl4tauG9H4WBChF0agXtP4kjfhfYVD48N9Hrt"
}

You can then use this payload to initiate payment.

Step 3: Initiate the payment

To initiate payment, send your encrypted payload to our charge card endpoint.

If you're using one of our SDKs, you'll pass in the payload directly, and we'll encrypt it for you. Otherwise, you can do the encryption manually like above, then call the endpoint:

// Install with: npm i flutterwave-node-v3

const Flutterwave = require('flutterwave-node-v3');
const flw = new Flutterwave(process.env.FLW_PUBLIC_KEY, process.env.FLW_SECRET_KEY);
const payload = {
    card_number: '4556052704172643',
    cvv: '899',
    expiry_month: '01',
    expiry_year: '23',
    currency: 'NGN',
    amount: '7500',
    email: 'developers@flutterwavego.com',
    fullname: 'Flutterwave Developers',
    tx_ref: 'MC-3243e',
    redirect_url: 'https://your-awesome.app/payment-redirect',
    enckey: process.env.FLW_ENCRYPTION_KEY
}
flw.Charge.card(payload)
    .then(response => console.log(response));
// Install with: composer require flutterwavedev/flutterwave-v3

$flw = new \Flutterwave\Rave(getenv('FLW_SECRET_KEY'));
// Set `PUBLIC_KEY` and `ENCRYPTION_KEY` as environment variables
$cardChargeService = new \Flutterwave\Card();
$payload = [
    'card_number' => '4556052704172643',
    'cvv' => '899',
    'expiry_month' => '01',
    'expiry_year' => '23',
    'currency' => 'NGN',
    'amount' => '7500',
    'email' => 'developers@flutterwavego.com',
    'fullname' => 'Flutterwave Developers',
    'tx_ref' => 'MC-3243e',
    'redirect_url' => 'https://your-awesome.app/payment-redirect',
];
$response = $cardChargeService->chargeCard($payload);
print_r($response);
# Install with: gem install flutterwave_sdk

require 'flutterwave_sdk'

flw = Flutterwave.new(ENV["FLW_PUBLIC_KEY"], ENV["FLW_SECRET_KEY"], ENV["FLW_ENCRYPTION_KEY"])
charge_card = Card.new(flw)
payload = {
    card_number: '4556052704172643',
    cvv: '899',
    expiry_month: '01',
    expiry_year: '23',
    currency: 'NGN',
    amount: '7500',
    email: 'developers@flutterwavego.com',
    fullname: 'Flutterwave Developers',
    tx_ref: 'MC-3243e',
    redirect_url: 'https://your-awesome.app/payment-redirect',
}
response = charge_card.initiate_charge payload
print response
# Install with: pip install rave_python

import os
from rave_python import Rave

rave = Rave(os.getenv("FLW_PUBLIC_KEY"), os.getenv("FLW_SECRET_KEY"))
details = {
    "card_number": '4556052704172643',
    "cvv": '899',
    "expiry_month": '01',
    "expiry_year": '23',
    "currency": 'NGN',
    "amount": '7500',
    "email": 'developers@flutterwavego.com',
    "fullname": 'Flutterwave Developers',
    "tx_ref": 'MC-3243e',
    "redirect_url": 'https://your-awesome.app/payment-redirect',
}
response = rave.Card.charge(payload)
print(response)
// Install with: go get github.com/Flutterwave/Rave-go/rave

import (
  "fmt"
  "os"
  "github.com/Flutterwave/Rave-go/rave"
)
var r = rave.Rave{
  false,
  os.Getenv("FLW_PUBLIC_KEY"),
  os.Getenv("FLW_SECRET_KEY"),
}
var card = rave.Card{
    r,
}
payload := rave.CardChargeData{
    Amount: 7500,
    Txref: "MC-3243e",
    Email: "developers@flutterwavego.com",
    Currency: "NGN",
    Cardno: "4556052704172643",
    Cvv:"899",
    FirstName: "Homer",
    LastName: "Simpson",
    Expirymonth: "01",
    Expiryyear: "23",
}
err, response := card.ChargeCard(payload)
if err != nil {
    panic(err)
}
fmt.Println(response)
curl --request POST \
   --url https://api.flutterwave.com/v3/charges?type=card \
   --header 'Authorization: Bearer YOUR_SECRET_KEY' \
   --header 'content-type: application/json' \
   --data '{ "client": "Of8p6iJUVUezgvjUkjjJsP8aPd6CjHR3f9ptHiH5Q0+2h/FzHA/X1zPlDmRmH5v+GoLWWB4TqEojrKhZI38MSjbGm3DC8UPf385zBYEHZdgvQDsacDYZtFEruJqEWXmbvw9sUz+YwUHegTSogQdnXp7OGdUxPngiv6592YoL0YXa4eHcH1fRGjAimdqucGJPurFVu4sE5gJIEmBCXdESVqNPG72PwdRPfAINT9x1bXemI1M3bBdydtWvAx58ZE4fcOtWkD/IDi+o8K7qpmzgUR8YUbgZ71yi0pg5UmrT4YpcY2eq5i46Gg3L+fxFl4tauG9H4WBChF0agXtP4kjfhfYVD48N9Hrt"}'

Step 4: Authorize the payment

This next step will vary, depending on the type of card you're charging. After initiating a charge, the customer may need to authorize the charge on their card. Card authorization methods are typically one of the following:

  • PIN: The customer enters the card PIN to authorize the payment.
  • AVS (Address Verification System): The customer enters details of the card's billing address. Often used for international cards.
  • 3DS (3D Secure)
  • None. In some cases, the card may not need any authorization steps at all.

The response you get from the previous step (initiating the payment) will tell you the authorization required for the card (if any) in the meta.authorization field. The meta.authorization.mode key indicates which type of authorization is needed, while the meta.authorization.fields key indicates the fields you need to collect from the customer. For instance:

  • If the mode is "pin", then the card requires PIN authentication. You'll need to collect the customer's PIN.
  • If the mode is "avs_noauth", the card requires AVS authentication. You'll need to collect the specified fields (for example, city, address, state, country, and zipcode).
  • If the mode is "redirect", then you won't be collecting any extra fields. Instead, you'll redirect to the specified URL(in meta.authorization.redirect) for the customer to authenticate with their bank.
  • In some cases, the card doesn't need any additional authentication, so you can skip to verifying the payment. In that case, the data.status field will be either "successful" or "failed".

Here are some sample responses for the different scenarios:

{
  "status": "success",
  "message": "Charge authorization data required",
  "meta": {
    "authorization": {
      "mode": "pin",
      "fields": ["pin"]
    }
  }
}
{
  "status": "success",
  "message": "Charge authorization data required",
  "meta": {
    "authorization": {
      "mode": "avs_noauth",
      "fields": [
        "city",
        "address",
        "state",
        "country",
        "zipcode"
      ]
    }
  }
}
{
  "status": "success",
  "message": "Charge initiated",
  "data": {
    "id": 1254647,
    "tx_ref": "MC-3243e",
    "flw_ref": "FLW-MOCK-587df5c89bd607a52f3e0ba71e671cd3",
    "device_fingerprint": "N/A",
    "amount": 100000,
    "charged_amount": 100000,
    "app_fee": 3800,
    "merchant_fee": 0,
    "processor_response": "Please enter the OTP sent to your mobile number 080****** and email te**@rave**.com",
    "auth_model": "VBVSECURECODE",
    "currency": "NGN",
    "ip": "N/A",
    "narration": "CARD Transaction ",
    "status": "pending",
    "payment_type": "card",
    "fraud_status": "ok",
    "charge_type": "normal",
    "created_at": "2020-04-30T20:09:56.000Z",
    "account_id": 27468,
    "customer": {
      "id": 370672,
      "phone_number": null,
      "name": "Anonymous customer",
      "email": "user@gmail.com",
      "created_at": "2020-04-30T20:09:56.000Z"
    },
    "card": {
      "first_6digits": "543889",
      "last_4digits": "0229",
      "issuer": "MASTERCARD MASHREQ BANK CREDITSTANDARD",
      "country": "EG",
      "type": "MASTERCARD",
      "expiry": "10/23"
    }
  },
  "meta": {
    "authorization": {
      "mode": "redirect",
      "redirect": "https://ravesandboxapi.flutterwave.com/mockvbvpage?ref=FLW-MOCK-587df5c89bd607a52f3e0ba71e671cd3&code=00&message=Approved. Successful&receiptno=RN1588277396664"
    }
  }
}

Handling 3DS authorization

With 3DS authorization, all you'll need to do is redirect your user to the specified redirect URL (the meta.authorization.redirect field). The user will then complete the authorization with their bank, and we'll redirect back to the redirect_url you specified initially, appending the tx_ref and status to the URL as query parameters.

Before redirecting, you should store the transaction ID returned from the 3DS response (data.id), as you'll need it to verify the payment when we redirect back to you (Step 6).

const transactionId = response.data.id;
const flwRef = response.data.flw_ref;
// Store the transaction ID
// so we can look it up later with the tx_ref
await redis.setAsync(`txref-${txRef}`, transactionId);
const authUrl = response.meta.authorization.redirect;
res.redirect(authUrl);
$transactionId = $response['data']['id'];
$txRef = $response['data']['tx_ref'];
// Store the transaction ID
// so we can look it up later with the tx_ref
Redis::set("txref-$txRef", $transactionId);
$authUrl = $response['meta']['authorization']['redirect'];
return redirect($authUrl);
transaction_id = response['data']['id']
tx_ref = response['data']['tx_ref']
# Store the transaction ID
# so we can look it up later with the tx_ref
redis.set "txref-#{tx_ref}", transaction_id
auth_url = response['meta']['authorization']['redirect']
redirect to: auth_url
transaction_id = response['data']['id']
tx_ref = response['data']['tx_ref']
# Store the transaction ID
# so we can look it up later with the tx_ref
redis.set(f'txref-{tx_ref}', transaction_id)
auth_url = response['meta']['authorization']['redirect']
redirect(auth_url)
transactionId := response["data"]["id"]
txRef := response["data"]["tx_ref"]
// Store the transaction ID
// so we can look it up later with the tx_ref
redis.Set("txref-" + txRef, transactionId)
authUrl := response["meta"]["authorization"]["redirect"]
c.Redirect(authUrl)

With 3DS authorization, you won't need to do Step 5 (validate the charge). In your redirect route, you can then skip to Step 6: verifying the payment.

Handling PIN and AVS authorization

With PIN and AVS authorization, you'll need to collect the specified fields (ie the card's PIN or address details) and add them to your initial payload (in an authorization object). Then you'll encrypt the new payload and send it to the same charge card endpoint.

Yes, this means you'll be calling the charge card endpoint twice—the first time to find out the authorization mode, and the second to authorize the charge.

In this example, that means we'll end up with a payload like this:

{
  "card_number": "4556052704172643",
  "cvv": "899",
  "expiry_month": "01",
  "expiry_year": "23",
  "currency": "NGN",
  "amount": "7500",
  "email": "developers@flutterwavego.com",
  "fullname": "Flutterwave Developers",
  "tx_ref": "MC-3243e",
  "redirect_url":"https://your-awesome.app/payment-redirect",
+  "authorization": {
+    "mode": "pin",
+    "pin": "3310"
+  }
}
{
  "card_number": "4556052704172643",
  "cvv": "899",
  "expiry_month": "01",
  "expiry_year": "23",
  "currency": "NGN",
  "amount": "7500",
  "email": "developers@flutterwavego.com",
  "fullname": "Flutterwave Developers",
  "tx_ref": "MC-3243e",
  "redirect_url":"https://your-awesome.app/payment-redirect",
+  "authorization": {
+    "mode": "avs_noauth",
+    "city":  "San Francisco",
+    "address":  "69 Fremont Street",
+    "state":  "CA",
+    "country":  "US",
+    "zipcode":  "94105"
+  }
}

Then you encrypt the new payload and send it to the charge card endpoint.

Step 5: Validate the charge

After encrypting and sending the new payload, if the authorization is successful, you'll get a response that looks like one of these:

{
  "status": "success",
  "message": "Charge initiated",
  "data": {
    "id": 288192886,
    "tx_ref": "MC-3243e",
    "flw_ref": "HomerSimpson/FLW275389391",
    "device_fingerprint": "N/A",
    "amount": 7500,
    "charged_amount": 100,
    "app_fee": 1.4,
    "merchant_fee": 0,
    "processor_response": "Kindly enter the OTP sent to *******0328",
    "auth_model": "PIN",
    "currency": "NGN",
    "ip": "N/A",
    "narration": "CARD Transaction ",
    "status": "pending",
    "auth_url": "N/A",
    "payment_type": "card",
    "fraud_status": "ok",
    "charge_type": "normal",
    "created_at": "2021-07-15T14:06:55.000Z"
    "account_id": 17321,
    "customer": {
      "id": 370672,
      "phone_number": null,
      "name": "Flutterwave Developers",
      "email": "developers@flutterwavego.com",
      "created_at": "2021-07-15T14:06:55.000Z"
    },
    "card": {
      "first_6digits": "455605",
      "last_4digits": "2643",
      "issuer": "MASTERCARD GUARANTY TRUST BANK Mastercard Naira Debit Card",
      "country": "NG",
      "type": "MASTERCARD",
      "expiry": "01/23"
    }
  },
  "meta": {
    "authorization": {
      "mode": "otp",
      "endpoint": "/v3/validate-charge"
    }
  }
}
{
  "status": "success",
  "message": "Charge initiated",
  "data": {
    "id": 1222390,
    "tx_ref": "MC-3243e",
    "flw_ref": "FLW-MOCK-0f6a173e664d3c6c98e3026ab4baa3e0",
    "device_fingerprint": "N/A",
    "amount": 7500,
    "charged_amount": 20000,
    "app_fee": 760,
    "merchant_fee": 0,
    "processor_response": "Pending Validation",
    "auth_model": "VBVSECURECODE",
    "currency": "NGN",
    "ip": "169.123.8.9",
    "narration": "CARD Transaction ",
    "status": "pending",
    "payment_type": "card",
    "fraud_status": "ok",
    "charge_type": "normal",
    "created_at": "2021-07-15T14:06:55.000Z"
    "account_id": 17321,
    "customer": {
      "id": 370672,
      "phone_number": null,
      "name": "Flutterwave Developers",
      "email": "developers@flutterwavego.com",
      "created_at": "2021-07-15T14:06:55.000Z"
    },
    "card": {
      "first_6digits": "455605",
      "last_4digits": "2643",
      "issuer": "VISA CREDIT",
      "country": "US",
      "type": "VISA",
      "expiry": "01/23"
    }
  },
  "meta": {
    "authorization": {
      "mode": "redirect",
      "redirect": "https://ravesandboxapi.flutterwave.com/mockvbvpage?ref=FLW-MOCK-0f6a173e664d3c6c98e3026ab4baa3e0&code=00&message=Approved. Successful&receiptno=RN1586794091383"
    }
  }
}

This response tells you how to proceed.

Note that even though the status field is "success" (the charge has been initiated), the data.status field is "pending", meaning that the transaction hasn't yet been approved by the customer. You'll need to validate the transaction to complete it.

As you can see, depending on the type of card you're charging, you'll get one of two things in the new meta.authorization.mode field.

  1. redirect: In this case, you'll get a processor_response message saying "Pending Validation". You'll need to redirect your customers to the returned link where they can complete the payment. Afterwards, we'll redirect the customer back to the redirect_url you specified earlier, with a tx_ref and status. This means you don't need to do anything else, and your redirect handler can skip to Step 6
  2. otp: This means an OTP has been sent to your customer's mobile phone. processor_response will contain instructions you can display to the cardholder. When the user enters the OTP, you'll need to validate the transaction by calling our validate charge endpoint with the customer's OTP and the flw_ref for the transaction (which was returned in the earlier response).
const response = await flw.Charge.validate({
    otp: req.body.otp,
    flw_ref: req.session.flw_ref
});
$response = $cardChargeService->validateTransaction([
    'otp' => $req->body->get('otp'),
    'flw_ref' => session('flw_ref'),
]);
response = charge_card.validate_charge(session[:flw_ref], params[:otp])
response = rave.Card.validate(request.session["flwRef"], request.POST['otp']);
payload := rave.CardValidateData{
    Reference: sessions.Default(c).Get("flw_ref"),
    Otp: otp,
}
err, response := card.ValidateCharge(payload)
curl --request POST \
   --url https://api.flutterwave.com/v3/validate-charge \
   --header 'Authorization: Bearer YOUR_SECRET_KEY' \
   --header 'content-type: application/json' \
   --data '{
        "otp": "398761",
        "flw_ref": "FLW19ee7de40fed192d2485202f"
    }'

You'll get a response like this:

{
  "status": "success",
  "message": "Charge validated",
  "data": {
    "id": 288192886,
    "tx_ref": "MC-3243e",
    "flw_ref": "HomerSimpson/FLW275407301",
    "device_fingerprint": "N/A",
    "amount": 7500,
    "charged_amount": 7500,
    "app_fee": 1.4,
    "merchant_fee": 0,
    "processor_response": "Approved by Financial Institution",
    "auth_model": "PIN",
    "currency": "NGN",
    "ip": "N/A",
    "narration": "CARD Transaction ",
    "status": "successful",
    "auth_url": "N/A",
    "payment_type": "card",
    "fraud_status": "ok",
    "charge_type": "normal",
    "created_at": "2021-07-15T14:06:55.000Z",
    "account_id": 17321,
    "customer": {
      "id": 370672,
      "phone_number": null,
      "name": "Flutterwave Developers",
      "email": "developers@flutterwavego.com",
      "created_at": "2021-07-15T14:06:55.000Z"
    },
    "card": {
      "first_6digits": "455605",
      "last_4digits": "2643",
      "issuer": "MASTERCARD GUARANTY TRUST BANK Mastercard Naira Debit Card",
      "country": "NG",
      "type": "MASTERCARD",
      "expiry": "01/23"
    }
  }
}

The data.status field indicates that the charge was successful. However, to be on the safe side, you should always verify the payment.

Step 6: Verify the payment

Almost done! The last step is to verify that the payment was successful before giving value to your customer. To do so, call our verify transaction endpoint with your transaction_id. See our guide to transaction verification for details.

// If we came from a redirect (Step 4), we'll need to
// fetch the transactionID we stored earlier, using the tx_ref
const txRef = req.query.tx_ref
const transactionId = await redis.getAsync(`txref-${txRef}`);
// Otherwise, if we came from a validate process (Step 5),
// we can just get the transaction ID from the response
const transactionId = response.data.id;
flw.Transaction.verify({ id: transactionId });
// If we came from a redirect (Step 4), we'll need to
// fetch the transactionID we stored earlier, using the tx_ref
$txRef = $req->query->get('tx_ref');
$transactionId = Redis::get("txref-$txRef");
// Otherwise, if we came from a validate process (Step 5),
// we can just get the transaction ID from the response
$transactionId = $response['data']['id'];
$cardChargeService->verifyTransaction($transactionId);
# If we came from a redirect (Step 4), we'll need to
# fetch the transactionID we stored earlier, using the tx_ref
transaction_id = redis.get "txref-#{params[:tx_ref]}"
# Otherwise, if we came from a validate process (Step 5),
# we can just get the transaction ID from the response
transaction_id = response['data']['id']
charge_card.verify_charge transaction_id
# If we came from a redirect (Step 4), we'll need to
# fetch the transactionID we stored earlier, using the tx_ref
transaction_id = redis.get(f'txref-#{request.GET["tx_ref"]}')
# Otherwise, if we came from a validate process (Step 5),
# we can just get the transaction ID from the response
transaction_id = response['data']['id']
rave.Card.verify(transaction_id)
payload := rave.CardVerifyData{
    Reference: txRef,
}
err, response := card.VerifyCard(payload)
curl --request GET \
   --url https://api.flutterwave.com/v3/transactions/288200108/verify \
   --header 'Authorization: Bearer YOUR_SECRET_KEY' \
   --header 'content-type: application/json'

You'll get a response that looks like this, and you can see that the data.status field is now "successful".

{
  "status": "success",
  "message": "Transaction fetched successfully",
  "data": {
    "id": 288200108,
    "tx_ref": "MC-3243e",
    "flw_ref": "HomerSimpson/FLW275407301",
    "device_fingerprint": "N/A",
    "amount": 7500,
    "currency": "NGN",
    "charged_amount": 7500,
    "app_fee": 1.4,
    "merchant_fee": 0,
    "processor_response": "Approved by Financial Institution",
    "auth_model": "PIN",
    "ip": "::ffff:10.5.179.3",
    "narration": "CARD Transaction ",
    "status": "successful",
    "payment_type": "card",
    "created_at": "2021-07-15T14:06:55.000Z",
    "account_id": 17321,
    "card": {
      "first_6digits": "455605",
      "last_4digits": "2643",
      "issuer": "MASTERCARD GUARANTY TRUST BANK Mastercard Naira Debit Card",
      "country": "NG",
      "type": "MASTERCARD",
      "token": "flw-t1nf-93da56b24f8ee332304cd2eea40a1fc4-m03k",
      "expiry": "01/23"
    },
    "meta": null,
    "amount_settled": 7500,
    "customer": {
      "id": 370672,
      "phone_number": null,
      "name": "Anonymous customer",
      "email": "user@gmail.com",
      "created_at": "2020-04-30T20:09:56.000Z"
    }
  }
}

All done! 🎉🎉

Putting it all together

Putting it all together, here's what an implementation of direct card charge might look like (in part):

// In an Express-like app:

// The route where we initiate payment (Steps 1 - 3)
app.post('/pay', async (req, res) => {
    const payload = {
        card_number: req.body.card_number,
        cvv: req.body.card_cvv,
        expiry_month: req.body.card_expiry_year,
        expiry_year: req.body.card_expiry_year,
        currency: 'NGN',
        amount: product.price,
        email: req.user.email,
        fullname: req.body.card_name,
        // Generate a unique transaction reference
        tx_ref: generateTransactionReference(),
        redirect_url: process.env.APP_BASE_URL + '/pay/redirect',
        enckey: process.env.FLW_ENCRYPTION_KEY
    }
    const response = await flw.Charge.card(payload);

    switch (response?.meta?.authorization?.mode) {
        case 'pin':
        case 'avs_noauth':
            // Store the current payload
            req.session.charge_payload = payload;
            // Now we'll show the user a form to enter
            // the requested fields (PIN or billing details)
            req.session.auth_fields = response.meta.authorization.fields;
            req.session.auth_mode = response.meta.authorization.mode;
            return res.redirect('/pay/authorize');
        case 'redirect':
            // Store the transaction ID
            // so we can look it up later with the flw_ref
            await redis.setAsync(`txref-${response.data.tx_ref}`, response.data.id);
            // Auth type is redirect,
            // so just redirect to the customer's bank
            const authUrl = response.meta.authorization.redirect;
            return res.redirect(authUrl);
        default:
            // No authorization needed; just verify the payment
            const transactionId = response.data.id;
            const transaction = await flw.Transaction.verify({ id: transactionId });
            if (transaction.data.status == "successful") {
                return res.redirect('/payment-successful');
            } else if (transaction.data.status == "pending") {
                // Schedule a job that polls for the status of the payment every 10 minutes
                transactionVerificationQueue.add({id: transactionId});
                return res.redirect('/payment-processing');
            } else {
                return res.redirect('/payment-failed');
            }
    }
});


// The route where we send the user's auth details (Step 4)
app.post('/pay/authorize', async (req, res) => {
    const payload = req.session.charge_payload;
    // Add the auth mode and requested fields to the payload,
    // then call chargeCard again
    payload.authorization = {
        mode: req.session.auth_mode,
    };
    req.session.auth_fields.forEach(field => {
        payload.authorization.field = req.body[field];
    });
    const response = await flw.Charge.card(payload);

    switch (response?.meta?.authorization?.mode) {
        case 'otp':
            // Show the user a form to enter the OTP
            req.session.flw_ref = response.data.flw_ref;
            return res.redirect('/pay/validate');
        case 'redirect':
            const authUrl = response.meta.authorization.redirect;
            return res.redirect(authUrl);
        default:
            // No validation needed; just verify the payment
            const transactionId = response.data.id;
            const transaction = await flw.Transaction.verify({ id: transactionId });
            if (transaction.data.status == "successful") {
                return res.redirect('/payment-successful');
            } else if (transaction.data.status == "pending") {
                // Schedule a job that polls for the status of the payment every 10 minutes
                transactionVerificationQueue.add({id: transactionId});
                return res.redirect('/payment-processing');
            } else {
                return res.redirect('/payment-failed');
            }
    }
});


// The route where we validate and verify the payment (Steps 5 - 6)
app.post('/pay/validate', async (req, res) => {
    const response = await flw.Charge.validate({
        otp: req.body.otp,
        flw_ref: req.session.flw_ref
    });
    if (response.data.status === 'successful' || response.data.status === 'pending') {
        // Verify the payment
        const transactionId = response.data.id;
        const transaction = flw.Transaction.verify({ id: transactionId });
        if (transaction.data.status == "successful") {
            return res.redirect('/payment-successful');
        } else if (transaction.data.status == "pending") {
            // Schedule a job that polls for the status of the payment every 10 minutes
            transactionVerificationQueue.add({id: transactionId});
            return res.redirect('/payment-processing');
        }
    }

    return res.redirect('/payment-failed');
});

// Our redirect_url. For 3DS payments, Flutterwave will redirect here after authorization,
// and we can verify the payment (Step 6)
app.post('/pay/redirect', async (req, res) => {
    if (req.query.status === 'successful' || req.query.status === 'pending') {
        // Verify the payment
        const txRef = req.query.tx_ref;
        const transactionId = await redis.getAsync(`txref-${txRef}`);
        const transaction = flw.Transaction.verify({ id: transactionId });
        if (transaction.data.status == "successful") {
            return res.redirect('/payment-successful');
        } else if (transaction.data.status == "pending") {
            // Schedule a job that polls for the status of the payment every 10 minutes
            transactionVerificationQueue.add({id: transactionId});
            return res.redirect('/payment-processing');
        }
    }

    return res.redirect('/payment-failed');
});
// In a Laravel-like app:

// The route where we initiate payment (Steps 1 - 3)
Route::post('/pay', function ($req) {
    $payload = [
        'card_number' => $req->input('card_number'),
        'cvv' => $req->input('card_cvv'),
        'expiry_month' => $req->input('card_expiry_month'),
        'expiry_year' => $req->input('card_expiry_year'),
        'currency' => 'NGN',
        'amount' => $product->price,
        'email' => auth()->email,
        'fullname' => $req->input('card_name'),
        'tx_ref' => generateTransactionReference(),
        'redirect_url' => url('/pay/redirect'),
    ];

    $response = $cardChargeService->chargeCard($payload);
    switch ($response['meta']['authorization']['mode'] ?? null) {
        case 'pin':
        case 'avs_noauth':
            // Store the current payload
            Session::put('charge_payload', $payload);
            // Now we'll show the user a form to enter
            // the requested fields (PIN or billing details)
            Session::put('auth_fields', $response['meta']['authorization']['fields']);
            Session::put('auth_mode', $response['meta']['authorization']['mode']);
            return redirect('/pay/authorize');
        case 'redirect':
            // Store the transaction ID
            // so we can look it up later with the flw_ref
            Redis::set("txref-{$response['data']['tx_ref']}", $response['data']['id']);
            // Auth type is redirect,
            // so just redirect to the customer's bank
            $authUrl = $response['meta']['authorization']['redirect'];
            return redirect($authUrl);
        default:
            // No authorization needed; just verify the payment
            $transactionId = $response['data']['id'];
            $transaction = $cardChargeService->verifyTransaction($transactionId);
            if ($transaction['data']['status'] == "successful") {
                return redirect('/payment-successful');
            } else if ($transaction['data']['status'] == "pending") {
                // Schedule a job that polls for the status of the payment every 10 minutes
                dispatch(new CheckTransactionStatus($transactionId);
                return redirect('/payment-processing');
            } else {
                return redirect('/payment-failed');
            }
        }
});


// The route where we send the user's auth details (Step 4)
Route::post('/pay/authorize', function ($req) {
    $payload = Session::get('charge_payload');
    // Add the auth mode and requested fields to the payload,
    // then call chargeCard again
    $payload['authorization'] = [
        'mode' => Session::get('auth_mode'),
    ];
    foreach (Session::get('auth_fields') as $field) {
        $payload['authorization'][$field] = $req->input($field);
    }
    $response = $cardChargeService->cardCharge(payload);

    switch ($response['meta']['authorization']['mode'] ?? null) {
        case 'otp':
            // Show the user a form to enter the OTP
            Session::put('flw_ref', $response['data']['flw_ref']);
            return redirect('/pay/validate');
        case 'redirect':
            $authUrl = $response['meta']['authorization']['redirect'];
            return redirect($authUrl);
        default:
            // No validation needed; just verify the payment
            $transactionId = $response['data']['id'];
            $transaction = $cardChargeService->verifyTransaction($transactionId);
            if ($transaction['data']['status'] == "successful") {
                return redirect('/payment-successful');
            } else if ($transaction['data']['status'] == "pending") {
                // Schedule a job that polls for the status of the payment every 10 minutes
                dispatch(new CheckTransactionStatus($transactionId);
                return redirect('/payment-processing');
            } else {
                return redirect('/payment-failed');
            }
    }
});

// The route where we validate and verify the payment (Steps 5 - 6)
Route::post('/pay/validate', function ($req) {
    $response = $cardChargeService->validateTransaction([
        'otp' => $req->body->get('otp'),
        'flw_ref' => session('flw_ref'),
    ]);
    if ($response['data']['status'] === 'successful' || $response['data']['status'] === 'pending') {
        // Verify the payment
        $transactionId = $response['data']['id'];
        $transaction = $cardChargeService->verifyTransaction($transactionId);
        if ($transaction['data']['status'] == "pending") {
            return redirect('/payment-successful');
        } else if ($transaction['data']['status'] == "pending") {
            // Schedule a job that polls for the status of the payment every 10 minutes
            dispatch(new CheckTransactionStatus($transactionId);
            return redirect('/payment-processing');
        }
    }

    return redirect('/payment-failed');
});

// Our redirect_url. For 3DS payments, Flutterwave will redirect here after authorization,
// and we can verify the payment (Step 6)
Route::post('/pay/redirect', function ($req) {
    if ($req->query->get('status') === 'successful') {
        // Verify the payment
        $txRef = $req->query->get('tx_ref');
        $transactionId = Redis::get("txref-$txRef");
        $transaction = $cardChargeService->verifyTransaction($transactionId);
        if ($transaction['data']['status'] == "successful") {
            return redirect('/payment-successful');
        } else if ($transaction['data']['status'] == "pending") {
            // Schedule a job that polls for the status of the payment every 10 minutes
            dispatch(new CheckTransactionStatus($transactionId);
            return redirect('/payment-processing');
        }
    }

    return redirect('/payment-failed');
});
# In a Rails-like app:

# Handles POST /pay
# The route where we initiate payment (Steps 1 - 3)
def pay
    charge_card = Card.new(flw)
    payload = {
        card_number: params[:card_number],
        cvv: params[:card_cvv],
        expiry_month: params[:card_expiry_month],
        expiry_year: params[:card_expiry_year],
        currency: 'NGN',
        amount: session[:amount],
        email: current_user.email,
        fullname: current_user.name,
        tx_ref: generate_transaction_reference,
        redirect_url: 'https://your-awesome.app/payment-redirect',
    }
    response = charge_card.initiate_charge payload

    case response["meta"]["authorization"]["mode"] rescue nil
    when 'pin', 'avs_noauth'
        # Store the current payload
        session[:charge_payload] = payload
        # Now we'll show the user a form to enter
        # the requested fields (PIN or billing details)
        session[:auth_fields] = response["meta"]["authorization"]["fields"]
        session[:auth_mode] = response["meta"]["authorization"]["mode"]
        redirect to: '/pay/authorize'
    when 'redirect'
        # Store the transaction ID
        # so we can look it up later with the flw_ref
        redis.set("txref-#{response['data']['tx_ref']}", response['data']['id'])
        # Auth type is redirect,
        # so just redirect to the customer's bank
        auth_url = response["meta"]["authorization"]["redirect"]
        redirect to: auth_url
    else
        # No authorization needed; just verify the payment
        transaction = charge_card.verify_charge transaction_id
        if transaction['data']['status'] == "successful"
            return redirect to: '/payment-successful'
        else if transaction['data']['status'] == "pending"
            # Schedule a job that polls for the status of the payment every 10 minutes
            CheckTransactionStatus.perform_later transaction_id
            return redirect to: '/payment-processing'
        else
            return redirect to: '/payment-failed'
        end
    end
end

# Handles POST /pay/authorize
# The route where we send the user's auth details (Step 4)
def authorize_payment
    payload = session[:charge_payload]
    # Add the auth mode and requested fields to the payload,
    # then call chargeCard again
    payload['authorization'] = {
        mode: session[:auth_mode],
    }
    session.auth_fields.each do |field|
        payload['authorization'][field] = params[field]
    end
    response = charge_card.initiate_charge payload

    case response["meta"]["authorization"]["mode"] rescue nil
    when 'otp'
        # Show the user a form to enter the OTP
        session[:flw_ref] = response['data']['flw_ref']
        redirect to: '/pay/validate'
    when 'redirect'
        auth_url = response["meta"]["authorization"]["redirect"]
        redirect to: auth_url
    else
        # No validation needed; just verify the payment
        transaction_id = response['data']['id']
        transaction = charge_card.verify_charge transaction_id
        if transaction['data']['status'] == "successful"
            return redirect to: '/payment-successful'
        else if transaction['data']['status'] == "pending"
            # Schedule a job that polls for the status of the payment every 10 minutes
            CheckTransactionStatus.perform_later transaction_id
            return redirect to: '/payment-processing'
        else
            return redirect to: '/payment-failed'
        end
    end
end

# Handles POST /pay/validate
# The route where we validate and verify the payment (Steps 5 - 6)
def validate_payment
    response = charge_card.validate_charge(session[:flw_ref], params[:otp])
    if (response['data']['status'] === 'successful' || response['data']['status'] === 'pending')
        # Verify the payment
        transaction = charge_card.verify_charge transaction_id
        if transaction['data']['status'] == "successful"
            return redirect to: '/payment-successful'
        else if transaction['data']['status'] == "pending"
            # Schedule a job that polls for the status of the payment every 10 minutes
            CheckTransactionStatus.perform_later transaction_id
            return redirect to: '/payment-processing'
        end
    end

    redirect to: '/payment-failed'
end

# Handles POST /pay/redirect
# Our redirect_url. For 3DS payments, Flutterwave will redirect here after authorization,
# and we can verify the payment (Step 6)
def payment_redirect
    if (params[:status] === 'successful' || params[:status] === 'pending')
        # Verify the payment
        tx_ref = params[:tx_ref]
        transaction_id = redis.get(`txref-#{tx_ref}`)
        charge_card.verify_charge transaction_id
        redirect to: '/payment-successful'
   else if transaction['data']['status'] == "pending"
      # Schedule a job that polls for the status of the payment every 10 minutes
      CheckTransactionStatus.perform_later transaction_id
      return redirect to: '/payment-processing'
   else
        redirect to: '/payment-failed'
    end
end
# In a Django-like app:

# Handles POST /pay
# The route where we initiate payment (Steps 1 - 3)
def pay:
    details = {
        "card_number": '4556052704172643',
        "cvv": '899',
        "expiry_month": '01',
        "expiry_year": '23',
        "currency": 'NGN',
        "amount": '7500',
        "email": 'developers@flutterwavego.com',
        "fullname": 'Flutterwave Developers',
        "tx_ref": 'MC-3243e',
        "redirect_url": 'https://your-awesome.app/payment-redirect',
    }
    response = rave.Card.charge(payload)
    mode = response.get("meta", {}).get("authorization", {}).get("mode", None)
    if mode == 'pin' or mode == 'avs_noauth':
        # Store the current payload
        request.session["charge_payload"] = payload
        # Now we'll show the user a form to enter
        # the requested fields (PIN or billing details)
        request.session["auth_fields"] = response["meta"]["authorization"]["fields"]
        request.session["auth_mode"] = response["meta"]["authorization"]["mode"]
        return redirect('/pay/authorize')
    elif mode == 'redirect':
        # Store the transaction ID
        # so we can look it up later with the flw_ref
        redis.set(f'txref-{response['data']['tx_ref']}', response['data']['id'])
        # Auth type is redirect,
        # so just redirect to the customer's bank
        auth_url = response["meta"]["authorization"]["redirect"]
        return redirect(auth_url)
    else:
        # No authorization needed; just verify the payment
        transaction = rave.Card.verify(transaction_id)
        if transaction['data']['status'] == "successful":
            return redirect('/payment-successful')
        elif transaction['data']['status'] == "pending":
            # Schedule a job that polls for the status of the payment every 10 minutes
            check_transaction_status(transaction_id)
            return redirect('/payment-successful')
        else:
             return redirect('/payment-failed')

# Handles POST /pay/authorize
# The route where we send the user's auth details (Step 4)
def authorize_payment:
    payload = request.session["charge_payload"]
    # Add the auth mode and requested fields to the payload,
    # then call chargeCard again
    payload['authorization'] = {
        "mode": request.session["auth_mode"],
    }
    for field in request.session.auth_fields:
        payload['authorization'][field] = request.POST[field]
    response = rave.Card.charge(payload)

    mode = response.get("meta", {}).get("authorization", {}).get("mode", None)
    if mode == 'otp':
        # Show the user a form to enter the OTP
        request.session["flw_ref"] = response['data']['flw_ref']
        return redirect('/pay/validate')
    elif mode == 'redirect':
        auth_url = response["meta"]["authorization"]["redirect"]
        return redirect(auth_url)
    else:
        # No validation needed; just verify the payment
        transaction_id = response["data"]["id"]
        transaction = rave.Card.verify(transaction_id)
        if transaction['data']['status'] == "successful":
            return redirect('/payment-successful')
        elif transaction['data']['status'] == "pending":
            # Schedule a job that polls for the status of the payment every 10 minutes
            check_transaction_status(transaction_id)
            return redirect('/payment-successful')
        else:
             return redirect('/payment-failed')

# Handles POST /pay/validate
# The route where we validate and verify the payment (Steps 5 - 6)
def validate_payment:
    response = charge_card.validate_charge(request.session["flw_ref"], request.POST["otp"])
    if response['data']['status'] == 'successful':
        # Verify the payment
        transaction = rave.Card.verify(transaction_id)
        if transaction['data']['status'] == "successful":
            return redirect('/payment-successful')
        elif transaction['data']['status'] == "pending":
            # Schedule a job that polls for the status of the payment every 10 minutes
            check_transaction_status(transaction_id)
            return redirect('/payment-successful')

    return redirect('/payment-failed')

# Handles POST /pay/redirect
# Our redirect_url. For 3DS payments, Flutterwave will redirect here after authorization,
# and we can verify the payment (Step 6)
def payment_redirect:
    if request.GET["status"] == 'successful' or request.GET["status"] == 'pending':
        # Verify the payment
        tx_ref = request.POST["tx_ref"]
        transaction_id = redis.get(f`txref-{tx_ref}`)
        transaction = rave.Card.verify(transaction_id)
        if transaction['data']['status'] == 'successful':
            return redirect('/payment-successful')
        elif transaction['data']['status'] == "pending":
            # Schedule a job that polls for the status of the payment every 10 minutes
            check_transaction_status(transaction_id)
            return redirect('/payment-successful')
    else:
        return redirect('/payment-failed')

Webhooks

You can also verify the payment in a webhook handler. Here's an example of the payload we send to your webhook for successful card charges. The event type is charge.completed, and the data object contains the transaction information.

{
  "event": "charge.completed",
  "data": {
    "id": 285959875,
    "tx_ref": "Links-616626414629",
    "flw_ref": "PeterEkene/FLW270177170",
    "device_fingerprint": "a42937f4a73ce8bb8b8df14e63a2df31",
    "amount": 100,
    "currency": "NGN",
    "charged_amount": 100,
    "app_fee": 1.4,
    "merchant_fee": 0,
    "processor_response": "Approved by Financial Institution",
    "auth_model": "PIN",
    "ip": "197.210.64.96",
    "narration": "CARD Transaction ",
    "status": "successful",
    "payment_type": "card",
    "created_at": "2020-07-06T19:17:04.000Z",
    "account_id": 17321,
    "customer": {
      "id": 215604089,
      "name": "Yemi Desola",
      "phone_number": null,
      "email": "user@gmail.com",
      "created_at": "2020-07-06T19:17:04.000Z"
    },
    "card": {
      "first_6digits": "123456",
      "last_4digits": "7889",
      "issuer": "VERVE FIRST CITY MONUMENT BANK PLC",
      "country": "NG",
      "type": "VERVE",
      "expiry": "02/23"
    }
  }
}
Loading...