Skip to main content

Overview

A production-quality integration needs to handle every possible failure case. This guide covers strategies for dealing with API errors, network issues, and edge cases.

Retry strategy

Not all errors should be retried. Here’s how to decide:
Status CodeActionRetry?
400 Bad RequestFix the request payloadNo
401 UnauthorizedCheck your API keyNo
403 ForbiddenCheck permissionsNo
404 Not FoundCheck the resource IDNo
409 ConflictCheck resource stateNo
422 ValidationFix the validation errorNo
429 Rate LimitedWait and retryYes — respect Retry-After
500 Server ErrorRetry with backoffYes
502 Bad GatewayRetry with backoffYes
503 UnavailableRetry with backoffYes

Exponential backoff

For retryable errors, use exponential backoff with jitter:
async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      // Don't retry client errors (except 429)
      if (response.status >= 400 && response.status < 500 && response.status !== 429) {
        return response;
      }

      // Retry rate limits
      if (response.status === 429) {
        const retryAfter = parseInt(response.headers.get("Retry-After") || "5");
        await sleep(retryAfter * 1000);
        continue;
      }

      // Retry server errors
      if (response.status >= 500) {
        if (attempt < maxRetries) {
          await sleep(getBackoff(attempt));
          continue;
        }
      }

      return response;
    } catch (err) {
      // Network error — retry
      if (attempt < maxRetries) {
        await sleep(getBackoff(attempt));
        continue;
      }
      throw err;
    }
  }
}

function getBackoff(attempt) {
  const base = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s, 8s...
  const jitter = Math.random() * 1000;       // 0-1s random jitter
  return base + jitter;
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Payout-specific error handling

Insufficient balance

{
  "error": {
    "code": 400,
    "message": "Insufficient balance: available USD 5000.00, required USD 10000.00"
  }
}
Don’t retry. Instead:
  1. Check your current balance
  2. Wait for pending payouts to complete (releasing held funds)
  3. Fund your account if needed
  4. Then retry the payout

Duplicate idempotency key

{
  "error": {
    "code": 409,
    "message": "Idempotency key already used with different request body"
  }
}
Don’t retry with the same key. This means you reused an idempotency key with a different payload. Generate a new key for the new request.

Invalid state transition

{
  "error": {
    "code": 409,
    "message": "Cannot cancel payout in status 'completed'"
  }
}
Don’t retry. The payout has already progressed past the point where this action is valid. Fetch the current payout status to understand the actual state.

Circuit breaker pattern

If you’re making many API calls, implement a circuit breaker to fail fast when the API is having issues:
class CircuitBreaker {
  constructor(threshold = 5, resetTimeout = 60000) {
    this.failures = 0;
    this.threshold = threshold;
    this.resetTimeout = resetTimeout;
    this.state = "closed"; // closed = normal, open = failing
    this.nextAttempt = 0;
  }

  async call(fn) {
    if (this.state === "open") {
      if (Date.now() < this.nextAttempt) {
        throw new Error("Circuit breaker is open — API unavailable");
      }
      this.state = "half-open";
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure();
      throw err;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = "closed";
  }

  onFailure() {
    this.failures++;
    if (this.failures >= this.threshold) {
      this.state = "open";
      this.nextAttempt = Date.now() + this.resetTimeout;
    }
  }
}

Testing error scenarios

In sandbox, you can trigger specific error conditions to test your error handling:
ScenarioHow to trigger
Insufficient balanceCreate a payout larger than your sandbox balance
Invalid beneficiaryUse a non-existent beneficiary ID
Validation errorOmit required fields or use invalid values
Rate limitingSend many requests rapidly from the same IP