Charge Card

Learn how you can compliantly collect card payments with your own custom checkout.

About Compliance

This API should only be used by businesses who are PCI DSS compliant. Before using this API, please reach out to [email protected], or your relationship manager for guidance. Paystack may choose to close your integration with us should you use this API without meeting our compliance requirements.

Charging a card

The Charge APIAPI requires you to add the card object to the transaction payload. The card object must contain cvv, number, expiry_month and expiry_year.

1{
2 "email": "[email protected]",
3 "amount": "10000",
4 "card": {
5 "cvv": "408",
6 "number": "4084084084084081",
7 "expiry_month": "01",
8 "expiry_year": "99"
9 },
10 "pin": "0000"
11}

Card transactions require an additional authentication step for security. Some transactions can go through multiple authentication steps which is normal.

Image showing transaction validation flow for a card

Handling charge responses

In cases where the API is working and the details provided are accurate, you should expect any of the following responses from the API.

StatusDescription
pendingTransaction is being processed. Call Check pending chargeAPI at least 10 seconds after getting this status to check status
timeoutTransaction has failed. You may start a new charge after showing the data.message to user.
successTransaction is successful. Give value after verifying the amount and transaction details are good. We recommend implementing Webhooks
send_pinThe transaction requires the customer’s PIN. Show data.display_text to user with input for PIN and call Submit PINAPI endpoint with reference and PIN
send_phoneThe transaction requires the phone number associated to the customer’s bank account. Show data.display_text to user with input for phone and call Submit PhoneAPI endpoint with reference and phone
send_birthdayCustomer's birthday is needed to complete the transaction. Show data.display_text to user with an input that accepts the birthdate and submit to the Submit BirthdayAPI endpoint with reference and birthday.
send_addressThe transaction requires the customer’s billing address. Provide input fields for street address, city, state and ZIP code. Submit the details to the Submit Address endpointAPI endpoint with reference and address.
send_otpThe customer will be sent a One-Time Password to authorise the transaction.
open_urlRequires the customer to complete the transaction on a bank controlled page. You should open the data.url in user’s browser. You can specify an optional URL to which we should redirect the user after the attempt is complete by adding redirect_to=[url] as a GET query parameter to the Check Pending ChargeAPI endpoint at least 5 seconds after user closes browser page or after 5 minutes, whichever comes first.
pay_offlineResponse will include data.ussd_code. You should prompt data.ussd_code as a phone string to user. User needs to dial the provided USSD code to complete the transaction offline
failedTransaction failed. No remedy for this, start a new charge after showing the data.message to user

You can simulate these responses using our Test Cards in your test environment. There’s also no particular order that these checks appear in. You can also draw inspiration from the Paystack Checkout on how to handle them. The statuses discussed below are found in the data.status part of the response payload.

1{
2 "status": true,
3 "message": "Charge attempted",
4 "data": {
5 "reference": "apg8mnjf5yg7zp8",
6 "status": "send_pin" // The status called send_pin
7 }
8 }
Note

There are cases where a transaction goes through multiple authentication steps. You should ensure you’re tracking the status and displaying the correct information to your customer.

Open URL

The open_url status, contains a secure URL where you should redirect the customer to. This is known as 3DS (Three Domain Secure). This url opens a page owned by the customer’s bank, where they will be asked to complete some authentication step. The customer’s bank determines the type of authentication and it can vary from transaction to transaction. Since the page is controlled by the customer’s bank, Paystack has no visibility on what’s happening on the page. As such the transaction status remains as ongoing until we receive an update from the bank.

1{
2 "status": true,
3 "message": "Charge attempted",
4 "data": {
5 "reference": "apg8mnjf5yg7zp8",
6 "url": "https://standard.paystack.co/charge/auth/0dc2d13i4aq",
7 "status": "open_url"
8 }
9 }

You can force the open_url for all transactions. This will create a checkout interface that will automatically handle multiple authentication steps like OTP, PIN or Address. To achieve this, set the use_hosted_url = true to your charge payload.

1{
2 "status": true,
3 "message": "Charge attempted",
4 "data": {
5 "reference": "7fw1oh5nmxrf6e8",
6 "status": "open_url",
7 "url": "https://standard.paystack.co/charge/redirect_hosted_url/tyct59luffn7wz2"
8 }
9 }

To customize the hosted page, append the whitelabel=1 parameter to the url returned. This will remove the Paystack logo and the "Secured by Paystack" tagline. You can also append the redirect_to parameter to redirect the customer to the url when the transaction is complete.

Send OTP

The send_otp status means that the bank has sent the customer a One-Time Password, to the phone number or email address associated with the customer’s account. You should present an input field and the value of the display_text parameter.

In some cases, the display_text gives the customer a hint of where the OTP will be sent. This can be a masked form of their phone number or email address. This is controlled by the customer’s bank.

