SOAPNoteAPI

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.

JSON
{
  "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

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

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.

JavaScript
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.

JavaScript
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