Skip to main content
Send payments from your customers’ internal accounts to their external bank accounts or to other destinations. Grid supports both same-currency transfers and cross-currency transfers with automatic exchange rate handling.

Overview

Grid provides two payment methods depending on your use case:

Prerequisites

Before sending payments, ensure you have:
  • An active internal account with sufficient balance
  • A verified external account for the destination
  • Valid API credentials with appropriate permissions
  • A webhook endpoint configured to receive payment status updates (recommended)
If you don’t have these set up yet, review the Internal Accounts and External Accounts guides first.

Same-Currency Transfers

Use the /transfer-out endpoint when sending funds in the same currency (no exchange rate needed). This is the simplest and fastest option for domestic transfers.

When to use same-currency transfers

  • Transferring USD from a USD internal account to a USD external account
  • Sending funds within the same country using the same payment rail
  • No currency conversion is required

Create a transfer

1

Get account IDs

Retrieve the internal account (source) and external account (destination) IDs:
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
  -H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
Note the id fields from both the internal and external accounts you want to use.
2

Initiate the transfer

Create the transfer by specifying the source and destination accounts:
cURL
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/transfer-out' \
  -H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
  -H 'Content-Type: application/json' \
  -d '{
    "source": {
      "accountId": "InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123"
    },
    "destination": {
      "accountId": "ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"
    },
    "amount": 12550
  }'
Success (201 Created)
{
  "id": "Transaction:019542f5-b3e7-1d02-0000-000000000015",
  "status": "PENDING",
  "type": "OUTGOING",
  "source": {
    "accountId": "InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
    "currency": "USD"
  },
  "destination": {
    "accountId": "ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
    "currency": "USD"
  },
  "sentAmount": {
    "amount": 12550,
    "currency": {
      "code": "USD",
      "name": "United States Dollar",
      "symbol": "$",
      "decimals": 2
    }
  },
  "receivedAmount": {
    "amount": 12550,
    "currency": {
      "code": "USD",
      "name": "United States Dollar",
      "symbol": "$",
      "decimals": 2
    }
  },
  "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
  "platformCustomerId": "customer_12345",
  "createdAt": "2025-10-03T15:00:00Z",
  "settledAt": null
}
The amount is specified in the smallest unit of the currency (cents for USD, pence for GBP, etc.). For example, 12550 represents $125.50 USD.
3

Track transfer status

The transaction is created with a PENDING status and progresses through PROCESSING to COMPLETED or FAILED. Monitor the status by:You’ll receive OUTGOING_PAYMENT.<STATUS> webhooks as the transaction progresses. The webhook body contains the full transaction resource:
{
  "type": "OUTGOING_PAYMENT.COMPLETED",
  "data": {
    "id": "Transaction:019542f5-b3e7-1d02-0000-000000000015",
    "status": "COMPLETED",
    "type": "OUTGOING",
    "sentAmount": { "amount": 12550, "currency": { "code": "USD", "decimals": 2 } },
    "receivedAmount": { "amount": 12550, "currency": { "code": "USD", "decimals": 2 } },
    "settledAt": "2025-10-03T15:02:30Z"
  },
  "timestamp": "2025-10-03T15:03:00Z"
}
If a transaction fails, Grid initiates a refund automatically. You’ll receive OUTGOING_PAYMENT.REFUND_PENDING followed by OUTGOING_PAYMENT.REFUND_COMPLETED or OUTGOING_PAYMENT.REFUND_FAILED. The transaction’s refund object tracks the refund status and reference.
For the full state diagram, refund object details, and all webhook scenarios (including bank returns and manual cancellations), see the Transaction Lifecycle guide.

Transaction statuses

StatusDescription
PENDINGTransfer initiated and awaiting processing
EXPIREDQuote wasn’t executed before the expiry window
PROCESSINGTransfer in progress through the payment rail
COMPLETEDTransfer successfully completed
FAILEDTransfer failed — accompanied by a failureReason
For the full state diagram including refund tracking and edge cases like bank returns, see the Transaction Lifecycle guide.

