Documentation

Error Handling

Error format, HTTP status codes, and retry guidance for the SimuTest API.

Error Format

All API errors return a JSON body with a top-level error object containing three fields:

FieldDescription
error.codeMachine-readable snake_case error identifier
error.messageHuman-readable description of the error
error.detailsOptional object with additional context specific to the error type
{
  "error": {
    "code": "url_unreachable",
    "message": "The provided URL could not be reached by our test runners.",
    "details": {
      "url": "https://example.com/checkout",
      "http_status": 503,
      "retried": true
    }
  }
}

HTTP Status Codes

StatusMeaningWhen it occurs
400Bad RequestMissing required fields, invalid parameter values, or malformed JSON
401UnauthorizedMissing, invalid, or revoked API key
403ForbiddenValid key but insufficient scope for the requested operation
404Not FoundRequested resource does not exist or is not accessible with your key
409ConflictResource already exists or is in a conflicting state (e.g., test already running)
422UnprocessableRequest is well-formed but semantically invalid (e.g., URL is unreachable)
429Too Many RequestsRate limit or session quota exceeded; check Retry-After header
500Server ErrorUnexpected internal error; safe to retry with backoff
503Service UnavailableAPI temporarily unavailable; retry after the indicated delay

Common Errors

Error CodeStatusDescription
url_unreachable422Test runner could not load the target URL; check the URL is publicly accessible
invalid_auth_config422Authentication configuration is invalid or credentials are incorrect
session_timeout422A session exceeded the maximum allowed duration; page may be unresponsive
quota_exceeded429Monthly session quota exhausted; upgrade plan or wait for quota reset
rate_limited429API request rate limit exceeded; retry after the value in Retry-After
invalid_api_key401API key is missing, malformed, or has been revoked
insufficient_scope403Key does not have the required scope for this operation
test_not_found404Test ID does not exist or belongs to a different organization

SDK Exception Mapping

The Node.js and Python SDKs map API errors to typed exceptions for easier handling:

ExceptionHTTP StatusTrigger
AuthError401, 403Invalid key or insufficient scope
NotFoundError404Resource not found
ValidationError400, 422Invalid request parameters
RateLimitError429 (rate)Request rate limit exceeded; .retryAfter gives seconds to wait
QuotaError429 (quota)Monthly session quota exhausted
ServerError500, 503Internal server error; retryable
SimuTestErrorAnyBase class for all SDK errors
import { SimuTest, SimuTestError, AuthError, QuotaError, RateLimitError } from '@simutest/node';

const client = new SimuTest({ apiKey: process.env.SIMUTEST_API_KEY });

try {
  const test = await client.tests.create({
    url: 'https://example.com',
    task: 'Find the pricing page',
    sessions: 100,
  });
} catch (err) {
  if (err instanceof AuthError) {
    // Invalid or expired API key — check your key
    console.error('Authentication failed:', err.message);
  } else if (err instanceof QuotaError) {
    // Monthly session quota exceeded
    console.error('Quota exceeded:', err.details.quota_resets_at);
  } else if (err instanceof RateLimitError) {
    // Too many requests — retry after the indicated delay
    const retryAfter = err.retryAfter; // seconds
    await sleep(retryAfter * 1000);
  } else if (err instanceof SimuTestError) {
    // All other API errors
    console.error(`[${err.code}] ${err.message}`);
  } else {
    throw err; // re-throw unexpected errors
  }
}

Retry Guidance

Not all errors should be retried. Use the following table to decide:

Status / ErrorRetryable?Notes
400, 422NoFix the request payload
401, 403NoCheck API key and scopes
404NoResource does not exist
429 (rate)YesWait for Retry-After seconds
429 (quota)NoUpgrade plan or wait for quota reset
500YesRetry with exponential backoff, max 3 attempts
503YesWait for Retry-After then retry
async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 3): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const isRetryable =
        err instanceof RateLimitError ||
        (err instanceof SimuTestError && err.retryable);

      if (!isRetryable || attempt === maxAttempts) throw err;

      const delay = err instanceof RateLimitError
        ? err.retryAfter * 1000
        : Math.min(1000 * 2 ** (attempt - 1), 10000);

      await sleep(delay);
    }
  }
  throw new Error('Unreachable');
}