Skip to content

Error Handling ​

The Octeth API returns detailed error information to help you debug issues quickly.

Response Structure ​

All API responses follow this format:

json
{
  "Success": false,
  "ErrorCode": 3,
  "ErrorText": "Invalid credentials"
}

Error codes can be single values or arrays:

json
{
  "Success": false,
  "ErrorCode": [1, 2],
  "ErrorText": ["Missing username", "Missing password"]
}

Common Error Codes ​

Authentication Errors ​

CodeDescriptionFix
99998Invalid API key or session, or a user session used from an address outside the user-area IP allow-list (ErrorText "Access from this IP address is not allowed")Check your authentication credentials, or call from an allowed address
99999Session expiredRe-authenticate to get a new session
3Invalid login credentialsVerify username and password
6Invalid 2FA codeCheck your authenticator app
1012FA requiredInclude tfacode parameter

AI Agent Key Errors ​

Returned only for requests made with an AI agent key. See AI agent keys.

CodeHTTPDescriptionFix
100010403The command needs a scope the key does not hold (RequiredScope, GrantedScopes), or is not on the agent allow-list (RequiredScope: none)Ask the account owner to grant the scope on the Connected AI assistants page, or do it in the app
100011429A budget was exceeded (Budget, Limit, Usage): sends per day, recipients per send or rows per import. Usage: null means the recipients could not be counted before the command runs, so a recipients cap or confirm threshold could not be honouredWait for the daily reset (midnight UTC) or send to fewer recipients. A confirmation cannot raise it. For an uncountable command, ask the account owner to do it in the app
100012409The send needs confirmation. The response carries Preview and ConfirmTokenShow the preview, then repeat the identical request with ConfirmToken within ten minutes
100013409The ConfirmToken is unknown, expired, already used, or was issued for different parametersRepeat the request without ConfirmToken to get a new preview and token
100014403An agent key was combined with a SessionID or an AdminAPIKeySend the agent key as the only credential
100015403The Command parameter and the endpoint route name two different commandsCall the endpoint of the command you mean, or drop the Command parameter
100016403A parameter an agent key may not send to this command (Parameter), an ID that is not a plain positive whole number, or a list or segment that is not the account's ownDrop the parameter, send the ID as digits only, or ask the account owner to do it in the app

Admin agent keys (operator AI assistants, see Admin agent keys) use the same codes. For them 100012 answers every action (admin:act) with a Preview of the values before and after, 100011 means the confirmation step could not run because Redis is unavailable, 100014 means the key is revoked, unknown or combined with another credential, and 100016 also covers a target user or campaign outside the admin's user groups (RefusedParameter).

Validation Errors ​

CodeDescriptionCommon Cause
1Missing required fieldCheck API documentation for required parameters
2Invalid field formatEnsure correct data type (string, number, etc.)
10Invalid email addressUse valid email format
11Invalid date formatUse YYYY-MM-DD format

Resource Errors ​

CodeDescriptionCommon Cause
404Resource not foundCheck if ID exists and belongs to your account
403Permission deniedEnsure correct scope (User vs Admin)
409Duplicate entryResource already exists (e.g., duplicate email)
429Rate limit exceededSlow down API requests

Limit Errors ​

CodeDescriptionResolution
16User limit reached (the license's user account limit)Use a license key with more user accounts, or delete unused users
18Subscriber limit reachedUpgrade plan or remove inactive subscribers
3List limit exceededDelete unused lists or upgrade plan

Hard Failures ​

Every code listed above is a handler error: the endpoint ran and rejected your request (bad parameters, missing permissions, an expired session). Those answer HTTP 200 with the normal envelope — that contract is unchanged.

A hard failure is different: the request never reached a working endpoint, or the endpoint failed in a way it did not model. These answer HTTP 4xx or 5xx with the same envelope shape.

CodeDescriptionHTTPResolution
100000The request body was not valid JSON400Fix the payload your client is sending
100001Unsupported or invalid connection method400Check the request method and endpoint you are calling
100003Unknown or unroutable command400Check the Command name against this reference
100002The command is registered but its handler is missing on the server500Report to your Octeth administrator; the installation is incomplete or a file failed to deploy
100004A required server-side component could not be loaded500Report to your Octeth administrator
100005An unhandled exception escaped the endpoint500Report to your Octeth administrator with the time of the call

The three 400 codes above are the complete set. Any hard-failure code outside that set — including one an endpoint invents — answers 500, because a code the server does not recognise cannot be assumed to be the caller's fault. Endpoints that need to reject a request signal it through their own Success: false envelope at HTTP 200 (see each endpoint's Error Codes block), not through a hard failure.

A hard failure looks like this:

json
{
  "Success": false,
  "ErrorCode": 100003,
  "ErrorText": "Invalid API command: \"nope\" not found.",
  "Errors": [ { "Code": 100003, "Message": "Invalid API command: \"nope\" not found." } ]
}

