Advanced Patterns
Error Handling and Retry Patterns
Production-grade error handling including retry strategies, balance management, and graceful degradation.
Updated March 18, 2026
Error response format
All SOAPNoteAPI errors follow a consistent JSON structure. Every error response includes an error object with a machine-readable code and a human-readable message. Some errors include a details array with field-level validation information.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request.",
"details": [
{ "field": "transcript", "message": "transcript (or shorthand_notes) is required" },
{ "field": "specialty", "message": "specialty (or provider_type) is required" }
]
}
}- error.code -- Machine-readable error code. Use this in your error handling logic, not the HTTP status code.
- error.message -- Human-readable description. Safe to display to developers but not end users (it may reference API field names).
- error.details -- Optional array of field-level errors. Present on VALIDATION_ERROR responses to indicate which fields failed validation.
HTTP status code reference
Client errors (4xx)
- 400 VALIDATION_ERROR -- The request body is invalid. Missing required fields (transcript, specialty), malformed JSON, or field values exceed limits. Check the details array for specific field errors. Not retryable -- fix the request.
- 401 AUTHENTICATION_ERROR -- The API key is missing, invalid, or expired. Verify your Authorization header format: "Bearer snapi_sk_...". Not retryable without a valid key.
- 403 AUTHORIZATION_ERROR -- The API key is valid but has been revoked, or you are trying to access a resource you do not own. Check your key status in the dashboard. Not retryable.
- 404 NOT_FOUND -- The requested resource does not exist. Common causes: note ID does not exist, note has expired past its retention window. Not retryable.
- 405 METHOD_NOT_ALLOWED -- The HTTP method is not supported for this endpoint (e.g., GET on a POST-only endpoint). Not retryable -- use the correct method.
- 409 CONFLICT -- The request conflicts with the current state (e.g., creating a duplicate resource). Not retryable without resolving the conflict.
- 429 INSUFFICIENT_BALANCE -- Insufficient account balance to process this request. Add funds at your dashboard before retrying. Not retryable without adding funds.
Server errors (5xx)
- 500 INTERNAL_ERROR -- An unexpected error occurred on the SOAPNoteAPI side. Retryable with backoff. If persistent, contact support.
- 503 SERVICE_UNAVAILABLE -- The service is temporarily unavailable (maintenance, upstream dependency failure). Retryable with backoff.
Retryable vs. non-retryable errors
Not all errors should be retried. Retrying a 400 VALIDATION_ERROR with the same payload will always fail. Only retry errors that are transient.
- Retryable: 500 (INTERNAL_ERROR), 503 (SERVICE_UNAVAILABLE), network errors, timeouts.
- Not retryable: 400 (VALIDATION_ERROR), 401 (AUTHENTICATION_ERROR), 403 (AUTHORIZATION_ERROR), 404 (NOT_FOUND), 405 (METHOD_NOT_ALLOWED), 409 (CONFLICT), 429 (INSUFFICIENT_BALANCE — add funds first).
Retry strategy with exponential backoff
For retryable errors, use exponential backoff with jitter. Start with a 1-second delay, double it on each retry, and add random jitter to prevent thundering herd. Cap retries at 3-5 attempts.
JavaScript
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Non-retryable client errors (4xx): return immediately
if (response.status >= 400 && response.status < 500) {
return response;
}
// Success: return
if (response.ok) {
return response;
}
// Retryable errors (5xx): backoff and retry
if (attempt < maxRetries) {
const baseDelay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
const jitter = Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, baseDelay + jitter));
continue;
}
return response; // Max retries exhausted
} catch (error) {
// Network errors: retry
if (attempt < maxRetries) {
const baseDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, baseDelay + jitter));
continue;
}
throw error;
}
}
}
// Usage
const response = await fetchWithRetry(
"https://api.soapnoteapi.com/v1/note",
{
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
transcript: "Patient presents with...",
specialty: "nurse_practitioner",
}),
}
);Python
import time
import random
import requests
def fetch_with_retry(url, method="POST", headers=None, json=None, max_retries=3):
for attempt in range(max_retries + 1):
try:
response = requests.request(method, url, headers=headers, json=json, timeout=30)
# Non-retryable client errors (4xx)
if 400 <= response.status_code < 500:
return response
# Success
if response.ok:
return response
# Retryable (5xx): backoff
if attempt < max_retries:
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
continue
return response
except requests.exceptions.RequestException:
if attempt < max_retries:
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
continue
raise
# Usage
response = fetch_with_retry(
"https://api.soapnoteapi.com/v1/note",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"transcript": "Patient presents with...",
"specialty": "nurse_practitioner",
},
)Handling validation errors
Validation errors include a details array that tells you exactly which fields failed and why. Use this to provide specific feedback in your application.
const response = await fetch("https://api.soapnoteapi.com/v1/note", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ specialty: "nurse_practitioner" }), // Missing transcript
});
if (!response.ok) {
const error = await response.json();
if (error.error.code === "VALIDATION_ERROR") {
// Show specific field errors to the developer / log them
for (const detail of error.error.details || []) {
console.error(`Field "${detail.field}": ${detail.message}`);
}
// Output: Field "transcript": transcript (or shorthand_notes) is required
}
}Circuit breaker pattern
For high-volume integrations, implement a circuit breaker to stop sending requests when SOAPNoteAPI is experiencing issues. This prevents cascading failures in your system and reduces unnecessary load on the API.
class CircuitBreaker {
constructor(failureThreshold = 5, resetTimeMs = 30000) {
this.failures = 0;
this.threshold = failureThreshold;
this.resetTime = resetTimeMs;
this.state = "closed"; // closed = normal, open = failing, half-open = testing
this.openedAt = null;
}
async execute(fn) {
if (this.state === "open") {
// Check if reset time has elapsed
if (Date.now() - this.openedAt > this.resetTime) {
this.state = "half-open";
} else {
throw new Error("Circuit breaker is open. SOAPNoteAPI may be experiencing issues.");
}
}
try {
const result = await fn();
// Success: reset failures
this.failures = 0;
this.state = "closed";
return result;
} catch (error) {
this.failures++;
if (this.failures >= this.threshold) {
this.state = "open";
this.openedAt = Date.now();
}
throw error;
}
}
}
// Usage
const breaker = new CircuitBreaker(5, 30000);
try {
const note = await breaker.execute(() =>
fetchWithRetry("https://api.soapnoteapi.com/v1/note", requestOptions)
);
} catch (error) {
// Circuit is open: show fallback UI
showMessage("Note generation is temporarily unavailable. Your transcript has been saved and will be processed when the service recovers.");
}Graceful degradation
When SOAPNoteAPI is unavailable, your application should degrade gracefully rather than crash. Here are recommended fallback patterns:
- Queue and retry -- Save the transcript locally and retry when the API is available. This is the best pattern for non-real-time workflows.
- Show a clear status -- Tell the provider "Note generation is temporarily unavailable" rather than showing a generic error or loading forever.
- Provide manual fallback -- Let the provider type their note manually if the API is down. Their workflow should never be blocked.
- Log for debugging -- Log the error code, message, and request ID (if available) to help with troubleshooting.
Common error scenarios
- Empty transcript: 400 VALIDATION_ERROR with details: [{ "field": "transcript", "message": "transcript (or shorthand_notes) is required" }]. Send at least one character of clinical text.
- Invalid specialty: 400 VALIDATION_ERROR. Use GET /v1/specialties to get the current list of valid values.
- Expired API key: 401 AUTHENTICATION_ERROR. Create a new key in the dashboard.
- Revoked API key: 403 AUTHORIZATION_ERROR. The key was explicitly revoked. Create a new one.
- Note expired: 404 NOT_FOUND when fetching a note past its retention window. Store notes in your own system after generation.
- Audio processing failure: The GET /v1/audio/status/:noteId endpoint returns status: "failed". Common causes: corrupted file, audio too short (< 5 seconds), no speech detected.
Need help? Contact support@soapnoteapi.com