> ## Documentation Index
> Fetch the complete documentation index at: https://agents-docs.lightspark.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Handle payment failures, API errors, and transaction issues gracefully

Learn how to handle errors when working with payments and transactions in Grid. Proper error handling ensures a smooth user experience and helps you quickly identify and resolve issues.

## HTTP status codes

Grid uses standard HTTP status codes to indicate the success or failure of requests:

| Status Code | Meaning | When It Occurs |
| - | - | - |
| `200 OK` | Success | Request completed successfully |
| `201 Created` | Resource created | New customer, account, or credential created |
| `202 Accepted` | Accepted for processing | Async operation initiated (e.g., bulk CSV upload) |
| `400 Bad Request` | Invalid input | Missing required fields or invalid parameters |
| `401 Unauthorized` | Authentication failed | Invalid or missing API credentials |
| `403 Forbidden` | Permission denied | Insufficient permissions or customer not ready |
| `404 Not Found` | Resource not found | Customer, transaction, or quote doesn't exist |
| `408 Request Timeout` | Request timed out | The request body could not be read in time; retry |
| `409 Conflict` | Resource conflict | Quote already executed, external account already exists, bank account still being validated |
| `412 Precondition Failed` | UMA version mismatch | Counterparty doesn't support required UMA version |
| `429 Too Many Requests` | Rate/volume limited | Rate limit hit or daily volume limit exceeded |
| `422 Unprocessable Entity` | Missing info | Additional counterparty information required |
| `424 Failed Dependency` | Counterparty issue | Problem with external UMA provider |
| `500 Internal Server Error` | Server error | Unexpected server issue ([contact our team](https://www.lightspark.com/contact)) |
| `501 Not Implemented` | Not implemented | Feature not yet supported |
| `502 Bad Gateway` | Counterparty error | Counterparty returned an error or an invalid response; retry |
| `504 Gateway Timeout` | Counterparty timeout | Counterparty could not be reached or timed out; retry |

## API error responses

All error responses include a structured format:

```json theme={null}
{
  "code": "INVALID_AMOUNT",
  "reason": "Amount must be greater than 0",
  "details": {
    "field": "amount",
    "value": -100
  }
}
```

### Common error codes

> [accordion] INVALID\_INPUT
>
> **Cause:** Missing required fields or invalid data format
>
> **Solution:** Check request parameters match API specification
>
> ```javascript theme={null}
> // Error
> {
>   "code": "INVALID_INPUT",
>   "reason": "Invalid account ID format"
> }
>
> // Fix: Ensure proper ID format
> const accountId = "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965";
> ```

> [accordion] QUOTE\_EXPIRED
>
> **Cause:** Attempting to execute an expired quote
>
> **Solution:** Create a new quote before executing
>
> ```javascript theme={null}
> async function executeQuoteWithRetry(quoteId) {
>   try {
>     return await executeQuote(quoteId);
>   } catch (error) {
>     if (error.code === "QUOTE_EXPIRED") {
>       // Create new quote and execute
>       const newQuote = await createQuote(originalQuoteParams);
>       return await executeQuote(newQuote.id);
>     }
>     throw error;
>   }
> }
> ```

> [accordion] INSUFFICIENT\_BALANCE
>
> **Cause:** Internal account doesn't have enough funds
>
> **Solution:** Check balance before initiating transfer
>
> ```javascript theme={null}
> async function safeSendPayment(accountId, amount) {
>   const account = await getInternalAccount(accountId);
>
>   if (account.balance.amount < amount) {
>     throw new Error(
>       `Insufficient balance. Available: ${account.balance.amount}, Required: ${amount}`
>     );
>   }
>
>   return await createQuote({ accountId, amount });
> }
> ```

> [accordion] BANK\_ACCOUNT\_VALIDATION\_PENDING
>
> **Cause:** Grid is still validating the US bank account on this request
>
> **Solution:** Wait and retry. Grid validates a newly added ACH account by sending a micro-entry to the bank and waiting out the return window, which takes a few banking days. The account has not been rejected, so do not ask the customer to re-enter their details.
>
> ```javascript theme={null}
> async function createQuoteWhenAccountIsReady({ accountId, amount }) {
>   try {
>     return await createQuote({ accountId, amount });
>   } catch (error) {
>     if (error.code === "BANK_ACCOUNT_VALIDATION_PENDING") {
>       // Surface the wait to the customer; the account is fine.
>       return { status: "AWAITING_ACCOUNT_VALIDATION" };
>     }
>     throw error;
>   }
> }
> ```

> [accordion] INVALID\_BANK\_ACCOUNT
>
> **Cause:** Bank account details are invalid or incomplete
>
> **Solution:** Validate account details before submission
>
> ```javascript theme={null}
> function validateUSAccount(account) {
>   if (!account.accountNumber || !account.routingNumber) {
>     throw new Error("Account and routing numbers required");
>   }
>
>   if (account.routingNumber.length !== 9) {
>     throw new Error("Routing number must be 9 digits");
>   }
>
>   return true;
> }
> ```

> [accordion] TRANSACTION\_SIZE\_LIMIT\_EXCEEDED
>
> **Cause:** The requested amount exceeds the maximum single-transaction limit for the trade corridor or withdrawal currency
>
> **Solution:** Reduce the transaction amount or split into multiple smaller transactions
>
> ```javascript theme={null}
> async function handleLargeTransfer(amount, maxPerTransaction) {
>   if (amount <= maxPerTransaction) {
>     return await createQuote({ amount });
>   }
>
>   // Split into multiple transactions
>   const batches = [];
>   let remaining = amount;
>
>   while (remaining > 0) {
>     const batchAmount = Math.min(remaining, maxPerTransaction);
>     batches.push(batchAmount);
>     remaining -= batchAmount;
>   }
>
>   return Promise.all(batches.map((amt) => createQuote({ amount: amt })));
> }
> ```

> [accordion] DAILY\_VOLUME\_LIMIT\_EXCEEDED
>
> **Cause:** The requested amount would exceed the daily volume limit for the trade corridor or withdrawal currency (HTTP 429)
>
> **Solution:** Wait until the next day or reduce the transaction amount
>
> ```javascript theme={null}
> async function createQuoteWithLimitCheck(params) {
>   try {
>     return await createQuote(params);
>   } catch (error) {
>     if (error.code === "DAILY_VOLUME_LIMIT_EXCEEDED") {
>       // Notify user about daily limit
>       return {
>         error: "DAILY_VOLUME_LIMIT_EXCEEDED",
>         message: "Daily transfer limit reached. Try again tomorrow.",
>         retryAfter: getNextDayMidnight(),
>       };
>     }
>     throw error;
>   }
> }
>
> function getNextDayMidnight() {
>   const tomorrow = new Date();
>   tomorrow.setDate(tomorrow.getDate() + 1);
>   tomorrow.setHours(0, 0, 0, 0);
>   return tomorrow.toISOString();
> }
> ```

## Transaction failure reasons

When a transaction fails, the `failureReason` field provides specific details:

### Outgoing payment failures

```json theme={null}
{
  "id": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
  "status": "FAILED",
  "type": "OUTGOING",
  "direction": "DEBIT",
  "failureReason": "QUOTE_EXECUTION_FAILED"
}
```

**Common outgoing failure reasons:**

* `QUOTE_EXPIRED` - Quote expired before execution
* `QUOTE_RATE_UNAVAILABLE` - Quoted exchange rate was refused at execution; nothing was exchanged
* `QUOTE_EXECUTION_FAILED` - Error executing the quote; a debited amount is refunded automatically
* `FUNDING_AMOUNT_MISMATCH` - Funding amount doesn't match expected amount
* `PAYOUT_RETURNED` - Receiving bank returned or reversed the payout
* `LIMIT_EXCEEDED` - Payout exceeds a partner limit
* `ACCOUNT_CANNOT_RECEIVE` - Recipient account can't accept the payment
* `ACCOUNT_INVALID` - Recipient account details are wrong or not found
* `COMPLIANCE_REJECTED` - Payout partner rejected on compliance grounds

### Incoming payment failures

```json theme={null}
{
  "id": "Transaction:019542f5-b3e7-1d02-0000-000000000005",
  "status": "FAILED",
  "type": "INCOMING",
  "direction": "CREDIT",
  "failureReason": "PAYMENT_APPROVAL_TIMED_OUT"
}
```

**Common incoming failure reasons:**

* `PAYMENT_APPROVAL_TIMED_OUT` - Webhook approval not received within 5 seconds
* `PAYMENT_APPROVAL_WEBHOOK_ERROR` - Webhook returned an error
* `OFFRAMP_FAILED` - Failed to convert and send funds to destination
* `QUOTE_EXPIRED` - Quote expired during processing

## Handling failures

### Monitor transaction status

```javascript theme={null}
async function monitorTransaction(transactionId) {
  const maxAttempts = 30; // 5 minutes with 10-second intervals
  let attempts = 0;

  while (attempts < maxAttempts) {
    const transaction = await getTransaction(transactionId);

    if (transaction.status === "COMPLETED") {
      return { success: true, transaction };
    }

    if (transaction.status === "FAILED") {
      return {
        success: false,
        transaction,
        failureReason: transaction.failureReason,
      };
    }

    // Still processing
    await new Promise((resolve) => setTimeout(resolve, 10000));
    attempts++;
  }

  throw new Error("Transaction monitoring timed out");
}
```

### Retry logic for transient errors

```javascript theme={null}
async function createQuoteWithRetry(params, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await createQuote(params);
    } catch (error) {
      // Rate limiting (429 with RATE_LIMITED) is retryable with backoff
      // Volume limits (DAILY_VOLUME_LIMIT_EXCEEDED) are NOT retryable
      const isRateLimited =
        error.status === 429 && error.code === "RATE_LIMITED";
      // 424 and 5xx are transient, except 501 Not Implemented
      const isRetryable =
        error.status === 424 ||
        (error.status >= 500 && error.status !== 501) ||
        isRateLimited;

      if (!isRetryable || attempt === maxRetries) {
        throw error;
      }

      // Use Retry-After header if present, otherwise exponential backoff
      const retryAfter = error.headers?.["retry-after"];
      const delay = retryAfter
        ? parseInt(retryAfter, 10) * 1000
        : Math.pow(2, attempt) * 1000;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}
```

### Handle webhook failures

```javascript theme={null}
app.post("/webhooks/grid", async (req, res) => {
  try {
    await processWebhook(req.body);
    res.status(200).json({ received: true });
  } catch (error) {
    console.error("Webhook processing error:", error);

    // For pending payments, default to async processing
    if (req.body.transaction?.status === "PENDING") {
      // Queue for retry
      await queueWebhookForRetry(req.body);
      return res.status(202).json({ message: "Queued for processing" });
    }

    // For other webhooks, acknowledge receipt
    res.status(200).json({ received: true });
  }
});
```

## Error recovery strategies

> [tab] Quote Expiration
>
> Automatically create a new quote when one expires:
>
> ```javascript theme={null}
> async function executeQuoteSafe(quoteId, originalParams) {
>   try {
>     return await fetch(
>       `https://api.lightspark.com/grid/2025-10-13/quotes/${quoteId}/execute`,
>       {
>         method: "POST",
>         headers: { Authorization: `Basic ${credentials}` },
>       }
>     );
>   } catch (error) {
>     if (error.status === 409 && error.code === "QUOTE_EXPIRED") {
>       // Create new quote with same parameters
>       const newQuote = await createQuote(originalParams);
>
>       // Execute immediately
>       return await fetch(
>         `https://api.lightspark.com/grid/2025-10-13/quotes/${newQuote.id}/execute`,
>         {
>           method: "POST",
>           headers: { Authorization: `Basic ${credentials}` },
>         }
>       );
>     }
>
>     throw error;
>   }
> }
> ```

> [tab] Insufficient Balance
>
> Notify users and suggest funding:
>
> ```javascript theme={null}
> async function handleInsufficientBalance(customerId, requiredAmount) {
>   const accounts = await getInternalAccounts(customerId);
>   const account = accounts.data[0];
>
>   const shortfall = requiredAmount - account.balance.amount;
>
>   // Notify customer
>   await sendNotification(customerId, {
>     type: "INSUFFICIENT_BALANCE",
>     message: `You need ${formatAmount(
>       shortfall
>     )} more to complete this payment`,
>     action: {
>       label: "Add Funds",
>       url: "/deposit",
>     },
>   });
>
>   // Return funding instructions
>   return {
>     error: "INSUFFICIENT_BALANCE",
>     currentBalance: account.balance.amount,
>     requiredAmount,
>     shortfall,
>     fundingInstructions: account.fundingPaymentInstructions,
>   };
> }
> ```

> [tab] Network Errors
>
> Implement retry with exponential backoff:
>
> ```javascript theme={null}
> async function fetchWithRetry(url, options, maxRetries = 3) {
>   for (let i = 0; i < maxRetries; i++) {
>     try {
>       const response = await fetch(url, options);
>
>       if (!response.ok) {
>         const error = await response.json();
>         throw error;
>       }
>
>       return await response.json();
>     } catch (error) {
>       const isLastAttempt = i === maxRetries - 1;
>       const isNetworkError =
>         error.code === "ECONNRESET" ||
>         error.code === "ETIMEDOUT" ||
>         error.status === 500;
>
>       if (isLastAttempt || !isNetworkError) {
>         throw error;
>       }
>
>       // Wait before retry (exponential backoff)
>       const delay = Math.pow(2, i) * 1000;
>       await new Promise((resolve) => setTimeout(resolve, delay));
>     }
>   }
> }
> ```

## User-friendly error messages

Convert technical errors to user-friendly messages:

```javascript theme={null}
function getUserFriendlyMessage(error) {
  const errorMessages = {
    QUOTE_EXPIRED: "Exchange rate expired. Please try again.",
    QUOTE_RATE_UNAVAILABLE: "Exchange rate is no longer available. Please try again.",
    INSUFFICIENT_BALANCE: "You don't have enough funds for this payment.",
    INVALID_BANK_ACCOUNT:
      "Bank account details are invalid. Please check and try again.",
    BANK_ACCOUNT_VALIDATION_PENDING:
      "We're still verifying this bank account. This usually takes a few business days.",
    PAYMENT_APPROVAL_TIMED_OUT: "Payment approval timed out. Please try again.",
    AMOUNT_OUT_OF_RANGE: "Payment amount is too high or too low.",
    INVALID_CURRENCY: "This currency is not supported.",
    WEBHOOK_ENDPOINT_NOT_SET:
      "Payment receiving is not configured. Contact support.",
    TRANSACTION_SIZE_LIMIT_EXCEEDED:
      "This amount exceeds the maximum single transaction limit. Try a smaller amount.",
    DAILY_VOLUME_LIMIT_EXCEEDED:
      "Daily transfer limit reached. Please try again tomorrow.",
  };

  return (
    errorMessages[error.code] ||
    error.message ||
    "An unexpected error occurred. Please try again or contact support."
  );
}