Server-side detail — file paths, stack context — is written to the Octeth error log and never to the response.

New in v5.9.3

Several of these paths previously answered with an HTML page and an HTTP 200 status, some of them disclosing an absolute filesystem path. Clients that branch on the status code treated a fatal error as a success and then failed to parse the body. They now return a proper 4xx/5xx with the standard error envelope.

The 4xx codes mean the call was wrong and retrying it unchanged will not help. The 5xx codes indicate a server-side fault; retrying will not help either until the installation is fixed.

ResponseFormat=XML on hard failures ​

Hard failures honour ResponseFormat=XML and answer with Content-Type: text/xml carrying the same envelope:

xml
<?xml version="1.0" encoding="utf-8"?>
<response>
  <Success><![CDATA[false]]></Success>
  <ErrorCode><![CDATA[100003]]></ErrorCode>
  <ErrorText><![CDATA[Invalid API command: "nope" not found.]]></ErrorText>
  <Errors><node_0><Code><![CDATA[100003]]></Code><Message><![CDATA[Invalid API command: "nope" not found.]]></Message></node_0></Errors>
</response>

There are two deliberate exceptions — 100000 and 100001 always answer in JSON, whatever you asked for. Both are raised before the request has been parsed far enough to know which format was requested.

ResponseFormat casing

By default the value must be exactly JSON or XML. Any other spelling, including lowercase xml, is silently treated as JSON on both successful responses and hard failures. This applies to the whole API, not just error paths.

An installation can opt into case-insensitive matching by setting API_RESPONSEFORMAT_CASE_INSENSITIVE=true in .oempro_env. With it on, xml, Xml and json resolve to XML and JSON, and the resolved format is used for both successful responses and hard failures, so one request never answers in two formats. Any value that is not a spelling of JSON or XML (an empty value, yaml, a value with surrounding spaces) still falls back to JSON. The setting is off by default. Send the uppercase value if you do not control the installation's configuration.

Handling Errors in Code ​

JavaScript/Node.js ​

javascript
async function apiCall(command, data) {
  const response = await fetch('https://your-domain.com/api.php', {
    method: 'POST',
    body: new URLSearchParams({
      ResponseFormat: 'JSON',
      Command: command,
      APIKey: apiKey,
      ...data
    })
  });
  
  const result = await response.json();
  
  if (!result.Success) {
    // Handle error
    const errors = Array.isArray(result.ErrorCode) 
      ? result.ErrorCode 
      : [result.ErrorCode];
    
    errors.forEach((code, index) => {
      const message = Array.isArray(result.ErrorText)
        ? result.ErrorText[index]
        : result.ErrorText;
      
      console.error(`Error ${code}: ${message || getErrorMessage(code)}`);
    });
    
    throw new Error(`API Error: ${errors.join(', ')}`);
  }
  
  return result;
}

// Usage with error handling
try {
  const list = await apiCall('List.Create', {
    SubscriberListName: 'Newsletter'
  });
  console.log('List created:', list.ListID);
} catch (error) {
  console.error('Failed to create list:', error.message);
}

PHP ​

php
<?php
function apiCall($command, $data = []) {
    $data['ResponseFormat'] = 'JSON';
    $data['Command'] = $command;
    $data['APIKey'] = $_ENV['OCTETH_API_KEY'];
    
    $ch = curl_init('https://your-domain.com/api.php');
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    $result = json_decode($response, true);
    
    if (!$result['Success']) {
        $errors = is_array($result['ErrorCode']) 
            ? $result['ErrorCode'] 
            : [$result['ErrorCode']];
        
        $errorMessages = [];
        foreach ($errors as $index => $code) {
            $message = is_array($result['ErrorText']) 
                ? $result['ErrorText'][$index] 
                : $result['ErrorText'] ?? '';
            
            $errorMessages[] = "Error $code: $message";
        }
        
        throw new Exception(implode(', ', $errorMessages));
    }
    
    return $result;
}

// Usage
try {
    $list = apiCall('List.Create', [
        'SubscriberListName' => 'Newsletter'
    ]);
    echo "List created: " . $list['ListID'];
} catch (Exception $e) {
    error_log("API Error: " . $e->getMessage());
}
?>

Python ​

python
import requests
from typing import Dict, Any

class OctethAPIError(Exception):
    def __init__(self, errors, messages=None):
        self.errors = errors if isinstance(errors, list) else [errors]
        self.messages = messages if isinstance(messages, list) else [messages]
        super().__init__(self.format_message())
    
    def format_message(self):
        if self.messages and self.messages[0]:
            return ', '.join(f"Error {e}: {m}" 
                           for e, m in zip(self.errors, self.messages))
        return f"API Errors: {', '.join(map(str, self.errors))}"

