Error Handling
Understand error codes and handle them gracefully in your integration.
Error Response Format
All API errors return a consistent JSON structure:
{
"success": false,
"error": {
"code": "INVALID_AMOUNT",
"message": "Amount must be a positive number",
"details": {
"field": "amount",
"value": "-50.00"
}
}
}| Field | Type | Description |
|---|---|---|
success | boolean | Always false for errors |
error.code | string | Machine-readable error code |
error.message | string | Human-readable error description |
error.details | object | Additional context (optional) |
HTTP Status Codes
| Status | Meaning | When it occurs |
|---|---|---|
200 | OK | Request succeeded |
201 | Created | Resource created successfully |
400 | Bad Request | Invalid request parameters |
401 | Unauthorized | Missing or invalid API key |
403 | Forbidden | Valid API key but insufficient permissions |
404 | Not Found | Resource doesn’t exist |
409 | Conflict | Resource already exists or state conflict |
422 | Unprocessable Entity | Request understood but cannot be processed |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Something went wrong on our end |
503 | Service Unavailable | Temporary maintenance or overload |
Error Codes
Authentication Errors
| Code | HTTP Status | Description |
|---|---|---|
MISSING_API_KEY | 401 | No API key provided in Authorization header |
INVALID_API_KEY | 401 | API key is malformed or doesn’t exist |
EXPIRED_API_KEY | 401 | API key has been revoked or expired |
INVALID_KEY_TYPE | 403 | Using public key for server-only operation |
INSUFFICIENT_SCOPE | 403 | API key doesn’t have required permission |
IP_NOT_ALLOWED | 403 | Request IP not in whitelist |
Validation Errors
| Code | HTTP Status | Description |
|---|---|---|
INVALID_AMOUNT | 400 | Amount is missing, negative, or invalid format |
INVALID_CURRENCY | 400 | Unsupported currency code |
INVALID_REFERENCE | 400 | Reference format is invalid |
INVALID_HPN_CODE | 400 | HPN code format is invalid |
MISSING_REQUIRED_FIELD | 400 | Required field not provided |
INVALID_URL | 400 | URL format is invalid (webhooks) |
Transaction Errors
| Code | HTTP Status | Description |
|---|---|---|
TRANSACTION_NOT_FOUND | 404 | Transaction with given reference doesn’t exist |
TRANSACTION_EXPIRED | 409 | Transaction has already expired |
TRANSACTION_ALREADY_VERIFIED | 409 | Transaction was already verified |
DUPLICATE_REFERENCE | 409 | Reference already used for another transaction |
RECONCILIATION_FAILED | 422 | Manual reconciliation couldn’t verify payment |
Webhook Errors
| Code | HTTP Status | Description |
|---|---|---|
WEBHOOK_NOT_FOUND | 404 | Webhook endpoint doesn’t exist |
INVALID_WEBHOOK_URL | 400 | Webhook URL must be HTTPS |
WEBHOOK_DELIVERY_FAILED | 422 | Webhook couldn’t be delivered |
INVALID_EVENT_TYPE | 400 | Unknown event type specified |
Rate Limit Errors
| Code | HTTP Status | Description |
|---|---|---|
RATE_LIMIT_EXCEEDED | 429 | Too many requests in time window |
Handling Errors
Example: Error Handling in 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
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:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704715600| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp when limit resets |
Handling Rate Limits
When you receive a 429 Too Many Requests response:
- Check the
X-RateLimit-Resetheader for when to retry - Implement exponential backoff for retries
- Consider caching responses where appropriate
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:
{
"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:
{
"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:
- Check the System Status page for outages
- Review your API logs in the dashboard
- 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
- Authentication - API key setup and scopes
- Payments API - Transaction endpoints
- Webhooks - Event handling