1{
2 "status": true,
3 "message": "Charge attempted",
4 "data": {
5 "reference": "vpw8wvwz4lghrgb",
6 "status": "send_otp",
7 "display_text": "Please enter OTP sent to..."
8 }
9 }

You should send the customer’s input to the Submit OTPAPI endpoint with provided OTP and transaction reference.

Send PIN

The send_pin status indicates that the customer needs to provide the PIN belonging to the card. You should create an input field that masks the PIN and the details should be sent to the Submit PIN endpointAPI with the transaction reference.

1{
2 "status": true,
3 "message": "Charge attempted",
4 "data": {
5 "reference": "apg8mnjf5yg7zp8",
6 "status": "send_pin"
7 }
8 }

Send phone

The send_phone status requests the customer to provide the phone number associated with the card. You should show the display_text together with an input field for them to enter the phone number. The data should be sent to the Submit Phone endpointAPI with the transaction reference.

1{
2 "status": true,
3 "message": "Charge attempted",
4 "data": {
5 "reference": "apg8mnjf5yg7zp8",
6 "status": "send_phone",
7 "display_text": "Kindly enter a mobile no (at least 10 digits)"
8 }
9 }

Send address

This status is common for cards issued in the UK, USA, Canada, Australia, and the Netherlands. Provide input fields for street address, city, state, and ZIP code, then submit them to the Submit Address endpointAPI.

To format the address correctly, call our List StatesAPI endpoint to get the valid states and codes for the card's country of issue. You should cache these results in your database for faster retrieval when needed.

The customer may need to complete another authentication step, so handle the response accordingly. Once they finish authenticating, you receive a charge.success webhook event and a redirect to the callback_url if it's been set up.

If the charge fails, the response status is failed. Log data.message and show an error message to the customer.

1{
2 "status": false,
3 "message": "Charge attempted",
4 "data": {
5 "reference": "9p0kbvxloo6grfu",
6 "status": "failed",
7 "message": "State not found"
8 }
9}
Note

If you want to offer the customer a retrial, then you should start a new transaction with a new reference because the current transaction won’t proceed even after providing correct details.

Authenticated recurring charges

If your business requires authentication for Recurring Charges , you send the authorization_code to the Charge endpoint. This returns the open_url response as shown in the Open URL section. The customer is taken through the same authentication steps as a normal charge.

1curl 'https://api.paystack.co/charge'
2 -H 'Authorization: Bearer YOUR_SECRET_KEY'
3 -H 'Content-Type: application/json'
4 -d '{
5 "authorization_code" : "AUTH_abc123xyz",
6 "email": "[email protected]",
7 "amount": "3000"
8 }'
9 -X POST

Card verification

About Verification

The card verification flow allows businesses to authenticate their customer's card before making an actual charge. This additional step reduces fraud, and validates the card before a charge is made.

Card verification allows businesses to perform zero amount authorization. This is where the customer's card is authenticated and validated before a transaction is made to charge the card. The additional steps ensure that the actual transaction charge won't fail due to card issues.

Initialize a card authorization

The first step need is to create a card authorization using the Initialize Authorization endpoint.

1curl 'https://api.paystack.co/customer/authorization/initialize'
2-H 'Content-Type: application/json'
3-H 'Authorization: Bearer YOUR_SECRET_KEY_HERE'
4-d '{
5 "customer": {
6 "email": "[email protected]",
7 "first_name": "Test",
8 "last_name": "Person"
9 },
10 "currency": "KES",
11 "channel": "card",
12 "card": {
13 "number": "4084084084084081",
14 "cvv": "408",
15 "expiry_month": "11",
16 "expiry_year": "2098",
17 "cardholder_name": "Person Test"
18 },
19 "purpose":"ADD_CARD",
20 "recurring_consent":"true",
21 "return_url":"https://paystack.com"
22}'
23-X POST

The endpoint requires the customer object and the card details for a successful authentication. The table below shows the parameters in detail and their usage:

ParameterTypeRequiredDescription
channelStringTrueCurrently only card is supported here.
customerObjectTrueCustomer object with the customer's details (for example first name, last name or phone). The customer email is required.
currencyStringTrueThe currency you’re charging the card. Checkout our Supported Currencies.
cardObjectTrueCard object contains the details of the card you want to charge. All the card details are required.
purposeStringTrueUse ADD_CARD when you only want to validate the card without charging the customer. Use CHARGE when you’re ready to charge the customer.
transactionObjectFalseOnly required when the purpose is set to CHARGE. The transaction object supports the parameters in the Initialize Transaction endpoint.
recurring_consentBooleanFalseOnly required when the purpose is set to ADD_CARD. Always set to true if you intend to charge the card via Charge Authorization . If not it won't be considered reusable.
return_urlStringFalseA fully qualified URL to which the customer is redirected after successfully completing the authorization process.
callback_urlStringFalseA URL used to receive webhook events from Paystack. This overrides the webhook URL set on the dashboard.

