Error Handling
Understand Credo API error responses and implement robust error handling in your integration.
The Credo API uses conventional HTTP status codes and structured JSON responses to communicate errors. This guide covers the error format, common errors, and patterns for handling them.
Error response format
All error responses follow this structure:
{
"status": 400,
"message": "Validation error",
"data": null,
"error": ["Amount is required", "Email is required"]
}| Field | Type | Description |
|---|---|---|
status | integer | HTTP status code |
message | string | Human-readable error summary |
data | null | Always null for errors |
error | array | List of specific error messages |
HTTP status codes
| Code | Meaning | When it happens |
|---|---|---|
200 | Success | Request completed successfully |
400 | Bad Request | Missing or invalid parameters |
401 | Unauthorized | Missing or invalid API key |
403 | Forbidden | Key lacks permission for this operation |
404 | Not Found | Transaction reference doesn't exist |
422 | Unprocessable Entity | Request is well-formed but semantically invalid |
429 | Too Many Requests | Rate limit exceeded |
500 | Server Error | Something went wrong on Credo's end |
Common errors and fixes
Authentication errors
401 - Missing or invalid API key
{
"status": 401,
"message": "Unauthorized",
"error": ["Invalid API key"]
}Causes and fixes:
- No
Authorizationheader - add the header to every request - Wrong key for the environment - sandbox keys don't work on production and vice versa
- Key was regenerated - get the current key from your dashboard
- Extra whitespace - trim the key value in your environment variable
403 - Wrong key type
Using a public key for an operation that requires a secret key (e.g., verification).
| Operation | Required key |
|---|---|
| Initialize transaction | Public key |
| Verify transaction | Secret key |
| Direct card charge | Secret key |
Validation errors
400 - Missing required fields
{
"status": 400,
"message": "Validation error",
"error": ["Amount is required", "Currency is required"]
}Check that your request includes all required fields.
400 - Invalid amount
{
"status": 400,
"message": "Validation error",
"error": ["Amount must be a positive integer"]
}Remember: amounts are in the lowest currency unit (kobo/cents). To charge NGN 100, send 10000, not 100.
400 - Invalid email
The email field must be a valid email format.
Transaction errors
404 - Transaction not found
{
"status": 404,
"message": "Transaction not found",
"error": ["No transaction found with reference: vs_invalid"]
}Check that:
- The
transRefis correct (not yourbusinessRef) - You're querying the right environment (sandbox vs. production)
Rate limiting
429 - Too many requests
{
"status": 429,
"message": "Rate limit exceeded",
"error": ["Too many requests. Please try again later."]
}Implement exponential backoff when retrying:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
return response;
}
throw new Error("Max retries exceeded");
}Building robust error handling
Wrap API calls
Create a helper that handles errors consistently:
class CredoApiError extends Error {
constructor(status, message, errors) {
super(message);
this.status = status;
this.errors = errors;
}
}
async function credoRequest(path, options = {}) {
const baseUrl = process.env.CREDO_API_URL;
const response = await fetch(`${baseUrl}${path}`, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
const body = await response.json();
if (body.status !== 200) {
throw new CredoApiError(body.status, body.message, body.error);
}
return body.data;
}
// Usage
try {
const data = await credoRequest("/transaction/initialize", {
method: "POST",
headers: { "Authorization": process.env.CREDO_PUBLIC_KEY },
body: JSON.stringify(payload),
});
// Redirect to data.authorizationUrl
} catch (error) {
if (error instanceof CredoApiError) {
if (error.status === 401) {
// Handle auth error
} else if (error.status === 400) {
// Handle validation - show error.errors to user
}
} else {
// Network error - retry or show generic message
}
}import requests
class CredoApiError(Exception):
def __init__(self, status, message, errors):
self.status = status
self.message = message
self.errors = errors
super().__init__(message)
def credo_request(path, method="GET", data=None, api_key=None):
base_url = os.environ["CREDO_API_URL"]
response = requests.request(
method,
f"{base_url}{path}",
headers={
"Authorization": api_key,
"Content-Type": "application/json",
},
json=data,
)
body = response.json()
if body["status"] != 200:
raise CredoApiError(body["status"], body["message"], body.get("error", []))
return body["data"]
# Usage
try:
data = credo_request(
"/transaction/initialize",
method="POST",
data=payload,
api_key=os.environ["CREDO_PUBLIC_KEY"],
)
except CredoApiError as e:
if e.status == 401:
# Handle auth error
pass
elif e.status == 400:
# Handle validation
pass<?php
class CredoApiError extends Exception {
public $status;
public $errors;
public function __construct($status, $message, $errors = []) {
parent::__construct($message);
$this->status = $status;
$this->errors = $errors;
}
}
function credoRequest($path, $method = "GET", $data = null, $apiKey = null) {
$baseUrl = getenv("CREDO_API_URL");
$ch = curl_init($baseUrl . $path);
$headers = [
"Authorization: " . $apiKey,
"Content-Type: application/json",
];
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
]);
if ($data) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$body = json_decode($response, true);
if ($body["status"] !== 200) {
throw new CredoApiError($body["status"], $body["message"], $body["error"] ?? []);
}
return $body["data"];
}
// Usage
try {
$data = credoRequest("/transaction/initialize", "POST", $payload, getenv("CREDO_PUBLIC_KEY"));
// Redirect to $data["authorizationUrl"]
} catch (CredoApiError $e) {
if ($e->status === 401) {
// Handle auth error
} elseif ($e->status === 400) {
// Handle validation
}
}import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class CredoApiClient {
private static final ObjectMapper mapper = new ObjectMapper();
private final String baseUrl;
private final HttpClient client;
public CredoApiClient() {
this.baseUrl = System.getenv("CREDO_API_URL");
this.client = HttpClient.newHttpClient();
}
public JsonNode request(String path, String method, String apiKey, String jsonBody) throws CredoApiException {
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + path))
.header("Authorization", apiKey)
.header("Content-Type", "application/json");
if ("POST".equals(method)) {
builder.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
} else {
builder.GET();
}
try {
HttpResponse<String> response = client.send(builder.build(), HttpResponse.BodyHandlers.ofString());
JsonNode body = mapper.readTree(response.body());
if (body.get("status").asInt() != 200) {
throw new CredoApiException(
body.get("status").asInt(),
body.get("message").asText(),
body.get("error")
);
}
return body.get("data");
} catch (Exception e) {
throw new CredoApiException(0, "Network error", null);
}
}
}
class CredoApiException extends Exception {
public final int status;
public final JsonNode errors;
public CredoApiException(int status, String message, JsonNode errors) {
super(message);
this.status = status;
this.errors = errors;
}
}using System.Text;
using System.Text.Json;
public class CredoApiException : Exception
{
public int Status { get; }
public JsonElement Errors { get; }
public CredoApiException(int status, string message, JsonElement errors)
: base(message)
{
Status = status;
Errors = errors;
}
}
public class CredoApiClient
{
private readonly string _baseUrl;
private readonly HttpClient _client;
public CredoApiClient(IConfiguration config)
{
_baseUrl = config["CREDO_API_URL"];
_client = new HttpClient();
}
public async Task<JsonElement> RequestAsync(string path, HttpMethod method, string apiKey, object data = null)
{
var request = new HttpRequestMessage(method, $"{_baseUrl}{path}");
request.Headers.Add("Authorization", apiKey);
if (data != null)
{
request.Content = new StringContent(
JsonSerializer.Serialize(data),
Encoding.UTF8,
"application/json"
);
}
var response = await _client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(body);
var root = doc.RootElement;
if (root.GetProperty("status").GetInt32() != 200)
{
throw new CredoApiException(
root.GetProperty("status").GetInt32(),
root.GetProperty("message").GetString(),
root.TryGetProperty("error", out var err) ? err : default
);
}
return root.GetProperty("data");
}
}
// Usage
try
{
var data = await client.RequestAsync("/transaction/initialize", HttpMethod.Post, apiKey, payload);
// Redirect to data.GetProperty("authorizationUrl")
}
catch (CredoApiException ex)
{
if (ex.Status == 401)
{
// Handle auth error
}
else if (ex.Status == 400)
{
// Handle validation
}
}Handle network failures
Network issues (DNS resolution, timeouts, connection resets) require different handling than API errors:
async function safeVerify(transRef) {
try {
const data = await credoRequest(
`/transaction/${transRef}/verify`,
{ headers: { "Authorization": process.env.CREDO_SECRET_KEY } }
);
return { success: true, data };
} catch (error) {
if (error instanceof CredoApiError) {
// API returned an error - don't retry
return { success: false, error: error.message };
}
// Network error - safe to retry
console.error("Network error verifying transaction:", error.message);
return { success: false, retryable: true };
}
}Log everything
Log all API interactions for debugging and support:
async function credoRequest(path, options) {
const startTime = Date.now();
try {
const response = await fetch(`${baseUrl}${path}`, options);
const body = await response.json();
const duration = Date.now() - startTime;
console.log({
type: "credo_api",
path,
status: body.status,
duration,
transRef: body.data?.transRef,
});
return body;
} catch (error) {
console.error({
type: "credo_api_error",
path,
error: error.message,
duration: Date.now() - startTime,
});
throw error;
}
}Transaction status errors
Not all completed transactions are successful. After verification, check the status code:
| Status | Meaning | Action |
|---|---|---|
0 | Successful | Fulfill the order |
3 | Failed | Show failure message, allow retry |
7 | Declined | Fraud check failed - do not retry |
9 | Cancelled (customer) | Allow the customer to try again |
10 | Cancelled (merchant) | Log and investigate |
See Concepts for the full status code table.
Best practices
Never trust the client
Always verify transactions server-side. Don't rely on redirect parameters alone.
Use idempotency
Track processed transactions by transRef to avoid double-fulfillment.
Implement timeouts
Set reasonable timeouts (10-30 seconds) on API calls. Retry on network failures with backoff.
Show clear messages
Map API errors to user-friendly messages. Don't expose raw error arrays to customers.
Next steps
Was this page helpful?
Last updated on
