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)
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
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.
Initiate the transfer
Create the transfer by specifying the source and destination accounts: 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
}'
{
"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.
Track transfer status
The transaction is created with a PENDING
status. Monitor the status by: Option 1: Webhook notifications (recommended){
"type" : "OUTGOING_PAYMENT" ,
"transaction" : {
"id" : "Transaction:019542f5-b3e7-1d02-0000-000000000015" ,
"status" : "COMPLETED" ,
"settledAt" : "2025-10-03T15:02:30Z"
},
"timestamp" : "2025-10-03T15:03:00Z"
}
Option 2: Poll the transaction endpoint curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions/Transaction:019542f5-b3e7-1d02-0000-000000000015' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
When the transaction status changes to COMPLETED
, the funds have been successfully transferred to the external account.
Transaction statuses
Status Description PENDING
Transfer initiated and awaiting processing PROCESSING
Transfer in progress through the payment rail COMPLETED
Transfer successfully completed FAILED
Transfer failed (see error details)
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
Create a quote
Request a quote to lock in the exchange rate and get transfer details: 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": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "EUR"
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000,
"description": "Payment for services - Invoice #1234"
}'
{
"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)
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
)
Quotes typically expire after 15 minutes. If expired, create a new quote to get an updated exchange rate.
Execute the quote
Confirm and execute the quote to initiate the transfer: 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'
{
"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.
Monitor completion
Track the transfer using webhooks or by polling the transaction: curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions/Transaction:019542f5-b3e7-1d02-0000-000000000030' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
You’ll receive a webhook when the transaction completes: {
"type" : "OUTGOING_PAYMENT" ,
"transaction" : {
"id" : "Transaction:019542f5-b3e7-1d02-0000-000000000030" ,
"status" : "COMPLETED" ,
"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"
}
Quote statuses
Status Description PENDING
Quote created, awaiting execution PROCESSING
Quote executed, transfer in progress COMPLETED
Transfer successfully completed FAILED
Transfer failed (funds returned to source) EXPIRED
Quote expired without execution
Checking Payment Status
Using webhooks (recommended)
Configure a webhook endpoint to receive real-time notifications when payment status changes:
// Your webhook endpoint
app . post ( "/webhooks/grid" , ( req , res ) => {
const { type , transaction } = req . body ;
if ( type === "OUTGOING_PAYMENT" ) {
const { id , status , settledAt } = transaction ;
switch ( status ) {
case "COMPLETED" :
console . log ( `Payment ${ id } completed at ${ settledAt } ` );
// Update your database, notify customer, etc.
break ;
case "FAILED" :
console . log ( `Payment ${ id } failed` );
// Handle failure, refund, notify customer
break ;
case "PROCESSING" :
console . log ( `Payment ${ id } is processing` );
// Optional: Update UI to show processing state
break ;
}
}
res . status ( 200 ). json ({ received: true });
});
See the Webhooks guide for complete
webhook implementation details including signature verification.
Polling transactions
If webhooks aren’t available, poll the transaction endpoint:
async function checkPaymentStatus ( transactionId ) {
const response = await fetch (
`https://api.lightspark.com/grid/2025-10-13/transactions/ ${ transactionId } ` ,
{
headers: {
Authorization: `Basic ${ credentials } ` ,
},
}
);
const transaction = await response . json ();
return transaction . status ;
}
// Poll every 10 seconds
const pollInterval = setInterval ( async () => {
const status = await checkPaymentStatus (
"Transaction:019542f5-b3e7-1d02-0000-000000000015"
);
if ( status === "COMPLETED" || status === "FAILED" ) {
clearInterval ( pollInterval );
// Handle completion
}
}, 10000 );
Polling can delay status updates and increase API usage. Use webhooks for
production applications whenever possible.
Best Practices
Always check account balance before transfers
Verify the internal account has sufficient balance to avoid failed transfers: async function checkBalance ( accountId , requiredAmount ) {
const response = await fetch (
`https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts` ,
{
headers: { Authorization: `Basic ${ credentials } ` },
}
);
const { data } = await response . json ();
const account = data . find (( a ) => a . id === accountId );
if ( account . balance . amount < requiredAmount ) {
throw new Error ( "Insufficient balance" );
}
return true ;
}
This prevents unnecessary API calls and provides better user feedback.
Use idempotency for quote execution
Store quote IDs in your database to prevent accidental duplicate executions: async function executeQuoteOnce ( quoteId ) {
// Check if already executed
const existing = await db . quotes . findOne ({ quoteId });
if ( existing ?. executed ) {
throw new Error ( "Quote already executed" );
}
// Execute and mark as executed
const result = await executeQuote ( quoteId );
await db . quotes . update (
{ quoteId },
{ executed: true , transactionId: result . transactionId }
);
return result ;
}
Handle quote expiration gracefully
Quotes expire after a short period. 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 );
}
Include descriptive payment references
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.
Store transaction IDs in your system
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