Cross-Currency Transfers

Use the quotes flow when sending funds with currency conversion. This locks in an exchange rate and provides all details needed to execute the transfer.

When to use cross-currency transfers

  • Converting USD to EUR, MXN, BRL, or other supported currencies
  • Sending international payments with automatic currency conversion
  • Need to lock in a specific exchange rate for the transfer

Create and execute a quote

1

Create a quote

Request a quote to lock in the exchange rate and get transfer details:
cURL
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
  -H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
  -H 'Content-Type: application/json' \
  -d '{
    "source": {
      "sourceType": "ACCOUNT",
      "accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"
    },
    "destination": {
      "destinationType": "ACCOUNT",
      "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123"
    },
    "lockedCurrencySide": "SENDING",
    "lockedCurrencyAmount": 10000,
    "description": "Payment for services - Invoice #1234"
  }'
Success (201 Created)
{
  "id": "Quote:019542f5-b3e7-1d02-0000-000000000025",
  "status": "PENDING",
  "source": {
    "accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
    "currency": "USD"
  },
  "destination": {
    "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
    "currency": "EUR"
  },
  "sendingAmount": {
    "amount": 10000,
    "currency": {
      "code": "USD",
      "name": "United States Dollar",
      "symbol": "$",
      "decimals": 2
    }
  },
  "receivingAmount": {
    "amount": 9200,
    "currency": {
      "code": "EUR",
      "name": "Euro",
      "symbol": "€",
      "decimals": 2
    }
  },
  "exchangeRate": 0.92,
  "fee": {
    "amount": 50,
    "currency": {
      "code": "USD",
      "symbol": "$",
      "decimals": 2
    }
  },
  "expiresAt": "2025-10-03T15:15:00Z",
  "createdAt": "2025-10-03T15:00:00Z",
  "description": "Payment for services - Invoice #1234"
}
Locked currency side determines which amount is fixed:
  • SENDING: Lock the sending amount (receiving amount calculated based on exchange rate)
  • RECEIVING: Lock the receiving amount (sending amount calculated based on exchange rate)
2

Review quote details

Before executing, review the quote to ensure:
  • Exchange rate is acceptable
  • Fees are as expected
  • Receiving amount meets requirements
  • Quote hasn’t expired (check expiresAt)
Quote expiration depends on the corridor but is typically ~5 minutes or greater. If expired, create a new quote to get an updated exchange rate.
3

Execute the quote

Confirm and execute the quote to initiate the transfer:
cURL
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes/Quote:019542f5-b3e7-1d02-0000-000000000025/execute' \
  -H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
Success (200 OK)
{
  "id": "Quote:019542f5-b3e7-1d02-0000-000000000025",
  "status": "PROCESSING",
  "transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
  "source": {
    "accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
    "currency": "USD"
  },
  "destination": {
    "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
    "currency": "EUR"
  },
  "sendingAmount": {
    "amount": 10000,
    "currency": {
      "code": "USD",
      "symbol": "$",
      "decimals": 2
    }
  },
  "receivingAmount": {
    "amount": 9200,
    "currency": {
      "code": "EUR",
      "symbol": "€",
      "decimals": 2
    }
  },
  "exchangeRate": 0.92,
  "executedAt": "2025-10-03T15:05:00Z"
}
Once executed, the quote creates a transaction and the transfer begins processing. The transactionId can be used to track the payment.
Real-time funding sources: If your quote uses a real-time funding source (USDC, BTC, RTP, or FedNow), you don’t call the execute endpoint. Instead, send a payment to the account specified in the quote’s paymentInstructions. Grid detects the deposit and processes the transfer automatically.
4

Monitor completion

