Skip to main content
The data string could not be decrypted. In order of likelihood:
  • Wrong padding. It must be PKCS#1 v1.5, not OAEP. Browser SubtleCrypto only does OAEP, so a hand-rolled WebCrypto implementation will always fail here.
  • Wrong environment. A test encryption key with a live API key (or vice versa).
  • Not base64. The ciphertext bytes must be base64-encoded before being placed in data.
  • Key not parsed correctly. Remember to base64-decode the key, split on !, and use the second half. See Encryption.
  • Payload too large. RSA encrypts a limited number of bytes per block. Trim optional fields if a payload has grown unusually large.
The api-key header is missing entirely. Check for a typo in the header name — it is api-key, not apikey, API-Key or Authorization.
You sent the wrong kind of key. Order creation, payment, status, fees, banks and events take the public key; verify, refunds, chargebacks and tokenized charges take the secret key. See Authentication for the full split.This error also appears when a key from one environment is used against the other.
The reference in your payload doesn’t match an order. Usually one of:
  • The order was never successfully created — check the create-order response.
  • A typo, or a reference from a different environment.
  • You generated a fresh reference for the pay call instead of reusing the one from create.
A payment has already been submitted for this order and hasn’t resolved yet. Don’t retry the pay call — poll Get order status instead. Creating a second payment for the same order risks double-charging the customer.
Expected for the first few minutes on bank transfers — the customer has to actually send the money. If a card payment stays pending beyond a few minutes, the customer most likely abandoned 3-D Secure. Leave the order pending, tell the customer you’ll confirm once payment completes, and reconcile from your backend. Don’t mark it failed: a payment can still land after the customer has closed their browser.
A "status": "success" on Pay an order means the payment was accepted for processing, not that it completed. Only a final status from Get order status, confirmed with Verify an order, tells you the money moved.
There are no webhooks on this API, so a customer who closes their browser mid-payment leaves you with no notification at all — even though the payment may still complete. Run a backend job that polls pending orders. See Callbacks and verification.
otherPaymentOptions depends on the order’s currency and on what’s enabled for your account. A USD order typically offers card only, where an NGN order also offers bank transfer. Don’t hard-code the list — render whatever create-order returns.
Almost always the encryption helper. POST /charge/data/encrypt does not exist in production — if your integration calls it, implement encryption yourself. The other usual cause is only swapping two of the three credentials: the public key, the secret key and the encryption key all change between environments.

Still stuck?

Have your order reference and the statusCode from the failing response to hand — together they identify the exact transaction.