The API returns a url where the 3DS authentication will occur. You should redirect the customer to this url for them to complete the 3DS authentication.

1{
2 "status": true,
3 "message": "Authorization initialized successfully.",
4 "data": {
5 "authorizationAccessCode": "ARC_xmk89GjXtX",
6 "action": "redirect",
7 "value": "https://api.paystack.co/payments/card/authentication?accessCode=ARC_xmk89GjXtX"
8 }
9}

Once the customer has successfully authenticated their card, the zero_charge_authorization.success webhook event is sent. You'll notice that the amount is zero which indicates that the verification was successful.

1{
2 "event": "zero_charge_authorization.success",
3 "data": {
4 "accessCode": "ARC_a5qjGccALk",
5 "id": 5259712479,
6 "domain": "test",
7 "channel": "zero_charge_authorization",
8 "created_at": "2025-08-21T10:43:16.000Z",
9 "paid_at": "2025-08-21T10:43:21.000Z",
10 "status": "success",
11 "reference": "Tfngpm48tylcvek8",
12 "amount": 0,
13 "currency": "NGN",
14 "gateway_response": "Authentication Successful",
15 "authorization": {
16 "authorization_code": "AUTH_v5sin4pygk",
17 "active": false,
18 "last4": "4081",
19 "channel": "card",
20 "card_type": "Visa",
21 "bank": "Test Bank",
22 "exp_month": 11,
23 "exp_year": 2098,
24 "country_code": "NG",
25 "brand": "Visa",
26 "reusable": false,
27 "signature": "SIG_Kd7OMcecpX0OSQjwikb9"
28 },
29 "customer": {
30 "first_name": "Test",
31 "last_name": "Person",
32 "code": "CUS_a6sk80bd48ewkx3",
33 "email": "[email protected]",
34 "phone": null,
35 "metadata": null,
36 "risk_action": "default"
37 }
38 }
39}

The values you need to store from the webhook event are the email, accessCode and authorization_code. You’ll need these details to charge the card via the Charge AuthorizationAPI endpoint.

Sometimes the card verification can fail. You'll receive the card_verification.failed event. This contains the status and the reason why it failed.

1{
2 "event": "card_verification.failed",
3 "data": {
4 "accessCode": "ARC_AgOPDPO6GT",
5 "id": 4240253419,
6 "domain": "live",
7 "channel": "card",
8 "created_at": "2024-10-13T20:26:56.000Z",
9 "paid_at": null,
10 "status": "failed",
11 "reference": "rOwz1FeinItsKniPHki6IFy8L",
12 "amount": 415,
13 "currency": "NGN",
14 "gateway_response": "No gateway was able to process the request",
15 "authorization": {
16 "authorization_code": "AUTH_nq9m0w3hp0",
17 "active": false,
18 "last4": "4081",
19 "channel": "card",
20 "card_type": "MASTERCARD debit",
21 "bank": "Sterling Bank",
22 "exp_month": 12,
23 "exp_year": 2026,
24 "country_code": "NG",
25 "brand": "MASTERCARD",
26 "reusable": false,
27 "signature": "SIG_zNIpIL6FSkE0eXRsZj"
28 },
29 "customer": {
30 "first_name": "Test",
31 "last_name": "User",
32 "code": "CUS_yekqlhtmul7113x",
33 "email": "[email protected]",
34 "phone": null,
35 "metadata": null,
36 "risk_action": "default"
37 }
38 }
39}

You can also use the Verify Authorization endpoint to check on the status of a validation.

1curl --location 'https://api.paystack.co/customer/authorization/verify/ARC_ZNZLVqcfzx'
2--header 'Authorization: Bearer YOUR_SECRET_KEY_HERE'
3--header 'Content-Type: application/json'

Should the verification fail, you should reinitialize the verification process again.

1{
2 "status": true,
3 "message": "Verification successful",
4 "data": {
5 "status": "initialized",
6 "advise": "PROCEED",
7 "message": "Authentication initialized",
8 "authorizationCode": "AUTH_egn78f2uqf",
9 "customerEmail": "[email protected]"
10 }
11}

Card-on-file

Feature availability

This feature is currently available to businesses in Nigeria and South Africa. Reach out to [email protected] to request access to this feature.

Card-on-file is a feature that lets you charge cards authenticated on other payment gateways without requiring the customer to re-authenticate.

This is especially helpful for recurring payments or when you allow users to save their cards and don't want to disrupt the customer experience when charging them via Paystack.

To use the feature, pass the card_on_file parameter set to true when calling the charge endpoint, and exclude the cvv parameter from the card object.