After execution, a transaction is created and progresses through PENDINGPROCESSINGCOMPLETED or FAILED. You’ll receive OUTGOING_PAYMENT.<STATUS> webhooks as the transaction progresses:
{
  "type": "OUTGOING_PAYMENT.COMPLETED",
  "data": {
    "id": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
    "status": "COMPLETED",
    "type": "OUTGOING",
    "sentAmount": {
      "amount": 10000,
      "currency": { "code": "USD", "symbol": "$", "decimals": 2 }
    },
    "receivedAmount": {
      "amount": 9200,
      "currency": { "code": "EUR", "symbol": "€", "decimals": 2 }
    },
    "exchangeRate": 0.92,
    "settledAt": "2025-10-03T15:30:00Z",
    "quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000025"
  },
  "timestamp": "2025-10-03T15:31:00Z"
}
If a transaction fails, Grid initiates a refund automatically. You’ll receive OUTGOING_PAYMENT.REFUND_PENDING followed by OUTGOING_PAYMENT.REFUND_COMPLETED or OUTGOING_PAYMENT.REFUND_FAILED. The transaction’s refund object tracks the refund status and reference.
For the full state diagram, refund object details, and all webhook scenarios (including bank returns and manual cancellations), see the Transaction Lifecycle guide.

Transaction statuses

StatusDescription
PENDINGQuote created, awaiting execution
PROCESSINGQuote executed, transfer in progress
COMPLETEDTransfer successfully completed
FAILEDTransfer failed — refund initiated automatically (track via refund object)
EXPIREDQuote expired without execution

Checking Payment Status

Configure a webhook endpoint to receive real-time notifications when payment status changes:
app.post("/webhooks/grid", (req, res) => {
  const { type, data } = req.body;

  switch (type) {
    case "OUTGOING_PAYMENT.COMPLETED":
      console.log(`Payment ${data.id} completed at ${data.settledAt}`);
      // Update your database, notify customer
      break;

    case "OUTGOING_PAYMENT.FAILED":
      console.log(`Payment ${data.id} failed: ${data.failureReason}`);
      // Handle failure, notify customer — refund webhook follows
      break;

    case "OUTGOING_PAYMENT.PROCESSING":
      console.log(`Payment ${data.id} is processing`);
      // Optional: Update UI to show processing state
      break;

    case "OUTGOING_PAYMENT.REFUND_COMPLETED":
      console.log(`Payment ${data.id} refund completed`);
      // Update your records with refund details
      break;

    case "OUTGOING_PAYMENT.REFUND_FAILED":
      console.log(`Payment ${data.id} refund failed`);
      // Alert your team — may require manual resolution
      break;
  }

  res.status(200).json({ received: true });
});
See the Webhooks guide for complete webhook implementation details including signature verification.

Best Practices

Quote expiration depends on the corridor (typically ~5 minutes or greater). Always check expiration before executing:
async function executeQuoteWithCheck(quoteId) {
  const quote = await getQuote(quoteId);

  if (new Date(quote.expiresAt) < new Date()) {
    // Quote expired, create a new one
    const newQuote = await createQuote({
      source: quote.source,
      destination: quote.destination,
      lockedCurrencySide: quote.lockedCurrencySide,
      lockedCurrencyAmount: quote.lockedCurrencyAmount,
    });

    return executeQuote(newQuote.id);
  }

  return executeQuote(quoteId);
}
Always include meaningful descriptions to help with reconciliation:
const description = [
  `Invoice #${invoiceId}`,
  `Customer: ${customerName}`,
  `Date: ${new Date().toISOString().split("T")[0]}`,
].join(" | ");

await createQuote({
  // ... other fields
  description: description,
});
This makes it easier to match payments in your accounting system and provides context when reviewing transactions.
Always save transaction and quote IDs for audit trails and support:
const quote = await createQuote(quoteData);

// Save to your database immediately
await db.payments.create({
  quoteId: quote.id,
  customerId: customer.id,
  amount: quote.sendingAmount.amount,
  currency: quote.sendingAmount.currency.code,
  status: "pending",
  createdAt: new Date(),
});

const execution = await executeQuote(quote.id);

// Update with transaction ID
await db.payments.update(
  { quoteId: quote.id },
  { transactionId: execution.transactionId, status: "processing" }
);

Next Steps