def api_call(command: str, data: Dict[str, Any] = {}) -> Dict:
    payload = {
        'ResponseFormat': 'JSON',
        'Command': command,
        'APIKey': os.getenv('OCTETH_API_KEY'),
        **data
    }
    
    response = requests.post('https://your-domain.com/api.php', data=payload)
    result = response.json()
    
    if not result.get('Success'):
        raise OctethAPIError(
            result.get('ErrorCode'),
            result.get('ErrorText')
        )
    
    return result

# Usage
try:
    list_data = api_call('List.Create', {
        'SubscriberListName': 'Newsletter'
    })
    print(f"List created: {list_data['ListID']}")
except OctethAPIError as e:
    print(f"Failed: {e}")

Error Recovery Strategies ​

Retry with exponential backoff ​

javascript
async function apiCallWithRetry(command, data, maxRetries = 3) {
  let lastError;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await apiCall(command, data);
    } catch (error) {
      lastError = error;
      
      // Check if error is retryable
      if (error.message.includes('99999') || // Session expired
          error.message.includes('429')) {    // Rate limited
        
        // Exponential backoff
        const delay = Math.pow(2, i) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
        
        // Re-authenticate if session expired
        if (error.message.includes('99999')) {
          await refreshSession();
        }
      } else {
        // Non-retryable error
        throw error;
      }
    }
  }
  
  throw lastError;
}

Handle specific errors ​

php
function createSubscriber($email, $listId) {
    try {
        return apiCall('Subscriber.Create', [
            'EmailAddress' => $email,
            'ListID' => $listId,
            'Status' => 'Subscribed',
            'SubscriptionDate' => date('Y-m-d'),
            'OptInDate' => date('Y-m-d'),
            'SubscriptionIP' => $_SERVER['REMOTE_ADDR']
        ]);
    } catch (Exception $e) {
        if (strpos($e->getMessage(), 'Error 20') !== false) {
            // Duplicate subscriber - update instead
            return apiCall('Subscriber.Update', [
                'EmailAddress' => $email,
                'ListID' => $listId,
                'Status' => 'Subscribed'
            ]);
        } elseif (strpos($e->getMessage(), 'Error 18') !== false) {
            // Subscriber limit reached
            throw new Exception('Subscriber limit reached. Please upgrade your plan.');
        } else {
            throw $e;
        }
    }
}

Endpoint-Specific Errors ​

User.Login ​

CodeDescription
1Missing username
2Missing password
3Invalid credentials
6Invalid 2FA code

List.Create ​

CodeDescription
1Missing list name
3List limit exceeded

Subscriber.Create ​

CodeDescription
1Missing ListID
2Missing EmailAddress
3Invalid email format
4Invalid ListID
18Subscriber limit exceeded
20Duplicate email address

Campaign.Create ​

CodeDescription
1Missing campaign name

Campaign.Update ​

CodeDescription
1Missing CampaignID
2Campaign not found
3Invalid status
4Invalid EmailID
5Invalid schedule type
6Missing send date
7Missing send time
14Invalid auto-resend days
15Missing auto-resend subject

Email.Create ​

No specific error codes (returns empty EmailID on success).

Email.Update ​

CodeDescription
1Missing EmailID
2Email not found
21FromEmail has no domain and no sender domain is set (Campaign scope)

Debugging Tips ​

Enable verbose logging ​

bash
# Log full request and response
curl -v https://your-domain.com/api.php \
  -F "ResponseFormat=JSON" \
  -F "Command=List.Create" \
  -F "APIKey=your-api-key" \
  -F "SubscriberListName=Test"

Check required parameters ​

javascript
// Helper to validate required fields
function validateRequired(data, required) {
  const missing = required.filter(field => !data[field]);
  if (missing.length > 0) {
    throw new Error(`Missing required fields: ${missing.join(', ')}`);
  }
}

// Use before API call
validateRequired(data, ['EmailAddress', 'ListID', 'Status']);

Log API errors ​

python
import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

def api_call_with_logging(command, data):
    logger.debug(f"API Call: {command} with {data}")
    
    try:
        result = api_call(command, data)
        logger.debug(f"Success: {result}")
        return result
    except OctethAPIError as e:
        logger.error(f"API Error in {command}: {e}")
        logger.debug(f"Request data: {data}")
        raise

Best Practices ​

  1. Always check Success field - Even with HTTP 200, check Success: false
  2. Handle arrays and single values - ErrorCode can be either
  3. Log errors with context - Include command and parameters
  4. Implement retry logic - For transient errors (rate limits, timeouts)
  5. Map error codes to user messages - Don't expose raw error codes to users
  6. Use appropriate authentication scope - User vs Admin based on operation
  7. Validate before sending - Check required fields client-side

Next Steps ​

Any questions? Contact us.