Errors
Understand Seev API error codes, HTTP status codes, and how to handle them gracefully.
The Seev API uses conventional HTTP status codes and returns a consistent JSON error envelope on every failure. This page explains the error format, lists all error codes, and covers best practices for handling errors gracefully in your integration.
Start here when a request fails
- Record the HTTP status, error code, message, and request reference in your server logs.
- Check the environment and API key product before changing request data.
- Fix
4xxrequests before retrying them. - Retry only temporary network, rate-limit, and
5xxfailures. - Never show a secret key or full raw backend error to the customer.
| What you see | First check |
|---|---|
401 | Missing, mistyped, rotated, or wrong-environment API key. |
403 | Key product, organization eligibility, verification, or account state. |
400 / 422 | Required fields, types, amount, currency, and recipient values. |
404 | Resource ID, environment, organization, or an already removed resource. |
409 | Duplicate operation or reused idempotency key with changed data. |
429 | Slow down and respect the retry guidance returned by the API. |
5xx | Keep the customer in a safe pending state and retry with backoff. |
Error response format
All API errors return a JSON body with the same shape:
{
"status": "error",
"code": "INVALID_API_KEY",
"message": "The provided API key is invalid or has been revoked.",
"param": null
}| Field | Type | Description |
|---|---|---|
status | "error" | Always "error" for failure responses. |
code | string | Machine-readable error code. Use this for programmatic handling. |
message | string | Human-readable description. Suitable for logs — not for end-user display. |
param | string | null | The specific request field that caused the error, if applicable. |
HTTP status codes
| Status | Meaning |
|---|---|
200 | Success |
400 | Bad request — invalid or missing parameters |
401 | Unauthenticated — missing or invalid API key |
403 | Forbidden — the key is valid but lacks permission for this action |
404 | Not found — the resource does not exist |
409 | Conflict — duplicate request (check Idempotency) |
422 | Unprocessable — request is well-formed but semantically invalid |
429 | Rate limited — too many requests |
500 | Internal server error — something went wrong on Seev's end |
Error codes
Authentication
| Code | Status | Description |
|---|---|---|
INVALID_API_KEY | 401 | The API key is missing, malformed, or revoked. |
KEY_MISMATCH | 401 | The public key and secret key belong to different accounts. |
FORBIDDEN | 403 | The key does not have permission for the requested operation. |
Request validation
| Code | Status | Description |
|---|---|---|
MISSING_PARAM | 400 | A required parameter was not provided. param contains the field name. |
INVALID_PARAM | 400 | A parameter was provided but has an invalid value or type. |
INVALID_AMOUNT | 400 | The amount value is zero, negative, or non-numeric. |
INVALID_CURRENCY | 400 | The currency code is not supported. |
INVALID_PAYLOAD | 400 | The request body could not be parsed (e.g. malformed JSON or webhook body). |
Payments
| Code | Status | Description |
|---|---|---|
CHARGE_FAILED | 422 | The payment was declined by the processor. |
CHARGE_EXPIRED | 422 | The payment session timed out before the customer completed the flow. |
DUPLICATE_CHARGE | 409 | A charge with the same idempotency key already exists. |
INVALID_SESSION | 404 | The checkout session ID is not found or has already been used. |
REFUND_EXCEEDS_AMOUNT | 422 | The refund amount is greater than the original charge or remaining refundable balance. |
ALREADY_REFUNDED | 409 | The charge has already been fully refunded. |
Webhooks
| Code | Status | Description |
|---|---|---|
INVALID_SIGNATURE | 401 | The webhook signature does not match — the payload may have been tampered with or the wrong secret is being used. |
MISSING_EVENT_TYPE | 400 | The webhook body does not contain an event field. |
Infrastructure
| Code | Status | Description |
|---|---|---|
RATE_LIMITED | 429 | You have exceeded the request rate limit. Back off and retry after the Retry-After header value. |
NOT_FOUND | 404 | The requested resource does not exist. |
INTERNAL_ERROR | 500 | An unexpected error occurred on the Seev API. Retry with exponential backoff. |
NETWORK_ERROR | — | Your server could not reach the API — check connectivity, DNS, firewall rules, and request timeouts. |
Handling errors with the payment gateway
When creating a checkout session through the REST API, errors generally fall into two categories:
1. Validation errors (before the customer sees anything) These happen when your server calls the checkout endpoint with invalid parameters. Catch them and respond to your user with a meaningful message — do not redirect to the checkout URL.
2. Payment errors (after the customer completes the flow)
These surface when you verify the returned session_id or receive a webhook event. A CHARGE_FAILED or CHARGE_EXPIRED code means the payment did not go through — show an error and let the customer try again.
Example
// --- Creating checkout ---
let checkoutUrl: string;
try {
const response = await fetch(
'https://api.seevplus.com/api/v1/developer/payments',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SEEV_CHECKOUT_API_KEY}`,
'Idempotency-Key': `order_${orderId}_attempt_1`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'checkout',
amount: 5000,
currency: 'GHS',
recipient: { name: 'Jane Doe', email: 'jane@example.com' },
redirect_url: 'https://yourapp.com/payment/callback',
meta: { orderId },
}),
},
);
const payload = await response.json();
if (!response.ok || !payload.success) {
console.error('[Seev] Checkout failed:', payload);
return res
.status(400)
.json({ error: 'Could not initiate payment. Please try again.' });
}
checkoutUrl = payload.data.checkout_url;
} catch (error) {
if (error instanceof Error) {
// Log the raw message for debugging
console.error('[Seev] Checkout request failed:', error.message);
// Return a generic message to the client — never expose raw API errors
return res
.status(500)
.json({ error: 'Could not initiate payment. Please try again.' });
}
}
// --- Verifying the result after redirect ---
try {
const response = await fetch(
`https://api.seevplus.com/api/v1/developer/payments/${encodeURIComponent(sessionRef)}`,
);
const result = await response.json();
const status = result?.data?.status;
if (status === 'completed' || status === 'success') {
// Fulfil the order
} else {
// Payment declined — prompt the user to retry
return res.status(400).json({ error: 'Payment was not completed.' });
}
} catch (error) {
if (error instanceof Error) {
console.error('[Seev] Verification error:', error.message);
return res
.status(400)
.json({ error: 'Could not verify this payment.' });
}
}Best practices
Never display raw API error messages to users
The message field is intended for logs and developers. It may contain internal details that are confusing or misleading to end users. Map error codes to friendly copy in your UI instead.
Use code for programmatic handling
Always branch on code, not message. The message may change across API versions; the code is stable.
switch (errorCode) {
case 'CHARGE_FAILED':
return 'Your payment was declined. Please try a different payment method.';
case 'CHARGE_EXPIRED':
return 'Your payment session timed out. Please start again.';
default:
return 'Something went wrong. Please try again or contact support.';
}Retry 5xx errors with exponential backoff
INTERNAL_ERROR (500) and RATE_LIMITED (429) are transient. Retry with increasing delays — for example, 1s, 2s, 4s — with a maximum of 3–4 attempts. Do not retry 4xx errors as they indicate a problem with the request itself.
Log the full error context
Include the error code, message, param, and the relevant resource ID (e.g. transaction reference) in your logs so you can correlate failures in your monitoring tools.
Reject invalid webhook signatures
If signature verification fails, do not process the event or apply any side effects. Return 401 Unauthorized (or another non-2xx response), log the event ID and failure, and investigate repeated invalid deliveries. Only return 2xx after the request signature and timestamp have been accepted.