Flutterwave
Flutterwave (API v4) encaisse des paiements par carte, mobile money, USSD, OPay, Pay with Bank et virement vers des comptes virtuels, et effectue des paiements vers des comptes bancaires, des portefeuilles mobile money et des portefeuilles Flutterwave ; les pays sont ceux que ses endpoints bancaires et de réseaux mobiles listent en Afrique.
À utiliser avec des agents IA
Après avoir installé le plugin ou ajouté le serveur MCP, donnez cette consigne à votre agent :
Capacités
| Capacité | Type | StatutiNon vérifiée — générée à partir de la doc publique, pas encore relue.Disponible — écrite à partir de la doc du fournisseur, pas encore testée : peut contenir des erreurs.Vérifiée — a correspondu une fois à l’API réelle (test en direct, app qui fonctionne ou rapport accepté), en sandbox ou en production. | Méthode | Exemple |
|---|---|---|---|---|
| create_charge | asynchrone | Disponible | POST | Exemple |
| create_customer | synchrone | Disponible | POST | Exemple |
| create_orchestrator_charge | asynchrone | Disponible | POST | Exemple |
| create_orchestrator_transfer | asynchrone | Disponible | POST | Exemple |
| create_payment_method | synchrone | Disponible | POST | Exemple |
| create_refund | asynchrone | Disponible | POST | Exemple |
| create_transfer | asynchrone | Disponible | POST | Exemple |
| create_transfer_rate | synchrone | Disponible | POST | Exemple |
| create_transfer_recipient | synchrone | Disponible | POST | Exemple |
| create_transfer_sender | synchrone | Disponible | POST | Exemple |
| create_virtual_account | synchrone | Disponible | POST | Exemple |
| get_charge | synchrone | Disponible | GET | Exemple |
| get_customer | synchrone | Disponible | GET | Exemple |
| get_refund | synchrone | Disponible | GET | Exemple |
| get_transfer | synchrone | Disponible | GET | Exemple |
| get_virtual_account | synchrone | Disponible | GET | Exemple |
| get_wallet_balance | synchrone | Disponible | GET | Exemple |
| list_bank_branches | synchrone | Disponible | GET | Exemple |
| list_banks | synchrone | Disponible | GET | Exemple |
| list_chargebacks | synchrone | Disponible | GET | Exemple |
| list_charges | synchrone | Disponible | GET | Exemple |
| list_mobile_networks | synchrone | Disponible | GET | Exemple |
| list_settlements | synchrone | Disponible | GET | Exemple |
| list_transfer_recipients | synchrone | Disponible | GET | Exemple |
| list_transfers | synchrone | Disponible | GET | Exemple |
| lookup_bank_account | synchrone | Disponible | POST | Exemple |
| resend_webhook | asynchrone | Disponible | POST | Exemple |
| retry_transfer | asynchrone | Disponible | POST | Exemple |
| search_customers | synchrone | Disponible | POST | Exemple |
| update_charge | synchrone | Disponible | PUT | Exemple |
| update_chargeback | synchrone | Disponible | PUT | Exemple |
| update_transfer | synchrone | Disponible | PUT | Exemple |
| webhook_event | webhook | Disponible | POST | Exemple |
Gotchas
Les gotchas sont affichés tels qu’ils figurent dans les specs, dans leur langue d’origine.
create_charge
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠Body: amount (decimal, ≥ 0.01, major units), currency, reference (6-42 chars, ^[a-zA-Z0-9-]+$, unique), customer_id, payment_method_id; optional redirect_url (needed for redirect flows), recurring, authorization, meta, order_id. A 201 only means the charge was created: decide on data.status and data.next_action, never on the top-level status (the API reference says success / failed; the card guide's sample shows "status": "pending").
- ⚠data.next_action drives the payment. Docs (General Flow): "For these types of authorization, [update the transaction](/v4.0/reference/charges_put) with the authorization object to complete them." (requires_pin, requires_otp, requires_additional_fields → update_charge). redirect_url → send the customer to next_action.redirect_url.url (an object, not a string; "response.data.next_action.redirect_url.url"). payment_instruction → show next_action.payment_instruction.note (mobile money push, USSD). requires_bank_transfer → show account_number / account_bank_name / account_expiration_datetime. qr_code (mobile_money payment method created with use_qr: true) → next_action.qr_code.image; the API reference's example value is a data:image/png;base64 URI. requires_capture belongs to card pre-authorisation orders, which afrotools does not cover. Treat an unknown type as pending and call get_charge. After each update_charge, read next_action again: "You’ll first need to update the charge with the encrypted card PIN." "Followed by updating the charge request with the OTP."
- ⚠requires_requery = upstream timeout, not a failure. Docs (Errors): "we return a `requires_requery` with an `HTTP 201 Created` status. This indicates that the charge is being processed and should not be retried." "wait 20 seconds before calling the retrieve [charge](/v4/reference/charges_get)/[order](/v4/reference/orders_get) endpoints"; "If the retrieve request still returns a `requires_requery` response, retry the request in 40 seconds. Transactions that remain unresolved after a minute will transition to a failed status." If you must re-send the POST, reuse the same X-Idempotency-Key.
- ⚠Method/currency limits: "USSD payments are only available for NGN (Nigerian Naira) collections." OPay: "This payment method is only available for **NGN** (Nigerian Naira) transactions." Pay with Bank (bank_account): "This payment method is only available for NGN (Nigerian Naira) collections." Mobile money: "The charge `currency` must match the `currency_code` used when creating the payment method." The error table gives 1125400 CURRENCY_NOT_SUPPORTED for a mismatch; the mobile money guide's sample returns 10400 REQUEST_NOT_VALID ("Currency not supported for GH Mobile Money.").
- ⚠Fulfil only after verification: Docs (General Flow): "verify the transaction's `reference`, `amount` and `status` before giving value to the customer." The success value of data.status is 'succeeded' (enum: succeeded, pending, failed, voided); also check currency. Pay-with-bank-transfer charges carry a reference generated by Flutterwave, not the virtual account's reference (the guide creates the account with one reference and the incoming charge shows another): match those on the virtual account (list_charges?virtual_account_id=…) or customer_id, plus amount and currency.
- ⚠Sandbox card scenarios go in X-Scenario-Key as "`scenario:<value>&issuer:<value>`" (auth_pin, auth_pin_3ds, auth_3ds, auth_avs; issuer e.g. approved, insufficient_funds). "If you omit the `X-Scenario-Key` header, all mock card transactions defaults to a `noauth` flow." Mobile money redirect flow: "To simulate the redirect flow, pass `scenario:auth_redirect` in the header."
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1100409 CHARGE_ALREADY_EXISTS (reused reference), 1101422 CUSTOMER_NOT_FOUND_FOR_CHARGE, 1102422 PAYMENT_METHOD_NOT_FOUND_FOR_CHARGE (the payment method must belong to this customer), 1125400 CURRENCY_NOT_SUPPORTED, 1130400 AMOUNT_BELOW_MINIMUM, 1137400 CLIENT_ENCRYPTION_ERROR, 1140400 CHARGE_NOT_SUCCESSFUL, 1141400 REDIRECT_URL_INVALID, 1142400 USSD_BANK_CODE_INVALID, 1150400 INVALID_CHARGE (bank transfer: use /virtual-accounts), 1105500 CHARGE_FAILED, 1119500 FAILED_TO_INITIATE_CHARGE, 1107500 CHARGE_CREATION_FAILED (the table says to retry: reuse the same X-Idempotency-Key). Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
create_customer
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠Docs (General Flow): "Only the `email` is required, but we recommend collecting as much information as possible." name, phone and address are optional, but when you send phone it needs country_code (1-3 digits) and number (7-10 digits, no country code), and address needs city, country (ISO alpha-2), line1, postal_code, state.
- ⚠Store data.id (cus_…): create_payment_method and create_charge take it as customer_id. The docs don't say whether email is unique; to avoid duplicates, call search_customers with the email first.
- ⚠Success is HTTP 201 in the API reference; the guides label their samples '200 OK'. Treat any 2xx as success.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: none specific to customers; 409 is documented without a cause. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
create_orchestrator_charge
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠Docs (Orchestrator Flow): "it is great for situations where you want to charge the customer quickly and would not be repeating the charge." For repeat customers use create_customer + create_payment_method + create_charge.
- ⚠Docs (Orchestrator Flow): "you still need to include the required fields for `customer` and `payment_method`." customer needs email; payment_method is the same union as create_payment_method.
- ⚠Method/currency limits: "USSD payments are only available for NGN (Nigerian Naira) collections." OPay: "This payment method is only available for **NGN** (Nigerian Naira) transactions." Pay with Bank (bank_account): "This payment method is only available for NGN (Nigerian Naira) collections." Mobile money: "The charge `currency` must match the `currency_code` used when creating the payment method." The error table gives 1125400 CURRENCY_NOT_SUPPORTED for a mismatch; the mobile money guide's sample returns 10400 REQUEST_NOT_VALID ("Currency not supported for GH Mobile Money.").
- ⚠data.next_action drives the payment. Docs (General Flow): "For these types of authorization, [update the transaction](/v4.0/reference/charges_put) with the authorization object to complete them." (requires_pin, requires_otp, requires_additional_fields → update_charge). redirect_url → send the customer to next_action.redirect_url.url (an object, not a string; "response.data.next_action.redirect_url.url"). payment_instruction → show next_action.payment_instruction.note (mobile money push, USSD). requires_bank_transfer → show account_number / account_bank_name / account_expiration_datetime. qr_code (mobile_money payment method created with use_qr: true) → next_action.qr_code.image; the API reference's example value is a data:image/png;base64 URI. requires_capture belongs to card pre-authorisation orders, which afrotools does not cover. Treat an unknown type as pending and call get_charge. After each update_charge, read next_action again: "You’ll first need to update the charge with the encrypted card PIN." "Followed by updating the charge request with the OTP."
- ⚠Card encryption: Docs (Encryption): "When making a card charge request, you **must encrypt** the card information before sending the request." "Retrieve your encryption key from the **API settings** section in your Flutterwave dashboard." The documented Node sample encrypts each field with AES-GCM ("name: "AES-GCM""): key = the base64-decoded encryption key, IV = the UTF-8 bytes of the nonce ("Nonce must be exactly 12 characters long"), output = base64 of the ciphertext. Send the same nonce in the request (card.nonce / pin.nonce). Bad encryption: "we'll return a `422` error with a failed encryption error message." The page's sample error has code 11100, the error table lists 1137400 (a 400): branch on error.type CLIENT_ENCRYPTION_ERROR. FLUTTERWAVE_ENCRYPTION_KEY is an afrotools env-var name; the docs name none. Official backend SDKs encrypt for you.
- ⚠requires_requery = upstream timeout, not a failure. Docs (Errors): "we return a `requires_requery` with an `HTTP 201 Created` status. This indicates that the charge is being processed and should not be retried." "wait 20 seconds before calling the retrieve [charge](/v4/reference/charges_get)/[order](/v4/reference/orders_get) endpoints"; "If the retrieve request still returns a `requires_requery` response, retry the request in 40 seconds. Transactions that remain unresolved after a minute will transition to a failed status." If you must re-send the POST, reuse the same X-Idempotency-Key.
- ⚠Fulfil only after verification: Docs (General Flow): "verify the transaction's `reference`, `amount` and `status` before giving value to the customer." The success value of data.status is 'succeeded' (enum: succeeded, pending, failed, voided); also check currency. Pay-with-bank-transfer charges carry a reference generated by Flutterwave, not the virtual account's reference (the guide creates the account with one reference and the incoming charge shows another): match those on the virtual account (list_charges?virtual_account_id=…) or customer_id, plus amount and currency.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1100409 CHARGE_ALREADY_EXISTS, 1125400 CURRENCY_NOT_SUPPORTED, 1130400 AMOUNT_BELOW_MINIMUM, 1137400 CLIENT_ENCRYPTION_ERROR, 1141400 REDIRECT_URL_INVALID, 1150400 INVALID_CHARGE (bank transfer: use /virtual-accounts). Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
create_orchestrator_transfer
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠Required: action (instant | deferred | scheduled), type and payment_instruction. type picks the branch: bank, mobile_money or wallet (wallet = another Flutterwave merchant, identified by its merchant ID). The API also has cash_pickup (EGP) and crypto (USDC/USDT/RLUSD) branches, left out of this spec. The guide's "Available methods are `bank`, `mobile_moeny`, and `wallet`." has a typo: the schema enum is mobile_money.
- ⚠payment_instruction is chosen by destination_currency (required): it sets the recipient (and sender) shape, e.g. bank NGN → recipient.bank{account_number, code}; bank GHS/RWF/SLL/UGX/XAF/XOF → name + bank{…, branch}; mobile money → recipient.mobile_money{network, msisdn, country for XAF/XOF}; EGP also needs payment_instruction.transfer_purpose. source_currency + amount{value, applies_to} are always required. Bank EGP / EUR / GBP and mobile money EGP also require payment_instruction.sender (KYC: EGP needs name, national_identification, date_of_birth, phone, email, address); some guide examples omit sender or transfer_purpose — follow the reference.
- ⚠Corridors in the guides but not in the API reference: mobile money MWK (network AIRTELMW) and bank AUD and INR. The schema rejects them; open question, check with Flutterwave before relying on them. Names: ASCII letters, 2-50 characters.
- ⚠A 2xx is not a completed payout. Docs (General Transfer Flow): "The `data.status` field in the response will always return `NEW` upon initiating a transfer." Final status (SUCCESSFUL / FAILED) comes from the transfer.disburse webhook, the per-transfer callback_url, or get_transfer. On FAILED read data.provider_response: type is a lowercase enum (e.g. 'insufficient_balance') and code a 4-digit string (e.g. '0307'); the payout error table spells the same reasons in uppercase (0307 INSUFFICIENT_BALANCE).
- ⚠Open question: amount.value is described as "This is a positive integer in the smallest currency unit", but every guide example reads as major units: the limit error says "Amount must be at least 100NGN." and the FX guide sends "value": 300 for "if a customer wants to send 300 USD". Test in sandbox before going live. applies_to = source_currency (debit exactly this) or destination_currency (recipient receives exactly this). Docs (Transfer Orchestrator): "Transfer limits are based on the `destination_currency`."
- ⚠Scheduling: disburse_option {date_time 'YYYY-MM-DD HH:MM:SS', timezone from the enum} is required when action is 'scheduled' (204400 REQUIRES_DISBURSE_OPTION, 205400 REQUIRES_TIMEZONE, 206400 REQUIRES_DATETIME, 231400 INVALID_DISBURSE_DATETIME); the payout error table: "For scheduled payouts, the `date_time `value must be more than 30minutes in the future." (207400). A deferred or scheduled transfer is released or cancelled with update_transfer.
- ⚠Prerequisites (transfer guides): whitelist your server IPs and fund the source balance — otherwise 220400 IP_WHITELISTING_REQUIRED / 221400 NON_WHITELISTED_IP, or provider_response {type 'insufficient_balance', code '0307'} (the sandbox's insufficient_balance scenario answers with a 400 instead). "Whitelist your IP addresses"
- ⚠Sandbox: Docs (Testing): "If you do not pass the `X-Scenario-Key` header, the transfer remains in a `pending` state and no webhook is sent for the test." Send X-Scenario-Key: scenario:successful (or reversed, failed, insufficient_balance, duplicate_reference…). Open question: the transfer guides say the opposite ("To test the successful transfers, send a request without adding any extra headers."); the testing page is the more specific one.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 201409 REFERENCE_ALREADY_EXISTS, 228400 TRANSFER_AMOUNT_LIMIT, 203400 UNSUPPORTED_BANK_INSTRUCTION, 209400 UNSUPPORTED_MOMO_INSTRUCTION, 202400 UNSUPPORTED_WALLET_INSTRUCTION, 230400 UNSUPPORTED_MOBILE_MONEY_COUNTRY, 230401 INVALID_MOBILE_NUMBER, 211400 INVALID_TRANSFER_TYPE, 220400 IP_WHITELISTING_REQUIRED, 221400 NON_WHITELISTED_IP. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
create_payment_method
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠The body is a union on type: card | mobile_money | ussd | opay | bank_account | applepay | googlepay. Send the matching sibling object (card {…}, mobile_money {country_code, network, phone_number}, ussd {account_bank}). opay and bank_account carry no fields.
- ⚠card requires only nonce + encrypted_card_number + encrypted_expiry_month + encrypted_expiry_year (encrypted_cvv optional). billing_address and cof are optional; Docs (Card Payments): "This feature is available only by request." (cof, Credential-on-File).
- ⚠Card encryption: Docs (Encryption): "When making a card charge request, you **must encrypt** the card information before sending the request." "Retrieve your encryption key from the **API settings** section in your Flutterwave dashboard." The documented Node sample encrypts each field with AES-GCM ("name: "AES-GCM""): key = the base64-decoded encryption key, IV = the UTF-8 bytes of the nonce ("Nonce must be exactly 12 characters long"), output = base64 of the ciphertext. Send the same nonce in the request (card.nonce / pin.nonce). Bad encryption: "we'll return a `422` error with a failed encryption error message." The page's sample error has code 11100, the error table lists 1137400 (a 400): branch on error.type CLIENT_ENCRYPTION_ERROR. FLUTTERWAVE_ENCRYPTION_KEY is an afrotools env-var name; the docs name none. Official backend SDKs encrypt for you.
- ⚠mobile_money: country_code is the phone dialling code (e.g. "233" for Ghana), network the operator (e.g. "MTN"), phone_number without the country code.
- ⚠Method/currency limits: "USSD payments are only available for NGN (Nigerian Naira) collections." OPay: "This payment method is only available for **NGN** (Nigerian Naira) transactions." Pay with Bank (bank_account): "This payment method is only available for NGN (Nigerian Naira) collections." Mobile money: "The charge `currency` must match the `currency_code` used when creating the payment method." The error table gives 1125400 CURRENCY_NOT_SUPPORTED for a mismatch; the mobile money guide's sample returns 10400 REQUEST_NOT_VALID ("Currency not supported for GH Mobile Money.").
- ⚠Store data.id (pmd_…) for create_charge. For recurring card charges, "set the `recurring` flag to `true` in your charge request" with the stored payment_method_id. "Some methods, like USSD and bank transfers, can be reused across customers."
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1104400 CARD_NOT_TOKENIZED, 1106400 PAYMENT_METHOD_TYPE_MISSING, 1108400 CARD_RETOKENIZATION_REQUIRED, 1118400 INVALID_CARD_NUMBER, 1135422 CARD_EXPIRY_YEAR_OUT_OF_RANGE, 1136422 CARD_EXPIRY_MONTH_OUT_OF_RANGE, 1142400 USSD_BANK_CODE_INVALID, 1126400 MOBILE_MONEY_COUNTRY_CODE_NOT_SUPPORTED, 1127400 MOBILE_MONEY_NETWORK_NOT_SUPPORTED, 1128500 PAYMENT_CARD_TOKENIZATION_FAILED, 1134422 CARD_EXPIRED, 1137400 CLIENT_ENCRYPTION_ERROR. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
create_refund
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠Body: charge_id, amount (decimal), reason (duplicate | fraudulent | requested_by_customer | expired_uncaptured_charge), meta. Only successful charges can be refunded (1403400 otherwise).
- ⚠Docs (Refunds): "To initiate a part or partial refund, ensure that the refund amount is smaller than the transaction amount." Several partial refunds are allowed on one charge; their total can't exceed the charge amount (the guide's wording is "equal to"). "The refund will be deducted from your available balance or the next settlement."
- ⚠Lifecycle: Docs (Refunds): "When a Refund is first initiated, its status is `new`." then pending, then succeeded or failed. The API's enum also has completed (the guide's own create example returns it), cancelled and requires_action. Treat succeeded / completed as refunded, failed / cancelled as not refunded, and requires_action as undocumented: escalate instead of polling forever. Treat any 2xx as created (the guide's sample is labelled 200 OK).
- ⚠Final status: refund.completed webhook or get_refund. Open question: the guide mentions a `callbackurl` body field that the API reference does not define.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1402400 REFUND_AMOUNT_INVALID (more than the charge), 1403400 REFUND_CREATION_FAILED (charge still pending), 1404400 REFUND_ALREADY_COMPLETED. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
create_transfer
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠Body: action (instant | deferred | scheduled), payment_instruction {source_currency, amount {value, applies_to}, recipient_id, sender_id?}, optional reference (unique), narration (≤ 180 chars), callback_url, meta, disburse_option. There is no destination_currency here: it comes from the recipient's type.
- ⚠A 2xx is not a completed payout. Docs (General Transfer Flow): "The `data.status` field in the response will always return `NEW` upon initiating a transfer." Final status (SUCCESSFUL / FAILED) comes from the transfer.disburse webhook, the per-transfer callback_url, or get_transfer. On FAILED read data.provider_response: type is a lowercase enum (e.g. 'insufficient_balance') and code a 4-digit string (e.g. '0307'); the payout error table spells the same reasons in uppercase (0307 INSUFFICIENT_BALANCE).
- ⚠Sender: Docs (General Transfer Flow): "The transfer sender entity is only required for transfers in `EGP`, `INR`, `EUR`, and `GBP`."
- ⚠Open question: amount.value is described as "This is a positive integer in the smallest currency unit", but every guide example reads as major units: the limit error says "Amount must be at least 100NGN." and the FX guide sends "value": 300 for "if a customer wants to send 300 USD". Test in sandbox before going live. applies_to = source_currency (debit exactly this) or destination_currency (recipient receives exactly this). Docs (Transfer Orchestrator): "Transfer limits are based on the `destination_currency`."
- ⚠Scheduling: disburse_option {date_time 'YYYY-MM-DD HH:MM:SS', timezone from the enum} is required when action is 'scheduled' (204400 REQUIRES_DISBURSE_OPTION, 205400 REQUIRES_TIMEZONE, 206400 REQUIRES_DATETIME, 231400 INVALID_DISBURSE_DATETIME); the payout error table: "For scheduled payouts, the `date_time `value must be more than 30minutes in the future." (207400). A deferred or scheduled transfer is released or cancelled with update_transfer.
- ⚠Prerequisites (transfer guides): whitelist your server IPs and fund the source balance — otherwise 220400 IP_WHITELISTING_REQUIRED / 221400 NON_WHITELISTED_IP, or provider_response {type 'insufficient_balance', code '0307'} (the sandbox's insufficient_balance scenario answers with a 400 instead). "Whitelist your IP addresses"
- ⚠Reusing a reference returns error.type REFERENCE_ALREADY_EXISTS / code 201409; the docs' example shows it with HTTP 400, not 409.
- ⚠Sandbox: Docs (Testing): "If you do not pass the `X-Scenario-Key` header, the transfer remains in a `pending` state and no webhook is sent for the test." Send X-Scenario-Key: scenario:successful (or reversed, failed, insufficient_balance, duplicate_reference…). Open question: the transfer guides say the opposite ("To test the successful transfers, send a request without adding any extra headers."); the testing page is the more specific one.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 201409 REFERENCE_ALREADY_EXISTS, 228400 TRANSFER_AMOUNT_LIMIT, 212400 MERCHANT_WALLET_NOT_FOUND, 213400 UNSUPPORTED_CURRENCY_PAIRS, 220400 IP_WHITELISTING_REQUIRED, 221400 NON_WHITELISTED_IP, 270400 RECIPIENT_NOT_FOUND, 280400 SENDER_NOT_FOUND, 204400-207400 / 231400 scheduling errors. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
create_transfer_rate
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠The quote does not lock the rate: Docs (Real-time FX conversion): "Fetching the live exchange rate is optional. Flutterwave automatically uses real-time rates when you start a transfer." create_transfer takes no rate id. The rate actually used is in the transfer's debit_information.
- ⚠Body: source {currency}, destination {currency, amount}, optional precision (2-9, default 6). The response gives the source amount needed for the destination amount.
- ⚠Limits apply to the converted amount: Docs (Transfer Orchestrator): "If the converted amount exceeds or falls below the allowed limits for the destination currency, the transfer will fail."
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 501422 RATE_UNAVAILABLE_ERROR, 213400 UNSUPPORTED_CURRENCY_PAIRS. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
create_transfer_recipient
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠type picks the corridor and its required fields (one schema branch per type): bank_ngn needs only bank{account_number, code}; bank_ghs / rwf / sll / ugx / xaf / xof need name + bank{account_number, code, branch}; bank_etb / kes / mwk need name + bank{account_number, code} (mwk: branch optional); bank_eur needs name, phone, email, address + bank{account_number, name, swift_code}; bank_gbp needs name, phone, email, address + bank{account_number, account_type, name, sort_code} (no swift_code); bank_usd needs name, phone, email, address + bank{account_number, code, account_type, routing_number, swift_code}; bank_zar and bank_egp need full KYC. mobile_money_* need name + mobile_money{network, msisdn}; mobile_money_egp also national_identification + address. Names: ASCII letters, 2-50 characters (^(?![ ,.'-]*$)[A-Za-z ,.'-]{2,50}$), so transliterate accented names.
- ⚠Mobile money: Docs (Mobile Money Transfers): "The `msisdn` field must contain the recipient’s mobile number **starting with the country code**." "For `XAF` and `XOF` transfers, include the country field explicitly in the request."
- ⚠Codes: bank.code comes from list_banks (the bank's code, not its id); mobile_money.network from list_mobile_networks; bank.branch from list_bank_branches. Docs (General Transfer Flow): "For some countries (e.g., Ghana), you may also need to call the [retrieve bank branches](/v4.0/reference/bank_branches_get) endpoint to get branch codes."
- ⚠This endpoint only creates bank_* and mobile_money_* recipients. Wallet payouts go through create_orchestrator_transfer; cash pickup and crypto exist in the API but are not covered by these specs.
- ⚠Store data.id (rcb_… for bank) for create_transfer. Creating the same beneficiary twice returns 270409 RECIPIENT_ALREADY_EXISTS: reuse the existing id (list_transfer_recipients).
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 270409 RECIPIENT_ALREADY_EXISTS, 252400 UNKNOWN_BANK_CODE, 251400 INVALID_ACCOUNT. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
create_transfer_sender
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠Docs (General Transfer Flow): "The transfer sender entity is only required for transfers in `EGP`, `INR`, `EUR`, and `GBP`." Pass data.id (sdr_…) as payment_instruction.sender_id.
- ⚠type: bank_egp and mobile_money_egp need name, phone, email, address, date_of_birth and national_identification (with expiration_date); bank_gbp and bank_eur need name, phone, email, address; generic_sender needs name. Open question: no sender type is named for INR (generic_sender is the likely one).
- ⚠phone.number is 7-10 digits without the country code; country_code 1-3 digits.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 280409 SENDER_ALREADY_EXISTS, 281400 UNKNOWN_SENDER_TYPE. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
create_virtual_account
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠Docs (Pay With Bank Transfer): "PWBT is currently available for transactions in **NGN** (Nigerian Naira) and **GHS** (Ghanaian Cedi) only." The API's currency enum is wider (NGN, GHS, EGP, KES, MAD, ZAR; customer_account_number is "required for EGP and KES").
- ⚠Required: customer_id, reference, currency, account_type. Static: "You must set `amount` to `0` when creating a static virtual account." "Static accounts are valid for `1000 years`".
- ⚠Dynamic: expiry is an integer number of seconds (60-31536000). "You must define an expiry when creating a dynamic account." but the same page gives "Default (if not specified): 1 hour (3600 seconds)". "Do **not** allow customers to save dynamic account details."
- ⚠Static NGN accounts: "For NGN virtual accounts, either of the following is required:" bvn or nin (11 digits); this sits under the static-account section and the guide's dynamic NGN example sends neither.
- ⚠Show the customer data.amount, not the amount you sent: the guide's dynamic example requests 1500 and gets back 1530; "Transfer the exact amount to the provided virtual account details." Sandbox: X-Scenario-Key issuer:approved or issuer:failed simulates the incoming transfer.
- ⚠Payment confirmation: Docs (Pay With Bank Transfer): "Flutterwave sends a `charge.completed` webhook to your configured webhook URL." Verify with get_charge (data.id from the webhook) and list payments with list_charges?virtual_account_id=….
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1900409 VIRTUAL_ACCOUNT_REFERENCE_ALREADY_EXISTS, 1900500 FAILED_TO_CREATE_VIRTUAL_ACCOUNT, 10503 VIRTUAL_ACCOUNT_CREATE_TIMEOUT (retry). Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
get_charge
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Fulfil only after verification: Docs (General Flow): "verify the transaction's `reference`, `amount` and `status` before giving value to the customer." The success value of data.status is 'succeeded' (enum: succeeded, pending, failed, voided); also check currency. Pay-with-bank-transfer charges carry a reference generated by Flutterwave, not the virtual account's reference (the guide creates the account with one reference and the incoming charge shows another): match those on the virtual account (list_charges?virtual_account_id=…) or customer_id, plus amount and currency. Webhooks are not enough on their own: Docs (Webhooks): "always re-query our API to verify the transaction details."
- ⚠requires_requery = upstream timeout, not a failure. Docs (Errors): "we return a `requires_requery` with an `HTTP 201 Created` status. This indicates that the charge is being processed and should not be retried." "wait 20 seconds before calling the retrieve [charge](/v4/reference/charges_get)/[order](/v4/reference/orders_get) endpoints"; "If the retrieve request still returns a `requires_requery` response, retry the request in 40 seconds. Transactions that remain unresolved after a minute will transition to a failed status." If you must re-send the POST, reuse the same X-Idempotency-Key.
- ⚠The payment method id is data.payment_method_details.id (there is no top-level payment_method_id). The failure reason of a failed charge is data.processor_response (type, code, message).
- ⚠Backup polling: Docs (Webhooks): "we recommend setting up a background job that polls for the status of any pending transactions at regular intervals (for instance, every hour)"
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1110404 CHARGE_NOT_FOUND. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
get_customer
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Docs (Card Payments): "To fetch an existing customer's details, use the [retrieve customer](/v4.0/reference/customers_get) endpoint with the customer's `id`. This is useful when initiating charges for returning users."
- ⚠The API reference lists only 200/400/401/403 for this endpoint; an unknown id falls under the general error table (10404 RESOURCE_NOT_FOUND). The exact HTTP status is not documented.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: none specific (the general table's 10404 RESOURCE_NOT_FOUND). Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
get_refund
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Status: new → pending → succeeded | failed per the guide; the enum adds completed (seen in the guide's create example), cancelled and requires_action. succeeded / completed = refunded; failed / cancelled = not; requires_action is undocumented.
- ⚠Status is not delivery time: Docs (Refunds): "Card refunds are completed between 3-15 days after the refund is initiated. Refunds to mobile money wallets take 3-5 days, while refunds to bank accounts are completed in 24 hours."
- ⚠Prefer the refund.completed webhook. It cannot be resent with resend_webhook, so keep this endpoint as the fallback.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1401404 REFUND_NOT_FOUND. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
get_transfer
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠data.status: NEW, PENDING, SUCCESSFUL, FAILED, CANCELLED, INITIATED (uppercase). Only SUCCESSFUL means the money left; FAILED carries data.provider_response {type, code, message}.
- ⚠data.reversal is set when a payout was reversed (reversal_datetime, initial_status, reconciliation_status).
- ⚠Prefer the transfer.disburse webhook; poll sparingly. Docs (General Transfer Flow): "This should be done periodically to avoid rate limiting."
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 301404 TRANSFER_NOT_FOUND. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
get_virtual_account
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Display data.account_number, data.account_bank_name and data.account_expiration_datetime. amount is 0 on static accounts; status is active or inactive.
- ⚠This does not tell you whether the customer paid: payments arrive as charges (charge.completed webhook, list_charges?virtual_account_id=…).
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1900404 VIRTUAL_ACCOUNT_NOT_FOUND. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
get_wallet_balance
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Returns {currency, available_balance} for the currency in the path (e.g. /wallets/balances/NGN). Payout guides require it: "Ensure your balance has sufficient funds." A transfer that exceeds it fails with provider_response 0307 INSUFFICIENT_BALANCE.
- ⚠The response for a currency you don't hold is not documented (the API reference lists only 400/401/403).
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 212400 MERCHANT_WALLET_NOT_FOUND is documented for transfers; nothing specific for this endpoint. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
list_bank_branches
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠The path id is the bank's id from list_banks (bnk_…), not its code.
- ⚠Branches are required for recipient types bank_ghs, bank_rwf, bank_sll, bank_ugx, bank_xaf and bank_xof. Docs (Bank Account Transfers): "Required only for certain currency transfers (e.g., GHS bank transfers)." Use data[].code as recipient.bank.branch: the guide asks for the "bank branch code" and its GHS example sends "branch": "001".
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: none specific. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
list_banks
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠country is required (enum of 20 ISO alpha-2 codes). Use data[].code as recipient.bank.code (e.g. "044" for Access Bank in the docs' examples); data[].id (bnk_…) is only for list_bank_branches.
- ⚠The same list feeds the USSD payment method: Docs (USSD): "Use the [get banks endpoint](/v4.0/reference/banks_get) with the query parameter `country=NG` to fetch Nigerian bank codes."
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: none specific. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
list_chargebacks
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Discovery: Docs (Chargebacks): "Flutterwave notifies you about chargebacks via your email." "We also send webhooks to your server when a chargeback is raised. This notification service is available on request." Without the webhook, poll this endpoint at least hourly (from = last poll): the guide's samples set due_datetime 24 hours after created_datetime.
- ⚠Respond before data.due_datetime: lost means "a lack of response from the merchant before the due date (48 hours)"; "If the merchant **does not respond**, the amount is debited automatically."
- ⚠Status: accepted / declined = you answered; won / lost / reversed = final; anything else (initiated, pending, and new, which the API enum has but the guide's table doesn't describe) = waiting for you. Stage pre-arbitration = last chance to send new evidence.
- ⚠Pagination: page ≥ 1 (default 1, most recent first), size 10-50 (default 10); read meta.page_info.total_pages. The API reference caps some list responses at 10 items (maxItems 10) although size goes up to 50: always paginate.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1700404 CHARGEBACK_NOT_FOUND. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
list_charges
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Pay with bank transfer reconciliation: Docs (Pay With Bank Transfer): "track all the payments made into a virtual account by [querying the charge list](/reference/charges_list) using the virtual account's ID." → GET /charges?virtual_account_id=van_…
- ⚠from / to are ISO 8601 date-times (format date-time, e.g. 2025-04-21T10:55:16Z). status filter: succeeded, pending, failed, voided.
- ⚠Pagination: page ≥ 1 (default 1, most recent first), size 10-50 (default 10); read meta.page_info.total_pages. The API reference caps some list responses at 10 items (maxItems 10) although size goes up to 50: always paginate. The API reference caps data at 10 items (maxItems 10) although size goes up to 50.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 701500 INVALID_PAGINATION_TYPE. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
list_mobile_networks
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠country is required (enum of 15 ISO alpha-2 codes). Docs (General Transfer Flow): "Use the Retrieve [mobile networks endpoint](/v4.0/reference/mobile_networks_get) to fetch available mobile money networks by country and pass the appropriate `network` value." Use data[].network, not data[].id.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: none specific. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
list_settlements
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Docs (Settlements): "Only transactions made in **live/production** mode are eligible for settlement." Expect no settlements in sandbox.
- ⚠A settlement moves collected funds to your bank account or "Your **Flutterwave for Business (F4B) wallet** (payout balance)." See data[].destination; amounts are gross_amount / net_amount with a fees[] breakdown; charge_count is a string.
- ⚠The guide's prose shows "`status`: The current status of the settlement (e.g., SUCCESSFUL, PENDING)." but the API returns the lowercase enum (e.g. 'completed').
- ⚠Pagination: page ≥ 1 (default 1, most recent first), size 10-50 (default 10); read meta.page_info.total_pages. The API reference caps some list responses at 10 items (maxItems 10) although size goes up to 50: always paginate. from / to are ISO 8601 date-times.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1500404 SETTLEMENT_NOT_FOUND. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
list_transfer_recipients
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Pagination is cursor-based: pass data.cursor.next / previous back as the next / previous query parameter; size 10-50 (default 10); data.cursor.has_more_items says when to stop. An invalid cursor returns 210400 INVALID_CURSOR.
- ⚠Each recipient is a union on type (bank, mobile_money, wallet, cash_pickup, crypto) with the matching sibling object. No filter by account number is documented: match locally.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 210400 INVALID_CURSOR. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
list_transfers
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this GET (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Pagination is cursor-based: pass data.cursor.next / previous back as the next / previous query parameter; size 10-50 (default 10); data.cursor.has_more_items says when to stop. An invalid cursor returns 210400 INVALID_CURSOR.
- ⚠Filters: status, action, type, recipient_id, sender_id, source_currency, destination_currency, bulk_id, from/to. The status filter takes the values listed in the enum; the transfer objects return status in uppercase.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 210400 INVALID_CURSOR. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
lookup_bank_account
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference doesn't list it for this endpoint; sending it is harmless and follows the guide. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠The body is a union on currency: NGN, GHS, UGX → account{code, number}; GBP → account{type individual | corporate, code, number, name}; USD → account{code, number, country: 'NG'} (USD accounts at Nigerian banks only). Other currencies can't be resolved here. Open question (GBP): the account-type branches are keyed individual / corporate, but the individual branch's own default is 'personal'.
- ⚠Docs (Introduction): "This step is important to ensure that your transfer is processed successfully to the correct recipient." Compare data.account_name with the name the user typed before creating the recipient.
- ⚠Sandbox example from the docs: account {code: "044", number: "0690000040"}. The sandbox transfer scenario account_verification_failed exists for transfers; no lookup-specific scenario is documented.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 250400 ACCOUNT_VERIFICATION_FAILED, 251400 INVALID_ACCOUNT, 252400 UNKNOWN_BANK_CODE. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
resend_webhook
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠Body: id = the entity id (chg_…, ord_…, trf_…) or a webhook message id (wbk_…); type = charge.completed, transfer.disburse, transfer.reversal or order.authorization. refund.completed is not in the enum, so refund webhooks cannot be resent this way: use get_refund.
- ⚠The 201 body is only {status, message}; the event itself arrives on your webhook URL, signed like any other.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1600400 WEBHOOK_URL_INVALID, 1601404 WEBHOOK_ENDPOINT_NOT_FOUND (no webhook configured), 1602422 UNPROCESSABLE_SVIX_RESPONSE, 1603500 INTERNAL_SERVER_ERROR_SVIX. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
retry_transfer
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference marks it optional; the headers guide lists it under 'Mandatory API Headers'. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠action is required: 'retry' re-sends a FAILED transfer; 'duplicate' sends a SUCCESSFUL transfer again, which pays the recipient a second time. Use duplicate only on purpose.
- ⚠A second retry while a previous one is pending or successful is rejected (405400 POSSIBLE_DUPLICATE_RETRIES). Scheduled or deferred transfers can't be retried or duplicated (403400).
- ⚠The response is a new transfer object (status NEW); track it like any transfer. Optional: a new reference, meta, callback_url.
- ⚠Prerequisites (transfer guides): whitelist your server IPs and fund the source balance — otherwise 220400 IP_WHITELISTING_REQUIRED / 221400 NON_WHITELISTED_IP, or provider_response {type 'insufficient_balance', code '0307'} (the sandbox's insufficient_balance scenario answers with a 400 instead). "Whitelist your IP addresses"
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 404400 REQUIRES_FAILED_STATUS (retry), 401400 REQUIRES_SUCCESSFUL_STATUS (duplicate), 403400 SCHEDULED_DEFERRED_NOT_ALLOWED, 405400 POSSIBLE_DUPLICATE_RETRIES. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
search_customers
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: send X-Idempotency-Key on this POST. Docs (Idempotency): "For all `POST` endpoints, include an idempotency key in the request header (`X-Idempotency-Key`)." The API reference doesn't list it for this endpoint; sending it is harmless and follows the guide. A reused key replays the first response ("the response will include the header `X-Idempotency-Cache-Hit: true`"), so reuse the key when retrying the same operation and use a new one for a new operation. X-Trace-Id is optional. For both: "It must be a string using only alphanumeric ASCII characters." "Its length should be between 12 and 255 characters." The idempotency page also says "We recommend using a **UUID** string as your idempotency key": strip the UUID's hyphens to satisfy both.
- ⚠The body takes a single required field, email (the API reference defines no phone or name filter). page and size are query parameters. The matching rule (exact or partial) is not documented.
- ⚠Use it to avoid duplicate customers: search by email, reuse data[0].id if present, otherwise create_customer.
- ⚠Pagination: page ≥ 1 (default 1, most recent first), size 10-50 (default 10); read meta.page_info.total_pages. The API reference caps some list responses at 10 items (maxItems 10) although size goes up to 50: always paginate.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: none specific. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
update_charge
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this PUT (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠authorization is an object discriminated on type: {type:'pin', pin:{nonce, encrypted_pin}}, {type:'otp', otp:{code}}, {type:'avs', avs:{address:{…}}}, or {type:'external_3ds', external_3ds:{…all eight fields required}}.
- ⚠PIN then OTP: "You’ll first need to update the charge with the encrypted card PIN." "Followed by updating the charge request with the OTP." Read data.next_action after every update.
- ⚠AVS: Docs (Card Payments): "If the charge response contains `data.next_action.requires_additional_fields`, it means that you are charging an AVS card." next_action.requires_additional_fields.fields lists dotted body paths (e.g. authorization.avs.address.postal_code).
- ⚠Card encryption: Docs (Encryption): "When making a card charge request, you **must encrypt** the card information before sending the request." "Retrieve your encryption key from the **API settings** section in your Flutterwave dashboard." The documented Node sample encrypts each field with AES-GCM ("name: "AES-GCM""): key = the base64-decoded encryption key, IV = the UTF-8 bytes of the nonce ("Nonce must be exactly 12 characters long"), output = base64 of the ciphertext. Send the same nonce in the request (card.nonce / pin.nonce). Bad encryption: "we'll return a `422` error with a failed encryption error message." The page's sample error has code 11100, the error table lists 1137400 (a 400): branch on error.type CLIENT_ENCRYPTION_ERROR. FLUTTERWAVE_ENCRYPTION_KEY is an afrotools env-var name; the docs name none. Official backend SDKs encrypt for you.
- ⚠The API reference lists 200/400/401/403 for this PUT (no 409): a finalised charge fails with 1111400 CHARGE_FINALIZED.
- ⚠The card guide's PIN, OTP and AVS update requests all send X-Idempotency-Key although the API reference doesn't list it here: sending one is harmless; use a new key per update (PIN and OTP are two operations).
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1110404 CHARGE_NOT_FOUND, 1111400 CHARGE_FINALIZED (already succeeded or failed), 1112400 CHARGE_UPDATE_NOT_ALLOWED, 1134422 CARD_EXPIRED, 1113500 CHARGE_UPDATE_FAILED, 1121400 INVALID_AUTHORIZATION (wrong PIN or OTP), 1137400 CLIENT_ENCRYPTION_ERROR. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
update_chargeback
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this PUT (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠status you can set: accepted, declined, partially-accepted. won / lost / reversed are set by Flutterwave.
- ⚠Decline: Docs (Chargebacks): "with the `status` set to `declined` and the link to the evidence passed as `uploaded_proof`." The error table adds that declined chargebacks must have a comment and proof (1703400). proof_data (base64) is the documented alternative to the uploaded_proof link.
- ⚠Open question: partially-accepted and the amount field are in the API reference only; the guide doesn't describe them, and the response's status enum doesn't list partially-accepted.
- ⚠Answer before data.due_datetime (48 hours): after it, 1706400 CHARGEBACK_DUE_DATE_INVALID.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 1700404 CHARGEBACK_NOT_FOUND, 1703400 CHARGEBACK_DECLINE_INVALID, 1704400 CHARGEBACK_PROOF_INVALID, 1705500 CHARGEBACK_PROOF_UPLOAD_FAILED, 1706400 CHARGEBACK_DUE_DATE_INVALID (deadline passed), 1707400 CHARGEBACK_AMOUNT_INVALID. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
update_transfer
- ⚠Auth: one token server for both environments — the docs' Multi-Environment Integrations example sends either environment's credentials to https://idp.flutterwave.com/realms/flutterwave/protocol/openid-connect/token, and Authentication says "This process applies to both the sandbox and production environments. Make sure to use the correct credentials for each environment to obtain a valid token." "Each token is valid for **10 minutes**"; "We recommend refreshing the token at least one minute before it expires." Cache the token. Sandbox: "We archive your test data after 30 days."
- ⚠Headers: X-Trace-Id is optional on this PUT (the headers guide lists it under 'Optional API Headers'; "It must be a string using only alphanumeric ASCII characters.", 12-255 characters). The API reference documents no X-Idempotency-Key for this endpoint.
- ⚠Body (API reference): initiate: true releases a deferred or scheduled transfer, close: true cancels it, disburse_option reschedules it. Only works while the transfer is NEW and deferred or scheduled.
- ⚠Open question: the guides describe a different body: Docs (General Transfer Flow): "setting `action` to `instant` to process the transfer or set it to `close` to cancel it." The API reference schema has no action field; follow the reference and verify in sandbox.
- ⚠The API reference lists 200/400/401/403 (no 409): wrong-state errors are the 30x400 codes above.
- ⚠Errors: {"status":"failed","error":{"type","code","message","validation_errors"}}; error.code is a string and 400s carry validation_errors[{field_name,message}]. Besides the generic codes (10400 REQUEST_NOT_VALID, 10401, 10403 FORBIDDEN, 10404 RESOURCE_NOT_FOUND, 10409 RESOURCE_CONFLICT, 10422 UNPROCESSABLE, 10500) the error table documents these for this endpoint: 302400 INVALID_TRANSFER_STATUS (not in NEW status), 303400 TRANSFER_NOT_DEFERRED_SCHEDULED, 304400 TIME_LIMIT_EXCEEDED, 208400 INVALID_DISBURSE_ACTION. Branch on error.type / error.code, not on the HTTP status alone. Docs (Errors): "The current limit is 500 API requests per minute." A 429 can come back as a non-JSON page ("HTML error pages such as `Cloudflare 1015 responses`"): parse error bodies defensively and back off exponentially with jitter.
webhook_event
- ⚠Signature: Docs (Webhooks): "Flutterwave uses the secret hash you specify on your dashboard to hash the webhook response using `HMAC-SHA256`." The value arrives in the flutterwave-signature header; the documented sample computes createHmac('sha256', secretHash).update(rawBody).digest('base64'). Use the raw body and a constant-time compare. The secret hash is the one you set in Dashboard → Settings → Webhooks (env FLUTTERWAVE_WEBHOOK_SECRET); it is not an API key.
- ⚠Respond fast: "your endpoint **must** return a `200` HTTP status code"; "Webhook requests time out after **60 seconds**." Retries: "Be sure to enable webhook retries on your dashboard." — "we'll retry the webhook call **3 times**, with a **30-minute** interval between each attempt."
- ⚠"Occasionally, we might send the same webhook event more than once." Deduplicate on webhook_id (the API reference requires it on every event; the webhooks page sample shows `id` instead, so read webhook_id ?? id), and record an event as processed only after the work succeeded.
- ⚠Status casing differs by event: charge.completed / refund.completed use lowercase (succeeded, failed…), transfer.disburse / transfer.reversal use uppercase SUCCESSFUL / FAILED. The customer id is data.customer.id (no data.customer_id). timestamp is Unix epoch milliseconds in every sample. data.created_datetime is an ISO string in the API reference but an epoch float in the webhooks page sample: accept both.
- ⚠Never fulfil on the webhook alone: Docs (Webhooks): "always re-query our API to verify the transaction details." (get_charge, get_transfer, get_refund).
- ⚠order.authorization (card pre-authorisation orders) also exists but is out of scope here. Missed a delivery? resend_webhook (POST /resend-webhook {id, type}). "Web frameworks like Rails or Django check POST requests for `CSRF` tokens": exempt this route.
Détails
- Catégorie
- paiement
- Sandbox
- Oui