Appearance
Error Handling
The MailShield API uses conventional HTTP response codes and returns errors in a consistent JSON format.
Error Response Format
All errors follow this structure:
json
{
"error": {
"code": "error_code",
"message": "Human-readable error message",
"details": [] // Optional: more detail for some errors
}
}message says what went wrong and, where it can, what to do about it. details is only present for some errors: a list of problems for validation errors, or the current count and limit when the domain limit is reached.
HTTP Status Codes
| Code | Description |
|---|---|
200 | Success |
201 | Created (for POST requests) |
400 | Bad Request: invalid JSON, request body or query parameters |
401 | Unauthorized: invalid or missing authentication |
403 | Forbidden: valid token, but the request isn't allowed |
404 | Not Found: the resource doesn't exist |
409 | Conflict: the resource already exists, or the change conflicts with its state |
429 | Too Many Requests: rate limit exceeded |
500 | Internal Server Error: something went wrong on our end |
Error Codes
Authentication Errors
| Code | Status | Description |
|---|---|---|
unauthorized | 401 | Missing or invalid Authorization header |
unauthorized | 401 | Invalid or revoked API token |
unauthorized | 401 | API token has expired |
Authorization Errors
| Code | Status | Description |
|---|---|---|
forbidden | 403 | Request from an IP address outside the token's allowlist |
forbidden | 403 | Read-only token used for a request other than GET |
forbidden | 403 | The plan doesn't include API access, or a payment is overdue |
forbidden | 403 | Domain limit reached |
forbidden | 403 | Hosted MTA-STS needs a verified domain and a paid plan |
Resource Errors
| Code | Status | Description |
|---|---|---|
not_found | 404 | Domain not found (also returned for an ID that isn't a UUID) |
not_found | 404 | Customer not found (also returned for an ID that isn't a UUID) |
not_found | 404 | DKIM selector not found |
conflict | 409 | The domain is already in your organization |
conflict | 409 | There's already a customer with that name, ignoring case |
conflict | 409 | The DKIM selector is already registered |
conflict | 409 | Usage type change on a domain with passive monitoring |
Validation Errors
| Code | Status | Description |
|---|---|---|
validation_error | 400 | Invalid JSON, request body or query parameters |
The message names the first problem, starting with the field it's about, and details lists every problem:
json
{
"error": {
"code": "validation_error",
"message": "domain: Not a valid domain name. Send just the name, such as example.com.",
"details": [
{
"code": "custom",
"path": ["domain"],
"message": "Not a valid domain name. Send just the name, such as example.com."
},
{
"code": "custom",
"path": ["testEmailLocalPart"],
"message": "Required for domains that receive email (sends-and-receives or receives-only)."
}
]
}
}A body that isn't valid JSON, or query parameters the API can't use, are rejected the same way, without details:
| Problem | Example message |
|---|---|
| Body isn't valid JSON | The request body isn't valid JSON: Expected property name or '}' in JSON at position 1 (line 1 column 2) |
| Body is empty | The request body is empty. Send a JSON object with Content-Type: application/json. |
page or limit isn't a whole number | limit must be a whole number from 1 to 100. |
| Date that isn't ISO 8601 | startDate must be an ISO 8601 date or date-time, such as 2026-09-01 or 2026-09-01T00:00:00Z. |
startDate after endDate | startDate must be before endDate. |
| Unknown alert severity | severity must be info, warning or critical. |
customerId filter that's neither an ID nor none | customerId must be a customer ID, or none for domains without a customer. |
A limit above 100 or a page of 0 isn't an error: the value is clamped to the allowed range.
Rate Limiting Errors
| Code | Status | Description |
|---|---|---|
rate_limited | 429 | Too many requests |
Rate limit errors include a Retry-After header with seconds to wait.
Handling Errors
Python Example
python
import requests
def api_request(url, headers):
response = requests.get(url, headers=headers)
if response.ok:
return response.json()["data"]
error = response.json()["error"]
if response.status_code == 401:
raise AuthenticationError(error["message"])
elif response.status_code == 403:
raise PermissionError(error["message"])
elif response.status_code == 404:
raise NotFoundError(error["message"])
elif response.status_code == 429:
retry_after = response.headers.get("Retry-After", 60)
raise RateLimitError(f"Rate limited. Retry after {retry_after}s")
else:
raise APIError(error["message"])Node.js Example
javascript
async function apiRequest(url, headers) {
const response = await fetch(url, { headers });
const body = await response.json();
if (response.ok) {
return body.data;
}
const { code, message, details } = body.error;
switch (response.status) {
case 401:
throw new AuthenticationError(message);
case 403:
throw new PermissionError(message);
case 404:
throw new NotFoundError(message);
case 429: {
const retryAfter = response.headers.get('Retry-After');
throw new RateLimitError(message, retryAfter);
}
case 400:
throw new ValidationError(message, details);
default:
throw new APIError(message);
}
}Debugging Tips
Check the error code: it tells you what kind of problem it is
Read the message: it says what went wrong and usually what to do next
Look at details: for validation errors, check every field that failed
Verify your token: most 401 errors are token-related
Check the token's settings: a 403 can mean the request came from an IP outside the token's allowlist, or that a read-only token tried to change something
Review rate limits: 429 means you're making too many requests