1{
2 "email": "[email protected]",
3 "amount": "10000",
4 "currency": "ZAR",
5 "card": {
6 "number": "4084086779797075",
7 "expiry_month": "05",
8 "expiry_year": "29"
9 },
10 "card_on_file": true
11}

Transaction correlation with Network Transaction ID

Geographic Availability

Network Transaction ID is currently available only for Visa and Mastercard transactions processed in South Africa.

The network_transaction_id is an optional parameter available to South African merchants processing Visa and Mastercard transactions. It uniquely identifies a transaction on the payment network and can be used to link related card-on-file charges across systems.

When you charge a card, the Charge CardAPI response or webhook may include a network_transaction_id. If you receive an ID from any transaction in a sequence, you can use it for subsequent transactions. You should always use the oldest network_transaction_id for the sequence of transactions. If you don't receive an ID on the first transaction but you receive one in a subsequent transaction, you can use that ID for future transactions. If you don't receive an ID, you can proceed with your transactions normally.

For example, if the first charge returns an id of nti-123 and the next one returns nti-456, you should continue to use nti-123 for all related charges. If the first charge has no ID but the second one returns nti-456, then you would use nti-456 going forward.

Initial charge response example:

1{
2 "status": true,
3 "message": "Charge attempted",
4 "data": {
5 "id": 5154776737,
6 "domain": "test",
7 "status": "success",
8 "reference": "tftnjm3kcw9ul4r",
9 ...
10 "network_transaction_id": "213475815243834",
11 "transaction_date": "2025-07-17T11:42:12.000Z"
12 }
13}

Subsequent charge request with Network Transaction ID:

1{
2 "email": "[email protected]",
3 "currency": "ZAR",
4 "amount": "10000",
5 "card": {
6 "number": "4084086779797075",
7 "expiry_month": "05",
8 "expiry_year": "29"
9 },
10 "network_transaction_id": "213475815243834"
11}

Visa Account Funding Transactions (AFT)

Feature Availability

This feature is only available to certain merchant categories in Kenya and South Africa. Reach out to [email protected] for enablement before you begin.

A Visa Account Funding Transaction (AFT) is a card transaction used to fund a wallet, account balance, or similar store of value, rather than to pay for goods or services. Card networks require these transactions to be identified separately from normal purchases.

To identify a charge as a Visa AFT, pass the account_funding object on either the Initialize TransactionAPI or Charge CardAPI endpoint, alongside your usual transaction parameters. All fields in the object are required.

ParameterTypeRequiredDescription
typeStringTrueThe type of account funding transaction. Currently, only top-up is supported.
receiverObjectTrueDetails of the account or wallet being funded.
senderObjectTrueDetails of the cardholder funding the transaction.

The Receiver Object

ParameterTypeRequiredDescription
first_nameStringTrueReceiver’s first name.
last_nameStringTrueReceiver’s last name.
account_identifier_typeStringTrueCode identifying the type of the receiver’s account referenced by account_identifier.
account_identifierStringTrueThe receiver’s account number or wallet identifier.
addressStringTrueReceiver’s street address.
cityStringTrueReceiver’s city.
country_codeStringTrueReceiver’s country, in ISO 3166-1 alpha-3 format (for example, NGA).
postal_codeStringTrueReceiver’s postal code.

The Sender Object

ParameterTypeRequiredDescription
first_nameStringTrueSender’s (cardholder’s) first name.
last_nameStringTrueSender’s (cardholder’s) last name.
account_numberStringTrueThe card number the funds are being sent from.
addressStringTrueSender’s street address.
cityStringTrueSender’s city.
country_codeStringTrueSender’s country, in ISO 3166-1 alpha-3 format (for example, NGA).
postal_codeStringTrueSender’s postal code.
mobile_phone_numberStringTrueSender’s mobile phone number.
reference_numberStringTrueA reference identifying the sender’s side of the transaction.
1{
2 "email": "[email protected]",
3 "amount": "10000",
4 "card": {
5 "cvv": "408",
6 "number": "4084084084084081",
7 "expiry_month": "01",
8 "expiry_year": "99"
9 },
10 "account_funding": {
11 "type": "top-up",
12 "receiver": {
13 "first_name": "Sam",
14 "last_name": "Smith",
15 "account_identifier_type": "01",
16 "account_identifier": "0123456789",
17 "address": "10 Broad Street",
18 "city": "Cape Town",
19 "country_code": "ZA",
20 "postal_code": "100001"
21 },
22 "sender": {
23 "first_name": "Jane",
24 "last_name": "Smith",
25 "account_number": "4111111111111111",
26 "address": "11 street, Parklands",
27 "city": "Nairobi",
28 "country_code": "KEN",
29 "postal_code": "100271",
30 "mobile_phone_number": "2349085662909",
31 "reference_number": "REF123"
32 }
33 }
34}

The account_funding object is accepted the same way on the Initialize TransactionAPI endpoint, alongside the standard email and amount parameters.