Error handling
Error handling best practices
How to parse, handle, and recover from errors across the Mercury API. Build integrations that are resilient by default.
HTTP status codes
The Mercury API uses a small, consistent set of status codes across all endpoints:
Error response format
All error responses follow the same structure, making it simple to build a single error handler for your integration.
{
"status": "Error",
"message": "Request validation failed",
"errors": [
{
"propertyName": "externalReference",
"errorDescription": "External reference is required"
},
{
"propertyName": "associates",
"errorDescription": "At least one associate is required"
}
]
}"Internal server error"Always return a structured JSON object with a status, message, and an errors array. Each entry tells you the field name and what went wrong. Multiple validation failures are returned in a single response.
Return a plain string. These are transient server-side issues. Do not parse for details. Log the response, wait, and retry.
Parsing validation errors
Build your error handler to iterate over the errors array. Each object contains exactly two fields:
import requests
response = requests.post(f"{BASE_URL}/api/v1/client", headers=headers, data=body)
if response.status_code == 200:
client = response.json()
print(f"Client created: {client['Response']['Identifier']}")
elif response.status_code == 400:
error_data = response.json()
print(f"Validation failed: {error_data['message']}")
for err in error_data.get("errors", []):
print(f" - {err['propertyName']}: {err['errorDescription']}")
elif response.status_code == 401:
print("Authentication failed. Check API key, nonce, and signature.")
elif response.status_code >= 500:
print(f"Server error ({response.status_code}). Retry with backoff.")const response = await fetch(`${BASE_URL}/api/v1/client`, {
method: "POST",
headers,
body,
});
if (response.ok) {
const { Response: client } = await response.json();
console.log(`Client created: ${client.Identifier}`);
} else if (response.status === 400) {
const { message, errors } = await response.json();
console.error(`Validation failed: ${message}`);
errors.forEach((e) =>
console.error(` - ${e.propertyName}: ${e.errorDescription}`)
);
} else if (response.status === 401) {
console.error("Authentication failed. Check API key, nonce, and signature.");
} else if (response.status >= 500) {
console.error(`Server error (${response.status}). Retry with backoff.`);
}var response = await client.PostAsync($"{baseUrl}/api/v1/client", content);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<ClientResponse>();
Console.WriteLine($"Client created: {result.Response.Identifier}");
}
else if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
Console.WriteLine($"Validation failed: {error.Message}");
foreach (var e in error.Errors)
Console.WriteLine($" - {e.PropertyName}: {e.ErrorDescription}");
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
Console.WriteLine("Authentication failed. Check API key, nonce, and signature.");
}
else
{
Console.WriteLine($"Server error ({(int)response.StatusCode}). Retry with backoff.");
}Common validation errors
These are the most frequent 400 errors and how to resolve them:
externalReference missing or already in useassociates empty (at least one required)dateOfBirth under 18 years oldcountryCode not a valid ISO 3166-1 alpha-2 codeindustryCode not in lookup tableclientReference does not exist or is not Activeamount exceeds GBP 1,000,000 limitendToEndIdentifier already submittedsortCode not 6 digitsiban fails mod-97 checksumswiftCode invalid formatAuthentication errors (401)
A 401 means your request signature did not match what the server computed. Walk through these checks in order:
Use the helper tool
Enter identical inputs in the Helper Tool and compare the canonical string and signature character-by-character.
Async errors via webhooks
Some errors only surface after the initial 200 response. Payments are processed asynchronously, so you must listen for error webhooks to catch failures:
A 200 on a payment request does not mean the payment succeeded. It means the request was accepted for processing. Always implement webhook listeners for the settlement and error events above.
Retry strategy
For 500 errors, use exponential backoff with jitter. Here is a recommended pattern:
import time
import random
def call_with_retry(fn, max_retries=3):
for attempt in range(max_retries + 1):
response = fn()
if response.status_code == 200:
return response.json()
if response.status_code == 400:
raise ValidationError(response.json())
if response.status_code == 401:
raise AuthError("Check API key, nonce, and signature")
if response.status_code >= 500 and attempt < max_retries:
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"Server error. Retrying in {wait:.1f}s (attempt {attempt + 1}/{max_retries})")
time.sleep(wait)
continue
raise ServerError(f"Failed after {max_retries + 1} attempts")async function callWithRetry(fn, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fn();
if (response.ok) return response.json();
if (response.status === 400) {
const error = await response.json();
throw new ValidationError(error);
}
if (response.status === 401) {
throw new AuthError("Check API key, nonce, and signature");
}
if (response.status >= 500 && attempt < maxRetries) {
const wait = 2 ** attempt + Math.random();
console.log(`Server error. Retrying in ${wait.toFixed(1)}s (attempt ${attempt + 1}/${maxRetries})`);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
throw new ServerError(`Failed after ${maxRetries + 1} attempts`);
}
}Best practices checklist
Updated about 2 hours ago
