Skip to main content
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 CodeMeaningWhen It Occurs
200 OKSuccessRequest completed successfully
201 CreatedResource createdNew transaction, quote, or customer created
202 AcceptedAccepted for processingAsync operation initiated (e.g., bulk CSV upload)
400 Bad RequestInvalid inputMissing required fields or invalid parameters
401 UnauthorizedAuthentication failedInvalid or missing API credentials
403 ForbiddenPermission deniedInsufficient permissions or customer not ready
404 Not FoundResource not foundCustomer, transaction, or quote doesn’t exist
409 ConflictResource conflictQuote already executed, external account already exists
412 Precondition FailedUMA version mismatchCounterparty doesn’t support required UMA version
422 Unprocessable EntityMissing infoAdditional counterparty information required
424 Failed DependencyCounterparty issueProblem with external UMA provider
500 Internal Server ErrorServer errorUnexpected server issue (contact support)
501 Not ImplementedNot implementedFeature not yet supported

API error responses

All error responses include a structured format:
{
  "status": 400,
  "code": "INVALID_AMOUNT",
  "message": "Amount must be greater than 0",
  "details": {
    "field": "amount",
    "value": -100
  }
}

Common error codes

Cause: Missing required fields or invalid data formatSolution: Check request parameters match API specification
// Error
{
  "status": 400,
  "code": "INVALID_INPUT",
  "message": "Invalid account ID format"
}

// Fix: Ensure proper ID format
const accountId = "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965";
Cause: Attempting to execute an expired quoteSolution: Create a new quote before executing
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;
  }
}
Cause: Internal account doesn’t have enough fundsSolution: Check balance before initiating transfer
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 createTransferOut({ accountId, amount });
}
Cause: Bank account details are invalid or incompleteSolution: Validate account details before submission
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;
}

Transaction failure reasons

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

Outgoing payment failures

{
  "id": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
  "status": "FAILED",
  "type": "OUTGOING",
  "failureReason": "QUOTE_EXECUTION_FAILED"
}
Common outgoing failure reasons:
  • QUOTE_EXPIRED - Quote expired before execution
  • QUOTE_EXECUTION_FAILED - Error executing the quote
  • FUNDING_AMOUNT_MISMATCH - Funding amount doesn’t match expected amount
  • TIMEOUT - Transaction timed out

Incoming payment failures

{
  "id": "Transaction:019542f5-b3e7-1d02-0000-000000000005",
  "status": "FAILED",
  "type": "INCOMING",
  "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

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

async function createQuoteWithRetry(params, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await createQuote(params);
    } catch (error) {
      const isRetryable =
        error.status === 500 ||
        error.status === 424 ||
        error.code === "QUOTE_REQUEST_FAILED";

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

      // Exponential backoff
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

Handle webhook failures

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

  • Quote Expiration
  • Insufficient Balance
  • Network Errors
Automatically create a new quote when one expires:
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;
  }
}

User-friendly error messages

Convert technical errors to user-friendly messages:
function getUserFriendlyMessage(error) {
  const errorMessages = {
    QUOTE_EXPIRED: "Exchange rate expired. 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.",
    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.",
  };

  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:
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

Validate data on your side before making API requests to catch errors early:
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;
}
Store transaction IDs to prevent duplicate submissions on retry:
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 createTransferOut(params);
    processedTransactions.add(idempotencyKey);
    return result;
  } catch (error) {
    // Don't mark as processed on error
    throw error;
  }
}
Configure timeouts for long-running operations:
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

I