Error Handling
How KDA API returns errors.
All API requests must be made over HTTPS to: https://kda-turbo-web.vercel.app
Error Response Format
All errors follow a consistent structure. When a request fails, the API responds with a non-200 HTTP status code and a JSON body containing the error details:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "A human-readable description of what went wrong.",
"transactionId": "KDS-AIR-A1B2C3D4",
"details": []
}
}The transactionId field is included inside the error object when a purchase transaction fails (TRANSACTION_FAILED) or when a duplicate idempotencyKey is detected (CONFLICT). This allows you to reference the failed transaction for support purposes.
Error Codes
Prop
Type
Validation Errors
When a VALIDATION_ERROR occurs, the details array pinpoints exactly which fields are invalid:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "One or more validation errors occurred.",
"details": [
{
"field": "identifier",
"message": "Invalid input: expected string, received undefined"
}
]
}
}Rate Limits
Every API key has a per-key rate limit, enforced by the API server on each request:
| Key type | Limit | Window |
|---|---|---|
| LIVE | 1,000 requests | 1 hour |
| SANDBOX | 500 requests | 1 hour |
If your base rate limit is too small for your expected traffic, contact support or an administrator to request a limit extension for your key.
The limit applies to all endpoints under /api/services. When the limit for your key is exhausted, subsequent requests are rejected with 429 - RATE_LIMITED until the window resets. The response includes retryAfterMs (time in milliseconds until the window resets) plus both the standard Retry-After and X-Retry-After HTTP headers (in seconds), so you can schedule retries accurately:
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded for this API key. Please try again later.",
"retryAfterMs": 1800000
}
}Do not retry immediately on a RATE_LIMITED response — every request against the limit is rejected. Wait for the Retry-After duration (or retryAfterMs) before making another call.
Error Examples by Code
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Your wallet balance is too low to complete the requested transaction."
}
}{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "The API key scope does not permit access to this endpoint."
}
}{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Unsupported endpoint"
}
}{
"success": false,
"error": {
"code": "CONFLICT",
"message": "This transaction has already been completed successfully.",
"transactionId": "DAT-A1B2C3D4E5F6G7H8I9J0K",
"idempotencyKey": "IDEMP-abc123def456"
}
}{
"success": false,
"error": {
"code": "TRANSACTION_FAILED",
"message": "Network issue with service provider. Please try again in a few minutes.",
"transactionId": "ELC-A1B2C3D4E5F6G7H8I9J0K"
}
}{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded for this API key. Please try again later.",
"retryAfterMs": 1800000
}
}{
"success": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected server error occurred.",
"transactionId": "KDS-DC-A1B2C3D4E5F6G7H8I9J0K"
}
}TRANSACTION_FAILED always includes the transactionId field, which the provider generates at the start of every purchase for tracing and refunds. CONFLICT errors also include transactionId and idempotencyKey from the previously processed transaction so you can identify which duplicate it refers to.
Handling Errors in Code
When building your integration, always check the success boolean in the response. If a transaction fails, parsing the error.code allows you to trigger specific fallback logic in your application. For validation errors, mapping over the error.details array is the best way to provide precise, field-level feedback directly to your end users.
const response = await fetch("https://kda-turbo-web.vercel.app/api/services/data", {
method: "POST",
headers: {
"Authorization": "Bearer kds_test_YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
planId: "kds-data-mtn-sme-v1-003",
number: "08012345678",
idempotencyKey: crypto.randomUUID()
})
});
const data = await response.json();
if (!data.success) {
const error = data.error;
console.error(`Error [${error.code}]: ${error.message}`);
switch (error.code) {
case "VALIDATION_ERROR":
error.details.forEach(d => {
console.error(` - ${d.field}: ${d.message}`);
});
break;
case "INSUFFICIENT_BALANCE":
// Fund your wallet and try again
console.error("Please fund your wallet and try again.");
break;
case "CONFLICT":
// Idempotency key re-use — the transaction already exists
console.error(`Existing transaction: ${error.transactionId}`);
// Return the existing ID instead of retrying
return { transactionId: error.transactionId };
case "TRANSACTION_FAILED":
console.error(`Purchase failed. Ref: ${error.transactionId}`);
// Notify support with the transaction ID if needed
break;
case "RATE_LIMITED":
// Wait for the window to reset before retrying
const retryAfter = error.retryAfterMs ?? 60000;
console.error(`Rate limited. Retrying in ${retryAfter}ms...`);
setTimeout(() => {
// Re-run your purchase logic here (same idempotencyKey is fine —
// the original request was never processed)
}, retryAfter);
break;
default:
// Generic fallback for unexpected errors
console.error(`Unexpected error: ${error.message}`);
}
} else {
console.log("Transaction ID:", data.transactionId);
}import requests
import time
import uuid
headers = {
"Authorization": "Bearer kds_test_YOUR_API_KEY",
"Content-Type": "application/json"
}
payload = {
"planId": "kds-data-mtn-sme-v1-003",
"number": "08012345678",
"idempotencyKey": str(uuid.uuid4())
}
response = requests.post(
"https://kda-turbo-web.vercel.app/api/services/data",
headers=headers,
json=payload
)
data = response.json()
if not data.get("success"):
error = data.get("error", {})
code = error.get("code")
print(f"Error [{code}]: {error.get('message')}")
if code == "VALIDATION_ERROR":
for d in error.get("details", []):
print(f" - {d.get('field')}: {d.get('message')}")
elif code == "INSUFFICIENT_BALANCE":
print("Please fund your wallet and try again.")
elif code == "CONFLICT":
print(f"Existing transaction: {error.get('transactionId')}")
# Return the existing ID instead of retrying
elif code == "TRANSACTION_FAILED":
print(f"Purchase failed. Ref: {error.get('transactionId')}")
# Notify support with the transaction ID if needed
elif code == "RATE_LIMITED":
# Wait for the window to reset before retrying
retry_after = error.get("retryAfterMs", 60000) / 1000
print(f"Rate limited. Retrying in {retry_after}s...")
time.sleep(retry_after)
else:
print(f"Unexpected error: {error.get('message')}")
else:
print("Transaction ID:", data.get("transactionId"))<?php
$ch = curl_init("https://kda-turbo-web.vercel.app/api/services/data");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer kds_test_YOUR_API_KEY",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"planId" => "kds-data-mtn-sme-v1-003",
"number" => "08012345678",
"idempotencyKey" => bin2hex(random_bytes(16)),
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true);
if (!$data['success']) {
$error = $data['error'];
$code = $error['code'];
echo "Error [$code]: {$error['message']}\n";
switch ($code) {
case 'VALIDATION_ERROR':
foreach ($error['details'] as $d) {
echo " - {$d['field']}: {$d['message']}\n";
}
break;
case 'INSUFFICIENT_BALANCE':
echo "Please fund your wallet and try again.\n";
break;
case 'CONFLICT':
echo "Existing transaction: {$error['transactionId']}\n";
// Return the existing ID instead of retrying
break;
case 'TRANSACTION_FAILED':
echo "Purchase failed. Ref: {$error['transactionId']}\n";
// Notify support with the transaction ID if needed
break;
case 'RATE_LIMITED':
// Wait for the window to reset before retrying
$retryAfter = $error['retryAfterMs'] ?? 60000;
echo "Rate limited. Retrying in {$retryAfter}ms...\n";
usleep($retryAfter * 1000);
break;
default:
echo "Unexpected error: {$error['message']}\n";
}
} else {
echo "Transaction ID: {$data['transactionId']}\n";
}