{
"status": "failed",
"error": "invalid_credentials",
"message": "The provided credentials are invalid."
}
error code. It is stable and safe to match on. The message is written for humans and may change wording, so show it to people but never parse it. Log the X-Request-Id response header with every failure and quote it when you contact tech@vestrapay.com.
Validation failures on request bodies, such as a missing field or a malformed value, come back as HTTP 400 with a message that names the field. Fix the request and resend it. They are not listed below because the message is the guidance.
Reading the HTTP status
| HTTP status | Meaning | Retry? |
|---|---|---|
| 400 | The request is invalid or the current state does not allow it. | No. Fix it first. |
| 401 | Credentials are missing, malformed or wrong. | No. |
| 403 | Authenticated, but not allowed: KYB not approved, live not enabled, missing dashboard permission, or partner access off. | No. Resolve the cause. |
| 404 | The thing does not exist in this environment or for your account. | No. |
| 409 | A conflict with existing state, such as a duplicate or a protected record. | No. |
| 410 | The session, token or link has expired or is no longer active. | No. Start again. |
| 422 | Too many attempts on this session. | No. Initialise a new payment. |
| 502, 503 | A bank or processor dependency is unavailable. | Yes, with exponential backoff. |
Authentication and session
| Error | Typical HTTP | What to do |
|---|---|---|
invalid_credentials | 401 | The key or token is wrong or missing. Check the header name and the value, and that you copied the whole key. |
missing_credentials | 401 | The request carried no credential at all. Send x-api-key, an access code or a session token as the endpoint requires. |
invalid_key_format | 401 | The key is malformed. Secret keys begin sk_test_ or sk_live_. Check for stray whitespace or truncation. |
key_not_found, invalid_key | 401 | The key does not exist or has been revoked or rotated out. Copy the current key from Settings → Developers. |
invalid_key_environment | 401 | A sandbox key was sent to the live host, or a live key to the sandbox. Match the key and the base URL. |
no_active_key_found | 401 | There is no active key for this environment. Create or rotate one in the dashboard. |
secret_key_required | 401 | The route needs a secret key and you sent something else, such as a public key. |
public_key_required | 401 | The route needs a public key. |
key_context_jwt_rejected | 401 | A merchant session token was sent to a route that only accepts API keys. Use your secret key. |
missing_access_code | 401 | A checkout call had no x-access-code. Send the access code from initialise. |
session.expired | 410 | The access code is past its 60 minutes. Initialise a new payment and restart checkout. |
transaction_expired | 401 | The access code has expired. Same remedy as session.expired. |
invalid_session, session_expired | 401 | Your dashboard session is no longer valid. Sign in again. |
merchant.kyb_not_approved | 403 | A live payment was attempted before KYB approval. Stay on the sandbox, or finish KYB. |
kyb_not_approved | 403 | A dashboard action needs approved KYB. See Go live. |
live_mode_not_enabled, live_keys_not_enabled | 403 | Live mode has not been activated for your account yet. Contact tech@vestrapay.com if KYB is already approved. |
insufficient_module_permission | 403 | The signed-in team member lacks access to this dashboard area. Ask an owner to adjust their role. |
Payments
| Error | Typical HTTP | What to do |
|---|---|---|
payment.invalid_amount | 400 | The amount must be a numeric string such as “100.00”. Do not send kobo or a formatted value like “₦1,000”. |
payment.unsupported_currency | 400 | Only NGN is supported. |
payment.customer_email_required | 400 | The channel needs a customer email and none was provided on initialise. |
INVALID_SPLIT_AMOUNT | 400 | The distribution map is invalid: a value was not positive, the total exceeds the amount, a service key was not found or is disabled, or the service has no split participants. The message says which. See Distribution. |
customer.blocked | 403 | The payment cannot be completed. The customer is not told why. Contact the customer through your own channel. |
transaction.not_found | 404 | The reference is wrong, or it belongs to the other environment. |
transaction.already_processed | 400 | The payment has already reached a final state. Verify it instead of charging again. |
transaction.reference_already_consumed | 409 | This reference already completed a payment. Initialise a new one. |
transaction.reference_parameter_mismatch | 409 | A transaction with this reference exists with different parameters. Use a fresh initialise. |
payment.max_attempts_exceeded | 422 | Too many charge attempts on this payment. Initialise a new payment. |
processor.unavailable | 503 | The processor is temporarily unavailable. Retry with backoff. |
collection.not_found | 404 | No collection with that identifier exists for your account. |
Card
| Error | What to do |
|---|---|
card.declined | Show the message to the customer and let them try another card. Do not retry the same card automatically. |
card.processor_error | A temporary processing fault. Offer a retry. If it persists, contact support with the request ID. |
card.session_missing | The hosted card session could not be established. Create a new card session, then charge again. |
card.retry_session_required | Processor sessions are single-use. After a failed charge, create a new card session before retrying. |
card.invalid_3ds_callback | The 3-D Secure callback is invalid or has expired. Restart the card flow with a new session. |
payment.invalid_card_payload | The card payload was malformed. Use the hosted fields flow described in Pay with card. |
Transfer, bank and USSD
| Error | What to do |
|---|---|
bank_transfer.failed | The virtual account could not be generated. Offer a retry. |
bank_transfer.virtual_account_unavailable | No unique account could be allocated just now (HTTP 503). Retry in a moment. |
bank_payment.bank_required | Send bankCode from the banks list. |
bank_payment.failed | The bank payment could not be started. Offer a retry or another channel. |
ussd.phone_required | Send phoneNumber. |
ussd.failed | The USSD request failed. Check the number and retry. |
Payment links
| Error | Typical HTTP | What to do |
|---|---|---|
payment_link.not_found | 404 | The slug or link ID is wrong, or the link belongs to the other environment. |
payment_link.inactive | 410 | The link has been paused, has expired or has reached its limit. Create or reactivate a link. |
payment_link.answers_required | 400 | The pay request had no answers to the link’s prompts. |
payment_link.missing_required_answer | 400 | A required prompt was left empty. |
payment_link.unknown_answer_id | 400 | An answer key does not match a prompt on the link. Use the field IDs from the display response. |
payment_link.email_required, payment_link.invalid_email | 400 | The link needs a valid customer email. |
payment_link.amount_required | 400 | A variable-amount link needs the customer’s amount. |
payment_link.fixed_amount_required | 400 | The link has no fixed amount and cannot be paid as a fixed link. |
payment_link.invalid_metadata, payment_link.invalid_metadata_prompt | 400 | The prompt definitions on create or update are invalid. |
payment_link.amount_required_for_currency_change | 400 | Changing a link’s currency requires a new amount. |
Services, sub-accounts and splits
| Error | Typical HTTP | What to do |
|---|---|---|
merchant_service_not_found | 404 | The service ID is wrong, or it belongs to another account or environment. |
merchant_service_name_already_exists | 409 | A service with this name exists. Choose a different name. |
merchant_service_default_not_deletable | 409 | The default service cannot be deleted. |
merchant_service_default_not_disableable | 409 | The default service cannot be disabled. |
split_group_not_found | 404 | No splits are configured for this service yet. Configure them first. |
split_group_invalid_entries | 400 | The split configuration breaks a rule. The message names it: shares not totalling 100, duplicate split names, the primary account used as a participant, conflicting manual settings, or a missing destination account number. See Splits. |
tsa_account_not_enabled | 400 | A Treasury Single Account destination was used but is not enabled. Contact tech@vestrapay.com. |
subaccount_not_found | 404 | The sub-account does not exist for your account, or is inactive when a split needs an active one. |
subaccount_already_exists | 409 | That bank account is already in your pool. |
subaccount_bank_details_required | 400 | Send an enquiryToken, or both bankCode and accountNumber. |
subaccount_name_enquiry_invalid | 400 | The enquiry token is unknown or already used. Run the enquiry again. |
subaccount_name_enquiry_expired | 410 | The enquiry token has expired. Run the enquiry again. |
settlement_account_required | 409 | Complete the KYB settlement stage before creating sub-accounts. |
subaccount_has_transactions | 409 | The account has transactions and cannot be deleted. Remove it from your splits, or deactivate it. |
subaccount_primary_not_deletable | 409 | The primary settlement account cannot be deleted. Change it through the settlement account process. |
nip_resolution_failed | 502 | The bank lookup failed. Retry. |
Partners
| Error | Typical HTTP | What to do |
|---|---|---|
partner_access_not_enabled | 403 | Partner access is not switched on for your account. Contact tech@vestrapay.com. |
partners_not_available | 403 | Partners cannot be managed while the merchant account is suspended or rejected. |
partner_already_exists | 409 | That partner is already attached to your account. Edit their permissions instead. |
email_already_registered | 409 | The email already belongs to a merchant or team account. Partners need their own address. |
partner_user_deactivated | 400 | The partner’s account has been deactivated. |
partner_service_not_found | 400 | A service in the request does not exist or is disabled. |
partner_service_has_no_splits | 400 | A service in the request has no splits configured. Configure splits first. |
partner_split_required | 400 | Each service entry needs at least one splitId. |
partner_split_not_in_service | 400 | A splitId does not belong to that service. Fetch current IDs from split options. |
partner_not_found | 404 | No such partner on your account. |
partner_invite_not_pending | 400 | The partner has already accepted the invitation. |
KYB and settlement account
| Error | Typical HTTP | What to do |
|---|---|---|
business_category_not_found | 404 | Use a code from the business categories list. |
unknown_kyb_identity_key | 409 | An identity field is not in the current requirements. Fetch the requirements again. |
missing_required_kyb_field, missing_required_kyb_document | 400 | A required field or document is missing. The message names it. |
invalid_kyb_field_value | 400 | A field value failed validation. |
invalid_kyb_document_reference | 400 | A document ID in the identity section does not match an uploaded document. |
kyb_settlement_name_enquiry_invalid, kyb_settlement_name_enquiry_expired | 400 | The settlement enquiry token is unknown, used or expired. Run the name enquiry again. |
kyb_under_review, kyb_already_approved | 409 | KYB cannot be changed in its current state. |
settlement_account_unchanged | 400 | The new account is the same as the current one. |
settlement_account_change_pending | 409 | A change request is already awaiting approval. Cancel it to submit another. |
settlement_account_change_request_not_found | 404 | No such change request. |
settlement_account_change_request_not_pending | 409 | Only pending requests can be cancelled. |
Webhook endpoints
Endpoint management errors come back as HTTP 400 or 409 with a message. The common causes are a URL that is not HTTPS, a URL that resolves to a private or loopback address, an unreachable hostname, and exceeding the limit of two endpoints per environment.webhook_endpoint_not_found (404) means the endpoint ID is wrong for this environment.
What to log
For every failure, log the HTTP status, theerror code, the X-Request-Id header, and the payment reference when you have one. With those four values support can usually answer in a single reply.