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"]
}
FieldTypeDescription
statusintegerHTTP status code
messagestringHuman-readable error summary
datanullAlways null for errors
errorarrayList of specific error messages

HTTP status codes

CodeMeaningWhen it happens
200SuccessRequest completed successfully
400Bad RequestMissing or invalid parameters
401UnauthorizedMissing or invalid API key
403ForbiddenKey lacks permission for this operation
404Not FoundTransaction reference doesn't exist
422Unprocessable EntityRequest is well-formed but semantically invalid
429Too Many RequestsRate limit exceeded
500Server ErrorSomething 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 Authorization header - 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).

OperationRequired key
Initialize transactionPublic key
Verify transactionSecret key
Direct card chargeSecret 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 transRef is correct (not your businessRef)
  • 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:

StatusMeaningAction
0SuccessfulFulfill the order
3FailedShow failure message, allow retry
7DeclinedFraud check failed - do not retry
9Cancelled (customer)Allow the customer to try again
10Cancelled (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

On this page