Reference

Error Handling

Understand error codes and handle them gracefully in your integration.

Error Response Format

All API errors return a consistent JSON structure:

JSON
{
  "success": false,
  "error": {
    "code": "INVALID_AMOUNT",
    "message": "Amount must be a positive number",
    "details": {
      "field": "amount",
      "value": "-50.00"
    }
  }
}
FieldTypeDescription
successbooleanAlways false for errors
error.codestringMachine-readable error code
error.messagestringHuman-readable error description
error.detailsobjectAdditional context (optional)

HTTP Status Codes

StatusMeaningWhen it occurs
200OKRequest succeeded
201CreatedResource created successfully
400Bad RequestInvalid request parameters
401UnauthorizedMissing or invalid API key
403ForbiddenValid API key but insufficient permissions
404Not FoundResource doesn’t exist
409ConflictResource already exists or state conflict
422Unprocessable EntityRequest understood but cannot be processed
429Too Many RequestsRate limit exceeded
500Internal Server ErrorSomething went wrong on our end
503Service UnavailableTemporary maintenance or overload

Error Codes

Authentication Errors

CodeHTTP StatusDescription
MISSING_API_KEY401No API key provided in Authorization header
INVALID_API_KEY401API key is malformed or doesn’t exist
EXPIRED_API_KEY401API key has been revoked or expired
INVALID_KEY_TYPE403Using public key for server-only operation
INSUFFICIENT_SCOPE403API key doesn’t have required permission
IP_NOT_ALLOWED403Request IP not in whitelist

Validation Errors

CodeHTTP StatusDescription
INVALID_AMOUNT400Amount is missing, negative, or invalid format
INVALID_CURRENCY400Unsupported currency code
INVALID_REFERENCE400Reference format is invalid
INVALID_HPN_CODE400HPN code format is invalid
MISSING_REQUIRED_FIELD400Required field not provided
INVALID_URL400URL format is invalid (webhooks)

Transaction Errors

CodeHTTP StatusDescription
TRANSACTION_NOT_FOUND404Transaction with given reference doesn’t exist
TRANSACTION_EXPIRED409Transaction has already expired
TRANSACTION_ALREADY_VERIFIED409Transaction was already verified
DUPLICATE_REFERENCE409Reference already used for another transaction
RECONCILIATION_FAILED422Manual reconciliation couldn’t verify payment

Webhook Errors

CodeHTTP StatusDescription
WEBHOOK_NOT_FOUND404Webhook endpoint doesn’t exist
INVALID_WEBHOOK_URL400Webhook URL must be HTTPS
WEBHOOK_DELIVERY_FAILED422Webhook couldn’t be delivered
INVALID_EVENT_TYPE400Unknown event type specified

Rate Limit Errors

CodeHTTP StatusDescription
RATE_LIMIT_EXCEEDED429Too many requests in time window

Handling Errors

Example: Error Handling in JavaScript

JavaScript
async function createPayment(amount, description) {
  try {
    const response = await fetch('https://api.harpoonsms.com/v1/transactions/initialize', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + process.env.HARPOON_API_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ amount, description })
    });

    const data = await response.json();

    if (!response.ok) {
      // Handle specific error codes
      switch (data.error?.code) {
        case 'INVALID_AMOUNT':
          throw new Error('Please enter a valid amount');
        case 'RATE_LIMIT_EXCEEDED':
          // Implement exponential backoff
          await delay(getBackoffDelay(retryCount));
          return createPayment(amount, description);
        case 'INSUFFICIENT_SCOPE':
          throw new Error('API key missing required permissions');
        default:
          throw new Error(data.error?.message || 'Payment creation failed');
      }
    }

    return data.data;
  } catch (error) {
    console.error('Payment error:', error);
    throw error;
  }
}

Example: Error Handling in Python

Python
import requests
from time import sleep

class HarpoonError(Exception):
    def __init__(self, code, message, details=None):
        self.code = code
        self.message = message
        self.details = details
        super().__init__(message)

def create_payment(amount: str, description: str, retries: int = 3):
    for attempt in range(retries):
        response = requests.post(
            'https://api.harpoonsms.com/v1/transactions/initialize',
            headers={
                'Authorization': 'Bearer ' + os.environ['HARPOON_API_KEY'],
                'Content-Type': 'application/json'
            },
            json={'amount': amount, 'description': description}
        )

        data = response.json()

        if response.ok:
            return data['data']

        error = data.get('error', {})
        code = error.get('code')

        # Handle rate limiting with exponential backoff
        if code == 'RATE_LIMIT_EXCEEDED' and attempt < retries - 1:
            sleep(2 ** attempt)
            continue

        raise HarpoonError(
            code=code,
            message=error.get('message', 'Unknown error'),
            details=error.get('details')
        )

Rate Limiting

API requests are rate-limited per API key. The default limit is 100 requests per minute for all endpoints.

Rate limiting is applied at multiple levels:

  • Per API key - Each API key has its own rate limit bucket
  • Per user - Authenticated users have per-user limits
  • Global - A global limit protects the overall system

Rate Limit Headers

Rate limit information is included in response headers:

Plain Text
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704715600
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when limit resets

Handling Rate Limits

When you receive a 429 Too Many Requests response:

  1. Check the X-RateLimit-Reset header for when to retry
  2. Implement exponential backoff for retries
  3. Consider caching responses where appropriate
JavaScript
function getBackoffDelay(attempt) {
  // Exponential backoff: 1s, 2s, 4s, 8s...
  return Math.min(1000 * Math.pow(2, attempt), 30000);
}

async function requestWithRetry(fn, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error.code === 'RATE_LIMIT_EXCEEDED' && attempt < maxRetries - 1) {
        await new Promise(r => setTimeout(r, getBackoffDelay(attempt)));
        continue;
      }
      throw error;
    }
  }
}

Debugging Tips

1. Verify API Key

Ensure your API key is valid and hasn’t been revoked. You can check your active keys in the dashboard.

2. Verify Scopes

If you receive INSUFFICIENT_SCOPE, check that your API key has the required permissions:

JSON
{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "API key does not have transactions:write scope",
    "details": {
      "required_scope": "transactions:write",
      "available_scopes": ["transactions:read"]
    }
  }
}

3. Inspect Request Details

The details object often contains field-specific error information:

JSON
{
  "error": {
    "code": "MISSING_REQUIRED_FIELD",
    "message": "Required field 'amount' is missing",
    "details": {
      "field": "amount",
      "location": "body"
    }
  }
}

Getting Help

If you encounter errors you can’t resolve:

  1. Check the System Status page for outages
  2. Review your API logs in the dashboard
  3. Contact support with:
    • Request ID (from response headers)
    • Error code and message
    • Request timestamp
    • Endpoint called

Tip

Always log the full error response, including the details object. This information is invaluable when debugging issues.

Related Resources