// Usage
try {
  await createQuote(params);
} catch (error) {
  const userMessage = getUserFriendlyMessage(error);
  showErrorToUser(userMessage);
}
```

## Logging and monitoring

Implement comprehensive error logging:

```javascript theme={null}
class PaymentErrorLogger {
  static async logError(error, context) {
    const errorLog = {
      timestamp: new Date().toISOString(),
      errorCode: error.code,
      errorMessage: error.message,
      httpStatus: error.status,
      context: {
        customerId: context.customerId,
        transactionId: context.transactionId,
        operation: context.operation,
      },
      stackTrace: error.stack,
    };

    // Log to your monitoring service
    await logToMonitoring(errorLog);

    // Alert on critical errors
    if (error.status >= 500 || error.code === "WEBHOOK_DELIVERY_ERROR") {
      await sendAlert({
        severity: "high",
        message: `Payment error: ${error.code}`,
        details: errorLog,
      });
    }

    return errorLog;
  }
}

// Usage
try {
  await executeQuote(quoteId);
} catch (error) {
  await PaymentErrorLogger.logError(error, {
    customerId: "Customer:123",
    operation: "executeQuote",
  });
  throw error;
}
```

## Best practices

> [accordion] Always validate before API calls
>
> Validate data on your side before making API requests to catch errors early:
>
> ```javascript theme={null}
> function validateTransferRequest(request) {
>   const errors = [];
>
>   if (!request.source?.accountId) {
>     errors.push("Source account ID is required");
>   }
>
>   if (!request.destination?.accountId) {
>     errors.push("Destination account ID is required");
>   }
>
>   if (!request.amount || request.amount <= 0) {
>     errors.push("Amount must be greater than 0");
>   }
>
>   if (errors.length > 0) {
>     throw new ValidationError(errors.join(", "));
>   }
>
>   return true;
> }
> ```

> [accordion] Implement idempotency
>
> Store transaction IDs to prevent duplicate submissions on retry:
>
> ```javascript theme={null}
> const processedTransactions = new Set();
>
> async function createTransferIdempotent(params) {
>   const idempotencyKey = generateKey(params);
>
>   if (processedTransactions.has(idempotencyKey)) {
>     throw new Error("Transaction already processed");
>   }
>
>   try {
>     const result = await createQuote(params);
>     processedTransactions.add(idempotencyKey);
>     return result;
>   } catch (error) {
>     // Don't mark as processed on error
>     throw error;
>   }
> }
> ```

> [accordion] Set appropriate timeouts
>
> Configure timeouts for long-running operations:
>
> ```javascript theme={null}
> async function executeWithTimeout(promise, timeoutMs = 30000) {
>   const timeout = new Promise((_, reject) =>
>     setTimeout(() => reject(new Error("Operation timed out")), timeoutMs)
>   );
>
>   return Promise.race([promise, timeout]);
> }
>
> // Usage
> try {
>   const result = await executeWithTimeout(
>     executeQuote(quoteId),
>     30000 // 30 seconds
>   );
> } catch (error) {
>   if (error.message === "Operation timed out") {
>     // Handle timeout specifically
>     console.log("Quote execution timed out, checking status...");
>     const transaction = await checkTransactionStatus(quoteId);
>   }
> }
> ```

## Next steps

![](https://agents-docs.lightspark.com/images/icons/paper-plane-top-right.svg)

Send Payments

Learn how to send payments from internal accounts

[Open](https://agents-docs.lightspark.com/payouts-and-b2b/payment-flow/send-payment.md)

![](https://agents-docs.lightspark.com/images/icons/file-text.svg)

List Transactions

Query and filter payment history

[Open](https://agents-docs.lightspark.com/payouts-and-b2b/payment-flow/list-transactions.md)

![](https://agents-docs.lightspark.com/images/icons/receipt-check.svg)

Reconciliation

Match payments with your internal systems

[Open](https://agents-docs.lightspark.com/payouts-and-b2b/payment-flow/reconciliation.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.