# Create a new API token
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/api-tokens/create-a-new-api-token
openapi.yaml post /tokens
Create a new API token to access the Grid APIs.
# Delete API token by ID
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/api-tokens/delete-api-token-by-id
openapi.yaml delete /tokens/{tokenId}
Delete an API token by their system-generated ID
# Get API token by ID
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/api-tokens/get-api-token-by-id
openapi.yaml get /tokens/{tokenId}
Retrieve an API token by their system-generated ID
# List tokens
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/api-tokens/list-tokens
openapi.yaml get /tokens
Retrieve a list of API tokens with optional filtering parameters. Returns all tokens that match
the specified filters. If no filters are provided, returns all tokens (paginated).
# Authentication
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/authentication
API uses HTTP Basic Authentication with your client ID as the username and your client secret as the password.\
Your API tokens are scoped to either your production or sandbox environment.
```bash theme={null}
curl -u "{client_id}:{client_secret}" https://api.lightspark.com/grid/2025-10-13...
```
If integrating with one of our SDKs, the SDK reads environment variables and will populate the credentials for you.
# List available Counterparty Providers
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/available-uma-providers/list-available-counterparty-providers
openapi.yaml get /uma-providers
Retrieve a list of available Counterparty Providers. The response includes basic information about each provider, such as its UMA address, name, and supported currencies.
# Create a transfer quote
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/cross-currency-transfers/create-a-transfer-quote
openapi.yaml post /quotes
Generate a quote for a cross-currency transfer between any combination of accounts
and UMA addresses. This endpoint handles currency exchange and provides the necessary
instructions to execute the transfer.
**Transfer Types Supported:**
- **Account to Account**: Transfer between internal/external accounts with currency exchange.
- **Account to UMA**: Transfer from an internal account to an UMA address.
- **UMA to Account or UMA to UMA**: This transfer type will only be funded by payment instructions, not from an internal account.
**Key Features:**
- **Flexible Amount Locking**: Always specify whether you want to lock the sending amount or receiving amount
- **Currency Exchange**: Handles all cross-currency transfers with real-time exchange rates
- **Payment Instructions**: For UMA or customer ID sources, provides banking details needed for execution
**Important:** If you are transferring funds in the same currency (no exchange required),
use the `/transfer-in` or `/transfer-out` endpoints instead.
**Sandbox Testing:** When using the `externalAccountDetails` destination type in sandbox mode, use account number patterns ending in specific digits to test different scenarios.
These patterns should be used with the primary alias, address, or identifier of whatever account type you're testing.
For example, the US account number, a CLABE, an IBAN, a spark wallet address, etc. The failure patterns are:
- Account numbers ending in **002**: Insufficient funds (transfer-in will fail)
- Account numbers ending in **003**: Account closed/invalid (transfers will fail)
- Account numbers ending in **004**: Transfer rejected (bank rejects the transfer)
- Account numbers ending in **005**: Timeout/delayed failure (stays pending ~30s, then fails)
- Any other account number: Success (transfers complete normally)
# Execute a quote
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/cross-currency-transfers/execute-a-quote
openapi.yaml post /quotes/{quoteId}/execute
Execute a quote by its ID. This endpoint initiates the transfer between
the source and destination accounts.
This endpoint can only be used for quotes with a `source` which is either an internal account,
or has direct pull functionality (e.g. ACH pull with an external account).
Once executed, the quote cannot be cancelled and the transfer will be processed.
# Get quote by ID
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/cross-currency-transfers/get-quote-by-id
openapi.yaml get /quotes/{quoteId}
Retrieve a quote by its ID. If the quote has been settled, it will include
the transaction ID. This allows clients to track the full lifecycle of a payment
from quote creation to settlement.
# List transfer quotes
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/cross-currency-transfers/list-transfer-quotes
openapi.yaml get /quotes
Retrieve a list of transfer quotes with optional filtering parameters. Returns all
quotes that match the specified filters. If no filters are provided, returns all quotes
(paginated).
# Look up an external account for payment
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/cross-currency-transfers/look-up-an-external-account-for-payment
openapi.yaml get /receiver/external-account/{accountId}
Lookup an external account by ID to determine supported currencies and exchange rates.
This endpoint helps platforms determine what currencies they can send to a given external account, along with the current estimated exchange rates and minimum and maximum amounts that can be sent.
# Look up an UMA address for payment
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/cross-currency-transfers/look-up-an-uma-address-for-payment
openapi.yaml get /receiver/uma/{receiverUmaAddress}
Lookup a receiving UMA address to determine supported currencies and exchange rates.
This endpoint helps platforms determine what currencies they can send to a given UMA address.
# Add a new customer
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/customers/add-a-new-customer
openapi.yaml post /customers
Register a new customer in the system with an account identifier and bank account information
# Delete customer by ID
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/customers/delete-customer-by-id
openapi.yaml delete /customers/{customerId}
Delete a customer by their system-generated ID
# Get a KYC link for onboarding a customer
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/customers/get-a-kyc-link-for-onboarding-a-customer
openapi.yaml get /customers/kyc-link
Generate a hosted KYC link to onboard a customer
# Get bulk import job status
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/customers/get-bulk-import-job-status
openapi.yaml get /customers/bulk/jobs/{jobId}
Retrieve the current status and results of a bulk customer import job. This endpoint can be used
to track the progress of both CSV uploads.
The response includes:
- Overall job status
- Progress statistics
- Detailed error information for failed entries
- Completion timestamp when finished
# Get customer by ID
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/customers/get-customer-by-id
openapi.yaml get /customers/{customerId}
Retrieve a customer by their system-generated ID
# List customers
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/customers/list-customers
openapi.yaml get /customers
Retrieve a list of customers with optional filtering parameters. Returns all customers that match
the specified filters. If no filters are provided, returns all customers (paginated).
# Update customer by ID
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/customers/update-customer-by-id
openapi.yaml patch /customers/{customerId}
Update a customer's metadata by their system-generated ID
# Upload customers via CSV file
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/customers/upload-customers-via-csv-file
openapi.yaml post /customers/bulk/csv
Upload a CSV file containing customer information for bulk creation. The CSV file should follow
a specific format with required and optional columns based on customer type.
### CSV Format
The CSV file should have the following columns:
Required columns for all customers:
- umaAddress: The customer's UMA address (e.g., $john.doe@uma.domain.com)
- platformCustomerId: Your platform's unique identifier for the customer
- customerType: Either "INDIVIDUAL" or "BUSINESS"
Required columns for individual customers:
- fullName: Individual's full name
- birthDate: Date of birth in YYYY-MM-DD format
- addressLine1: Street address line 1
- city: City
- state: State/Province/Region
- postalCode: Postal/ZIP code
- country: Country code (ISO 3166-1 alpha-2)
Required columns for business customers:
- businessLegalName: Legal name of the business
- addressLine1: Street address line 1
- city: City
- state: State/Province/Region
- postalCode: Postal/ZIP code
- country: Country code (ISO 3166-1 alpha-2)
Optional columns for all customers:
- addressLine2: Street address line 2
- platformAccountId: Your platform's identifier for the bank account
- description: Optional description for the customer
Optional columns for individual customers:
- email: Customer's email address
Optional columns for business customers:
- businessRegistrationNumber: Business registration number
- businessTaxId: Tax identification number
### Example CSV
```csv
umaAddress,platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,platformAccountId,businessLegalName
john.doe@uma.domain.com,customer123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US
acme@uma.domain.com,biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US
```
The upload process is asynchronous and will return a job ID that can be used to track progress.
You can monitor the job status using the `/customers/bulk/jobs/{jobId}` endpoint.
# Environments
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/environments
provides two environments: production and sandbox. Both environments use the same base URL and API tokens are scoped to an environment.
## Base API URL
`https://api.lightspark.com/grid/2025-10-13`
## Sandbox
Sandbox enables you to test your integration without making real payments. In sandbox, we expose sandbox specific APIs to trigger specific test cases like incoming payments. Additionally you'll find test UMA addresses to simulate different sending scenarios. For more information, see [Sandbox Testing](../payouts-and-b2b/platform-tools/sandbox-testing).
## Production
Production moves real money. To get access to a production environment, please reach out to your Lightspark contact.
## Service IPs
Grid APIs and webhooks are served from the following IP addresses:
* `52.42.15.30`
* `34.216.87.164`
* `44.226.21.146`
These IPs are subject to change, but we will notify the account contact email before making any changes.
# Get exchange rates
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/exchange-rates/get-exchange-rates
openapi.yaml get /exchange-rates
Retrieve cached exchange rates for currency corridors. Returns FX rates that are cached
for approximately 5 minutes. Rates include fees specific to your platform for authenticated requests.
**Filtering Options:**
- Filter by source currency to get all available destination corridors
- Filter by specific destination currency or currencies
- Provide a sending amount to get calculated receiving amounts
# Add a new external account
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/external-accounts/add-a-new-external-account
openapi.yaml post /customers/external-accounts
Register a new external bank account for a customer.
**Sandbox Testing:** In sandbox mode, use these account number patterns to test different transfer scenarios. These patterns should be used with the primary alias, address, or identifier of whatever account type you're testing. For example, the US account number, a CLABE, an IBAN, a spark wallet address, etc. The failure patterns are:
- Account numbers ending in **002**: Insufficient funds (transfer-in will fail)
- Account numbers ending in **003**: Account closed/invalid (transfers will fail)
- Account numbers ending in **004**: Transfer rejected (bank rejects the transfer)
- Account numbers ending in **005**: Timeout/delayed failure (stays pending ~30s, then fails)
- Any other account number: Success (transfers complete normally)
# Add a new platform external account
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/external-accounts/add-a-new-platform-external-account
openapi.yaml post /platform/external-accounts
Register a new external bank account for the platform.
**Sandbox Testing:** In sandbox mode, use these account number patterns to test different transfer scenarios. These patterns should be used with the primary alias, address, or identifier of whatever account type you're testing. For example, the US account number, a CLABE, an IBAN, a spark wallet address, etc. The failure patterns are:
- Account numbers ending in **002**: Insufficient funds (transfer-in will fail)
- Account numbers ending in **003**: Account closed/invalid (transfers will fail)
- Account numbers ending in **004**: Transfer rejected (bank rejects the transfer)
- Account numbers ending in **005**: Timeout/delayed failure (stays pending ~30s, then fails)
- Any other account number: Success (transfers complete normally)
# List Customer external accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/external-accounts/list-customer-external-accounts
openapi.yaml get /customers/external-accounts
Retrieve a list of external accounts with optional filtering parameters. Returns all
external accounts that match the specified filters. If no filters are provided, returns all external accounts
(paginated).
External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform.
# List platform external accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/external-accounts/list-platform-external-accounts
openapi.yaml get /platform/external-accounts
Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer.
These accounts are used for platform-wide operations such as receiving funds from external sources or managing platform-level payment destinations.
# Request Plaid Link token
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/external-accounts/request-plaid-link-token
openapi.yaml post /plaid/link-tokens
Creates a Plaid Link token that can be used to initialize Plaid Link in your application.
The Link token is used to authenticate the customer and allow them to select their bank account.
**Async Flow:**
1. Platform calls this endpoint to get a link_token and callbackUrl
2. Platform displays Plaid Link UI to the end customer using the link_token
3. End customer authenticates with their bank and selects an account
4. Plaid returns a public_token to the platform
5. Platform POSTs the public_token to the callbackUrl
6. Lightspark exchanges the public_token with Plaid and creates the external account asynchronously
7. Platform receives a webhook notification when the external account is ready
# Submit Plaid public token
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/external-accounts/submit-plaid-public-token
openapi.yaml post /plaid/callback/{plaid_link_token}
After the customer completes Plaid Link authentication, the platform should POST
the public_token to this callback URL (provided in the link token response).
This will trigger asynchronous processing:
1. Lightspark exchanges the public_token for an access_token with Plaid
2. Lightspark retrieves and verifies the account details
3. An external account is created
4. A webhook notification is sent to the platform when complete
# List Customer internal accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/internal-accounts/list-customer-internal-accounts
openapi.yaml get /customers/internal-accounts
Retrieve a list of internal accounts with optional filtering parameters. Returns all
internal accounts that match the specified filters. If no filters are provided, returns all internal accounts
(paginated).
Internal accounts are created automatically when a customer is created based on the platform configuration.
# List platform internal accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/internal-accounts/list-platform-internal-accounts
openapi.yaml get /platform/internal-accounts
Retrieve a list of all internal accounts that belong to the platform, as opposed to an individual customer.
These accounts are created automatically when the platform is configured for each supported currency. They can be used for things like distributing bitcoin rewards to customers, or for other platform-wide purposes.
# Cancel an UMA invitation
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/invitations/cancel-an-uma-invitation
openapi.yaml post /invitations/{invitationCode}/cancel
Cancel a pending UMA invitation. Only the inviter or platform can cancel an invitation.
When an invitation is cancelled:
1. The invitation status changes from PENDING to CANCELLED
2. The invitation can no longer be claimed
3. The invitation URL will show as cancelled when accessed
Only pending invitations can be cancelled. Attempting to cancel an invitation
that is already claimed, expired, or cancelled will result in an error.
# Claim an UMA invitation
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/invitations/claim-an-uma-invitation
openapi.yaml post /invitations/{invitationCode}/claim
Claim an UMA invitation by associating it with an invitee UMA address.
When an invitation is successfully claimed:
1. The invitation status changes from PENDING to CLAIMED
2. The invitee UMA address is associated with the invitation
3. An INVITATION_CLAIMED webhook is triggered to notify the platform that created the invitation
This endpoint allows customers to accept invitations sent to them by other UMA customers.
# Create an UMA invitation
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/invitations/create-an-uma-invitation
openapi.yaml post /invitations
Create an UMA invitation from a given platform customer.
# Get an UMA invitation by code
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/invitations/get-an-uma-invitation-by-code
openapi.yaml get /invitations/{invitationCode}
Retrieve details about an UMA invitation by its invitation code.
# Get platform configuration
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/platform-configuration/get-platform-configuration
openapi.yaml get /config
Retrieve the current platform configuration
# Update platform configuration
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/platform-configuration/update-platform-configuration
openapi.yaml patch /config
Update the platform configuration settings
# Create a transfer-in request
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/same-currency-transfers/create-a-transfer-in-request
openapi.yaml post /transfer-in
Transfer funds from an external account to an internal account for a specific customer. This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the paymentInstructions on the internal account to deposit funds.
# Create a transfer-out request
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/same-currency-transfers/create-a-transfer-out-request
openapi.yaml post /transfer-out
Transfer funds from an internal account to an external account for a specific customer.
# Simulate funding an internal account
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/sandbox/simulate-funding-an-internal-account
openapi.yaml post /sandbox/internal-accounts/{accountId}/fund
Simulate receiving funds into an internal account in the sandbox environment. This is useful for testing scenarios where you need to add funds to a customer's or platform's internal account without going through a real bank transfer or following payment instructions.
This endpoint is only for the sandbox environment and will fail for production platforms/keys.
# Simulate payment send to test receiving an UMA payment
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/sandbox/simulate-payment-send-to-test-receiving-an-uma-payment
openapi.yaml post /sandbox/uma/receive
Simulate sending payment from an sandbox uma address to a platform customer to test payment receive.
This endpoint is only for the sandbox environment and will fail for production platforms/keys.
# Simulate sending funds
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/sandbox/simulate-sending-funds
openapi.yaml post /sandbox/send
Simulate sending funds to the bank account as instructed in the quote.
This endpoint is only for the sandbox environment and will fail for production platforms/keys.
# Core Concepts
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/terminology
Core concepts and terminology for the Grid API
There are several key entities in the Grid API: **Platform**, **Customers**, **Internal Accounts**, **External Accounts**, **Quotes**, **Transactions**, and **UMA Addresses**.
## Businesses, People, and Accounts
### Platform
Your **platform** is you! It's the top-level entity that integrates with the Grid API. The platform:
* Has its own configuration (webhook endpoint, supported currencies, API tokens, etc.)
* A platform can have many customers both business and individual
* Manages multiple customers and their accounts
* Can hold platform-owned internal accounts for settlement and liquidity management
* Acts as the integration point between your application and the open Money Grid
### Customers
**Customers** are your end users who send and receive payments through your platform. Each customer:
* Can be an individual or business entity
* Has a KYC/KYB status that determines their ability to transact. If you are a regulated financial institution, this will typically be `APPROVED` since you do the KYC/KYB yourself.
* Is identified by both a system-generated ID and optionally your platform-specific customer ID
* May have associated internal accounts and external accounts
* May have a unique **UMA address** (e.g., `$john.doe@yourdomain.com`). If you don't assign an UMA address when creating a customer, they will be assigned a system-generated one.
### Internal Accounts
**Internal accounts** are Grid-managed accounts that hold balances in specific currencies. They can belong to either:
* **Platform internal accounts** - Owned by the platform for settlement, liquidity, and float management
* **Customer internal accounts** - Associated with specific customers for holding funds
Internal accounts:
* Have balances in a single currency (USD, EUR, MXN, etc.)
* Can be funded via bank transfers or crypto deposits using payment instructions
* Are used as sources or destinations for transactions instantly 24/7/365
* Track available balance for sending payments or receiving funds
### External Accounts
**External accounts** are traditional bank accounts, crypto wallets, or other payment instruments connected to customers
for on-ramping or off-ramping funds. Each external account:
* Are associated with a specific customer or the platform
* Represents a real-world bank account (with routing number, account number, IBAN, etc.), wallet, or payment instrument
* Has an associated beneficiary (individual or business) who receives payments from the customer or platform
* Has a status indicating screening status (ACTIVE, PENDING, INACTIVE, etc.)
* Can be used as a destination for quote-based transfers or same currency transfers like withdrawals
* For pullable sources like debit cards or ACH pulls, an external account can be used as a source for transfers-in to
fund internal accounts or to fund cross-border transfers via quotes.
## Entity Examples by Use Case
Understanding how entities map to your specific use case helps clarify your integration architecture. Here are common examples:
### B2B Payouts Platform (e.g., Bill.com, Routable)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------- | ------------------------------------------ |
| **Platform** | The payouts platform itself | Your company providing AP automation |
| **Customer** | Businesses sending payments to vendors | Acme Corp (your client company) |
| **External Account** | Vendors/suppliers receiving payments | Office supply vendor, freelance contractor |
**Flow**: Acme Corp (customer) uses your platform to pay their vendor invoices → funds move from Acme's internal account → to vendor's external bank account
### Direct Rewards Platform (Platform-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | ------------------------------------------- | ----------------------------------- |
| **Platform** | The app paying rewards directly to users | Your cashback app |
| **Customer** | (Not used in this model) | N/A |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Your platform sends micro-payouts directly from platform internal accounts → to users' external crypto wallets at scale. Common for cashback apps where the platform earns affiliate commissions and shares them with users.
### White-Label Rewards Platform (Customer-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------------- | ----------------------------------- |
| **Platform** | The rewards infrastructure provider | Your white-label rewards API |
| **Customer** | Brands or merchants running reward campaigns | Nike, Starbucks |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Nike (customer) funds their internal account → your platform sends rewards on their behalf → to users' external crypto wallets. Common for brand loyalty programs where merchants manage their own reward budgets.
### Remittance/P2P App (e.g., Wise, Remitly)
| Entity Type | Who They Are | Example |
| -------------------- | ----------------------------------- | ---------------------------------------------------------------- |
| **Platform** | The remittance service | Your money transfer app |
| **Customer** | Both sender and recipient of funds | Maria (sender in US), Juan (recipient in Mexico) |
| **External Account** | Bank accounts for funding/receiving | Maria's US bank (funding), Juan's Mexican bank (receiving funds) |
**Flow**: Maria (customer) funds transfer from her external account → to Juan (also a customer) → who receives funds in his external bank account. Alternatively, Maria could send to Juan's UMA address directly.
## Transactions and Addressing Entities
### Quotes
**Quotes** provide locked-in exchange rates and payment instructions for transfers. A quote:
* Specifies a source (internal account, customer ID, or the platform itself) and destination (internal/external account or UMA address)
* Locks an exchange rate for a short period (typically 1-5 minutes) or can be immediately executed with the `immediatelyExecute` flag
* Calculates total fees and amounts for currency conversion
* Provides payment instructions for funding the transfer if needed, or can be funded via an internal account balance.
* Must be executed before it expires
* Creates a transaction when executed
### Transactions
**Transactions** represent completed or in-progress payment transfers. Each transaction:
* Has a type (INCOMING or OUTGOING from the platform's perspective)
* Has a status (PENDING, COMPLETED, FAILED, etc.)
* References a customer (sender for outgoing, recipient for incoming) or a platform internal account
* Specifies source and destination (accounts or UMA addresses)
* Includes amounts, currencies, and settlement information
* May include counterparty information for compliance purposes if required by your platform configuration
Transactions are created when:
* A quote is executed (either incoming or outgoing)
* A same currency transfer is initiated (transfer-in or transfer-out)
### UMA Addresses (optional)
**UMA addresses** are human-readable payment identifiers that follow the format `$username@domain.com`. They:
* Uniquely identify entities on the Grid network
* Enable sending and receiving payments across different platforms without knowing the recipient's underlying account details or personal information
* Support currency negotiation and cross-border transfers
* Work similar to email addresses but for payments
* Are an optional UX improvement for some use cases. Use of UMA addresses is not required in order to use the Grid API.
# Approve a pending incoming payment
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/transactions/approve-a-pending-incoming-payment
openapi.yaml post /transactions/{transactionId}/approve
Approve a pending incoming payment that was previously acknowledged with a 202 response.
This endpoint allows platforms to asynchronously approve payments after async processing.
# Get transaction by ID
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/transactions/get-transaction-by-id
openapi.yaml get /transactions/{transactionId}
Retrieve detailed information about a specific transaction.
# List transactions
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/transactions/list-transactions
openapi.yaml get /transactions
Retrieve a paginated list of transactions with optional filtering.
The transactions can be filtered by customer ID, platform customer ID, UMA address,
date range, status, and transaction type.
# Reject a pending incoming payment
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/transactions/reject-a-pending-incoming-payment
openapi.yaml post /transactions/{transactionId}/reject
Reject a pending incoming payment that was previously acknowledged with a 202 response.
This endpoint allows platforms to asynchronously reject payments after additional processing.
# Account status notification webhook
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/webhooks/account-status-notification-webhook
openapi.yaml webhook account-status
Webhook that is called when the balance of an account changes
This endpoint should be implemented by clients of the Grid API.
### Authentication
The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
To verify the signature:
1. Get the Grid public key provided to you during integration
2. Decode the base64 signature from the header
3. Create a SHA-256 hash of the request body
4. Verify the signature using the public key and the hash
If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
### Account status
When the balance of an internal account changes, we will push a notification with information on the account, the new balance, and who the account belongs to.
# Bulk upload status webhook
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/webhooks/bulk-upload-status-webhook
openapi.yaml webhook bulk-upload
Webhook that is called when a bulk customer upload job completes or fails.
This endpoint should be implemented by clients of the Grid API.
### Authentication
The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
To verify the signature:
1. Get the Grid public key provided to you during integration
2. Decode the base64 signature from the header
3. Create a SHA-256 hash of the request body
4. Verify the signature using the public key and the hash
If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
This webhook is sent when a bulk upload job completes or fails, providing detailed information about the results.
# Incoming payment webhook and approval mechanism
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/webhooks/incoming-payment-webhook-and-approval-mechanism
openapi.yaml webhook incoming-payment
Webhook that is called when an incoming payment is received by a customer's UMA address.
This endpoint should be implemented by clients of the Grid API.
### Authentication
The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
To verify the signature:
1. Get the Grid public key provided to you during integration
2. Decode the base64 signature from the header
3. Create a SHA-256 hash of the request body
4. Verify the signature using the public key and the hash
If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
### Payment Approval Flow
When a transaction has `status: "PENDING"`, this webhook serves as an approval mechanism:
1. The client should check the `counterpartyInformation` against their requirements
2. To APPROVE the payment synchronously, return a 200 OK response
3. To REJECT the payment, return a 403 Forbidden response with an Error object
4. To request more information, return a 422 Unprocessable Entity with specific missing fields
5. To process the payment asynchronously, return a 202 Accepted response and then call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint within 5 seconds. Note that synchronous approval/rejection is preferred where possible.
The Grid system will proceed or cancel the payment based on your response.
For transactions with other statuses (COMPLETED, FAILED, REFUNDED), this webhook is purely informational.
# Invitation claimed webhook
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/webhooks/invitation-claimed-webhook
openapi.yaml webhook invitation-claimed
Webhook that is called when an invitation is claimed by a customer.
This endpoint should be implemented by platform clients of the Grid API.
When a customer claims an invitation, this webhook is triggered to notify the platform that:
1. The invitation has been successfully claimed
2. The invitee UMA address is now associated with the invitation
3. The invitation status has changed from PENDING to CLAIMED
This allows platforms to:
- Track invitation usage and conversion rates
- Trigger onboarding flows for new customers who joined via invitation
- Apply referral bonuses or rewards to the inviter
- Update their UI to reflect the claimed status
### Authentication
The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
To verify the signature:
1. Get the Grid public key provided to you during integration
2. Decode the base64 signature from the header
3. Create a SHA-256 hash of the request body
4. Verify the signature using the public key and the hash
If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
# Kyc customer status change
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/webhooks/kyc-customer-status-change
openapi.yaml webhook kyc-status
Webhook that is called when the KYC status of a customer is updated.
This endpoint should be implemented by clients of the Grid API.
### Authentication
The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
To verify the signature:
1. Get the Grid API public key provided to you during integration
2. Decode the base64 signature from the header
3. Create a SHA-256 hash of the request body
4. Verify the signature using the public key and the hash
If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
### KYC/B Flow
This webhook is triggered when KYC/B has reached a decision on a customer. Generally most customers will finish KYC within a few minutes. Others might be rejected because of incorrect data passed in or may have been flagged for manual review.
The webhook will only trigger for final states. This will be APPROVED, REJECTED, EXPIRED, CANCELED, MANUALLY_APPROVED, MANUALLY_REJECTED.
* APPROVED: The customer has been approved.
* REJECTED: The customer has been rejected after a KYC check.
* PENDING_REVIEW: KYC check is in progress.
* EXPIRED: KYC check has expired. This is generally because a customer did not submit all required information needed within a session.
* CANCELED: KYC check was canceled.
* MANUALLY_APPROVED: The customer was manually approved.
* MANUALLY_REJECTED: The customer was manually rejected.
* NOT_STARTED: KYC has not started on the customer.
# Outgoing payment status webhook
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/webhooks/outgoing-payment-status-webhook
openapi.yaml webhook outgoing-payment
Webhook that is called when an outgoing payment's status changes.
This endpoint should be implemented by clients of the Grid API.
### Authentication
The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
To verify the signature:
1. Get the Grid public key provided to you during integration
2. Decode the base64 signature from the header
3. Create a SHA-256 hash of the request body
4. Verify the signature using the public key and the hash
If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
This webhook is informational only and is sent when an outgoing payment completes successfully or fails.
# Send a test webhook
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/webhooks/send-a-test-webhook
openapi.yaml post /webhooks/test
Send a test webhook to the configured endpoint
# Test webhook for integration verification
Source: https://ramps-feat-building-with-ai.mintlify.app/api-reference/webhooks/test-webhook-for-integration-verification
openapi.yaml webhook test-webhook
Webhook that is sent once to verify your webhook endpoint is correctly set up.
This is sent when you configure or update your platform settings with a webhook URL.
### Authentication
The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by the Grid API.
To verify the signature:
1. Get the Grid public key provided to you during integration
2. Decode the base64 signature from the header
3. Create a SHA-256 hash of the request body
4. Verify the signature using the public key and the hash
If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
This webhook is purely for testing your endpoint integration and signature verification.
# Implementation Overview
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/getting-started/implementation-overview
High-level implementation plan for configuring, onboarding, funding, and moving money
This page gives you a 10,000‑ft view of an end‑to‑end implementation. It is intentionally generalized because the flow supports multiple customer types and external account types (e.g., CLABE, IBAN, US accounts, UPI). The detailed guides that follow provide concrete fields, edge cases, and step‑by‑step instructions.
## Platform configuration
Configure your platform once before building user flows.
* Set your UMA domain and brand details used for UMA addressing
* Configure required counterparty and compliance fields for your target corridors
* Provide webhook endpoints for outgoing and incoming payment notifications
* Generate API credentials for Sandbox (and later Production)
* Review regional capabilities (rails, currencies, settlement windows)
## Customer creation and onboarding
Onboard customers and assign UMA addresses if appropriate. There are two patterns:
* Regulated entities can directly create customers by providing KYC/KYB data via API
* Unregulated entities should request a KYC link and embed the hosted KYC flow; once completed, the customer can transact
* Store platform customer IDs and UMA handles for use in payment flows
## Account funding
Choose how transactions are funded based on your product design and region.
* Prefunded: Maintain balances in one or more currencies and spend from those balances
* Just‑in‑time (JIT): Create a quote and fund it in real time using the payment instructions provided; ideal when you don’t wish to hold float
You can mix models: keep small operational float for common corridors and use JIT for long tail routes.
## External account creation (payout destinations)
Register payout accounts your customers will send to (or receive from), such as CLABE (MX), IBAN (EU/UK), US accounts, UPI (IN), and others.
* Capture beneficiary details (individual or business) and required banking fields
* Validate account formats where applicable and map them to your internal customer
## Sending payments
Sending consists of lookup, pricing, funding, and execution.
* Resolve the counterparty: look up receiver information (UMA or bank account) for compliance review and to determine capabilities
* Create a quote: specify source/destination, currencies, and whether you lock sending or receiving amount; receive exchange rate, limits, fees, and (for JIT) funding instructions
* Fund and execute: for prefunded, confirm/execute; for JIT, push funds exactly as instructed (amount, reference) and the platform handles FX and delivery
* Observe status via webhooks and surface outcomes in your UI
When sending UMA payments, the sender can retrieve counterparty information before initiating to support compliance and risk checks.
## Receiving payments
Enable customers to receive funds to their UMA or linked bank account.
* Expose customer UMA/addressing to payers
* The platform handles conversion and offramping to the receiver’s account currency
* Approve or auto‑approve per your policy; update balances on completion via webhooks
## Reconciling transactions
Implement operational processes to keep your ledger in sync.
* Process webhooks idempotently; map statuses (pending, processing, completed, failed)
* Tie transactions back to quotes and customers; persist references
* Produce statements and audit trails; handle refunds, retries, and dispute flows where applicable
## Testing in Sandbox
Use Sandbox to build and validate end‑to‑end without moving real funds.
* Exercise receiver lookup, quote creation, funding instructions, and webhook lifecycles
* Validate compliance decisioning with realistic but synthetic data
* Optionally use the Test Wallet as a counterparty for faster iteration (see Tools)
## Enabling Production
When you’re ready to go live:
* Complete corridor and provider onboarding as needed for your regions
* Confirm webhook security, monitoring, and alerting are in place
* Review rate limits, error handling, retries, and idempotency keys
* Run final UAT in Sandbox, then request Production access from our team
Contact our team to enable Production and finalize corridor activations.
## Support
If you need assistance with the Grid API, please contact our support team
at [support@lightspark.com](mailto:support@lightspark.com) or visit our support
portal at [https://support.lightspark.com](https://support.lightspark.com).
# Platform Configuration
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/getting-started/platform-configuration
Configuring credentials, webhooks and currencies for your platform global P2P payments
## Supported currencies
During onboarding, choose the currencies your platform will support. For prefunded models, Grid automatically creates per‑currency accounts for each new customer. You can add or remove supported currencies anytime in the Grid dashboard.
## API credentials and authentication
Create API credentials in the Grid dashboard. Credentials are scoped to an environment (Sandbox or Production) and cannot be used across environments.
* Authentication: Use HTTP Basic Auth with your API key and secret in the `Authorization` header.
* Keys: Sandbox keys only work against Sandbox; Production keys only work against Production.
Never share or expose your API secret. Rotate credentials periodically and restrict access.
### Example: HTTP Basic Auth in cURL
```bash theme={null}
# Using cURL's Basic Auth shorthand (-u):
curl -sS -X GET "https://api.lightspark.com/grid/2025-10-13/config" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
## Base API path
The base API path is consistent across environments; your credentials determine the environment.
Base URL: `https://api.lightspark.com/grid/2025-10-13` (same for Sandbox and Production; your keys select the environment).
## Webhooks and signature verification
Configure your webhook endpoint to receive payment lifecycle events. Webhooks use asymmetric (public/private key) signatures; verify each webhook using the Grid public key available in your dashboard.
* Expose a public HTTPS endpoint (for development, reverse proxies like ngrok can help). You'll also need to set your webhook endpoint in the Grid dashboard.
* When receiving webhooks, verify the `X-Grid-Signature` header against the exact request body using the dashboard-provided public key
* Process events idempotently and respond with 2xx on success
You can trigger a test delivery from the API to validate your endpoint setup. The public key for verification is shown in the dashboard; rotate and update it when instructed by Lightspark.
### Test your webhook endpoint
Use the webhook test endpoint to send a synthetic event to your configured endpoint.
```bash theme={null}
curl -sS -X POST "https://api.lightspark.com/grid/2025-10-13/webhooks/test" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
Example test webhook payload:
```json theme={null}
{
"test": true,
"timestamp": "2023-08-15T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000001",
"type": "TEST"
}
```
For more details about webhooks like retry policy and examples, take a look at our Webhooks documentation.
## UMA configuration (optional)
To send and receive using UMA with your own domain (e.g., `$alice@yourdomain.com`), configure the following:
1. Configure your UMA domain
2. Proxy inbound UMA requests to
3. Define supported currencies and, if you are a regulated institution, the counterparty information you require
If you do not configure an UMA domain, Grid will use the default domain `grid.lightspark.com`.
You can find more information about the UMA protocol and end user experience at [https://uma.me](https://uma.me).
### UMA domain
The `umaDomain` parameter defines the domain part of all UMA addresses for your users. For example, if you set `umaDomain` to `mycompany.com`, your users' UMA addresses will follow the format `$username@mycompany.com`.
### Configure UMA proxy requests
Set up proxying so UMA‑related requests are forwarded to your assigned `proxyUmaSubdomain`.
* UMA domain determines the address format (e.g., `$alice@yourdomain.com`)
* Proxy the following paths to `{proxyUmaSubdomain}`:
* `https:///.well-known/lnurlp/*` -> `https://.grid.lightspark.com/.well-known/lnurlp/*`
* `https:///.well-known/lnurlpubkey` -> `https://.grid.lightspark.com/.well-known/lnurlpubkey`
* `https:///.well-known/uma-configuration` -> `https://.grid.lightspark.com/.well-known/uma-configuration`
Additionally, configure:
* Supported currencies (min/max, enabled transaction types)
* Required counterparty fields per currency for compliance screening
### UMA supported currencies
Define per‑currency rules for your UMA flows. Each entry can include:
* `currencyCode`: (String, required) The ISO 4217 currency code (e.g., "USD").
* `minAmount`: (Integer, required) Minimum transaction amount in the smallest unit of the currency.
* `maxAmount`: (Integer, required) Maximum transaction amount in the smallest unit of the currency.
* `requiredCounterpartyFields`: (Array, required) For regulated entities, defines PII your platform requires about *external counterparties* for transactions in this currency.
* `providerRequiredCustomerFields`: (Array, read-only) For regulated entities, lists user info field names (from `UserInfoFieldName`) that the UMA provider mandates for *your own users* to transact in this currency. This impacts user creation/updates.
### Required counterparty fields
For regulated entities, within each currency defined in `supportedCurrencies`, the `requiredCounterpartyFields` parameter allows you to specify what information your platform needs to collect from *external counterparties* (senders or receivers) involved in transactions with your users for that specific currency.
Available counterparty fields (to be specified with a `name` and `mandatory` flag):
| Field Name (type `UserInfoFieldName`) | Description |
| ------------------------------------- | ---------------------------------------------------- |
| `FULL_NAME` | Full legal name of the individual or business |
| `BIRTH_DATE` | Date of birth in YYYY-MM-DD format (for individuals) |
| `NATIONALITY` | Nationality of the individual |
| `ADDRESS` | Physical address including country, city, etc. |
| `PHONE_NUMBER` | Contact phone number including country code |
| `EMAIL` | Email address |
| `BUSINESS_NAME` | Legal business name (for business entities) |
| `TAX_ID` | Tax identification number |
Each field in `requiredCounterpartyFields` is an object containing:
* `name`: The `UserInfoFieldName` representing the PII you require.
* `mandatory`: A boolean (true/false) indicating if this field is strictly required by your platform for transactions in this currency.
This information will be provided to your platform via webhooks for pending payments, allowing you to screen the counterparty based on your compliance rules before approving the payment.
### UMA provider required user fields
For regulated financial institutions, the `providerRequiredCustomerFields` array (per currency) lists the user fields required by the underlying provider. This array is read‑only and informs what you must capture for your own users.
This list specifies which user information fields are mandated by the underlying UMA provider for *your own registered users* if they intend to send or receive payments in that particular currency. For example, to allow a user to transact in "USD", the UMA provider might require that the user has a `NATIONALITY` on record.
These fields must be supplied when creating or updating a user via the `POST /customers` or `PATCH /customers/{customerId}` endpoints if that user is expected to use the specified currency. Refer to the [Configuring Customers](/global-p2p/onboarding-customers/configuring-customers) guide for more details on how this impacts user setup.
## Manage configuration via API
If you prefer to manage settings programmatically, use the `/config` endpoints.
### Retrieve current configuration
You can retrieve your current platform configuration to see what settings are already in place:
```bash theme={null}
curl -sS -X GET "https://api.lightspark.com/grid/2025-10-13/config" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
Response example:
```json theme={null}
{
"id": "PlatformConfig:019542f5-b3e7-1d02-0000-000000000003",
"umaDomain": "example.com",
"webhookEndpoint": "https://api.example.com/webhooks/uma",
"supportedCurrencies": [
{
"currencyCode": "USD",
"minAmount": 100,
"maxAmount": 1000000,
"requiredCounterpartyFields": [
{
"name": "FULL_NAME",
"mandatory": true
},
{
"name": "BIRTH_DATE",
"mandatory": true
}
],
"providerRequiredCustomerFields": [
"NATIONALITY",
"FULL_NAME"
]
}
],
"createdAt": "2023-06-15T12:30:45Z",
"updatedAt": "2023-07-01T10:00:00Z"
}
```
If this is your first time configuring the platform, some default values may be returned which were set up when you first created your account.
### Update platform configuration
To update your platform configuration, call the PATCH endpoint with the fields you want to change:
```bash theme={null}
curl -sS -X PATCH "https://api.lightspark.com/grid/2025-10-13/config" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"umaDomain": "mycompany.com",
"webhookEndpoint": "https://api.mycompany.com/webhooks/uma",
"supportedCurrencies": [
{
"currencyCode": "USD",
"minAmount": 100,
"maxAmount": 1000000,
"requiredCounterpartyFields": [
{ "name": "FULL_NAME", "mandatory": true },
{ "name": "BIRTH_DATE", "mandatory": true },
{ "name": "ADDRESS", "mandatory": false }
]
}
]
}'
```
Response:
```json theme={null}
{
"id": "PlatformConfig:019542f5-b3e7-1d02-0000-000000000003",
"umaDomain": "mycompany.com",
"webhookEndpoint": "https://api.mycompany.com/webhooks/uma",
"supportedCurrencies": [
{
"currencyCode": "USD",
"minAmount": 100,
"maxAmount": 1000000,
"requiredCounterpartyFields": [
{
"name": "FULL_NAME",
"mandatory": true
},
{
"name": "BIRTH_DATE",
"mandatory": true
},
{
"name": "ADDRESS",
"mandatory": false
}
],
"providerRequiredCustomerFields": [
"NATIONALITY",
"FULL_NAME"
]
}
],
"createdAt": "2023-06-15T12:30:45Z",
"updatedAt": "2023-06-15T12:30:45Z"
}
```
## Verify Configuration
After updating your configuration, it's recommended to verify that the changes were saved correctly:
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/config" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
The response should reflect your updated **settings**.
# Global P2P
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/index
With , you can send and receive low cost real-time payments to bank accounts and UMA addresses worldwide through a single, simple API. automatically routes each payment across its network of Grid switches, handling FX, blockchain settlement, and instant banking off-ramps for you.
interacts with the Money Grid to route your payments globally.
Leverages local instant banking rails and global low latency crypto rails to settle payments in real-time.
Leverages local instant banking rails and global low latency crypto rails to settle payments in real-time.
For background on the UMA protocol itself, see the UMA Standard documentation: [UMA Standard—Introduction](https://docs.uma.me/uma-standard/introduction).
## Payment Flow
You can either prefund an internal account with fiat or receive just-in-time payment instructions as part of the quote.
Create a quote to lock exchange rate to the receiving foreign account and parse payment instructions.
Execute the quote or send funds as per payment instructions to initiate the transfer from the internal account to the external bank account.
## Features
Users interact with through two main interfaces.
Programmatic access to create customers, quotes, fund the account, send payments and reconcile via webhooks.
Your development and operations team can use the dashboard to monitor payments and webhooks, manage API keys and environments, and troubleshoot with logs.
supports the following functionality.
### Onboarding customers
has two customer onboarding options - one for non regulated entities where handles the KYC/KYB process and one for regulated entities where you handle the KYC/KYB process.
When creating customers, you'll be able to assign a customized UMA address to facilitate sending and receiving UMA payments.
### Funding Payments
supports multiple transaction funding options including prefunded accounts and real-time funding. You can prefund an account using several payment rails such as ACH, SEPA Instant, wire transfers, Lightning, and more.
With real-time funding, you'll receive payment instructions as part of the quote. Once payment is received by our services, we'll initiate the payment to the receiver.
### Sending & Receiving Payments
To send with , you query recipient information and pricing, then execute and fund a quote. resolves the receiver (by UMA or external bank details), returns min/max and an exchange rate, and provides funding instructions. Once funded, handles FX and delivery to the receiving account.
To receive with , you expose an UMA or supported account identifier. The platform handles conversion and offramping to the receiver’s account currency and notifies you via webhooks so you can credit the customer.
### Environments
supports two environments: **Sandbox** and **Production**.
Sandbox mirrors production behavior and webhooks so you can test receiver resolution, quotes and funding instructions, settlement status changes, and full end‑to‑end flows without moving real funds.
Production uses live credentials and base URLs for real payments once you’re ready to launch.
# External Accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/managing-accounts/external-accounts
Register and manage beneficiary bank accounts
External accounts are bank accounts, cryptocurrency wallets, or payment destinations outside Grid where you can send funds. Grid supports two types:
* **Customer external accounts** - Scoped to individual customers, used for withdrawals and customer-specific payouts
* **Platform external accounts** - Scoped to your platform, used for platform-wide operations like receiving funds from external sources
Customer external accounts often require some basic beneficiary information for compliance.
Platform accounts are managed at the organization level.
## Create external accounts by region or wallet
**ACH, Wire, RTP**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}'
```
Category must be `CHECKING` or `SAVINGS`. Routing number must be 9 digits.
**CLABE/SPEI**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "MXN",
"platformAccountId": "mx_beneficiary_001",
"accountInfo": {
"accountType": "CLABE",
"clabeNumber": "123456789012345678",
"bankName": "BBVA Mexico",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "María García",
"birthDate": "1985-03-15",
"nationality": "MX",
"address": {
"line1": "Av. Reforma 123",
"city": "Ciudad de México",
"state": "CDMX",
"postalCode": "06600",
"country": "MX"
}
}
}
}'
```
**PIX**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "BRL",
"platformAccountId": "br_pix_001",
"accountInfo": {
"accountType": "PIX",
"pixKey": "user@email.com",
"pixKeyType": "EMAIL",
"bankName": "Nubank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "João Silva",
"birthDate": "1988-07-22",
"nationality": "BR",
"address": {
"line1": "Rua das Flores 456",
"city": "São Paulo",
"state": "SP",
"postalCode": "01234-567",
"country": "BR"
}
}
}
}'
```
Key types: `CPF`, `CNPJ`, `EMAIL`, `PHONE`, or `RANDOM`
**IBAN/SEPA**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "EUR",
"platformAccountId": "eu_iban_001",
"accountInfo": {
"accountType": "IBAN",
"iban": "DE89370400440532013000",
"swiftBic": "DEUTDEFF",
"bankName": "Deutsche Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Hans Schmidt",
"birthDate": "1982-11-08",
"nationality": "DE",
"address": {
"line1": "Hauptstraße 789",
"city": "Berlin",
"state": "Berlin",
"postalCode": "10115",
"country": "DE"
}
}
}
}'
```
**UPI**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "INR",
"platformAccountId": "in_upi_001",
"accountInfo": {
"accountType": "UPI",
"vpa": "user@okbank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Priya Sharma",
"birthDate": "1991-05-14",
"nationality": "IN",
"address": {
"line1": "123 MG Road",
"city": "Mumbai",
"state": "Maharashtra",
"postalCode": "400001",
"country": "IN"
}
}
}
}'
```
**Bitcoin Lightning (Spark Wallet)**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "BTC",
"platformAccountId": "btc_spark_001",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}'
```
Spark wallets don't require beneficiary information as they are self-custody wallets.
Use `platformAccountId` to tie your internal id with the external account.
**Sample Response:**
```json theme={null}
{
"id": "ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"status": "ACTIVE",
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}
```
### Business beneficiaries
For business accounts, include business information:
```json theme={null}
{
"currency": "USD",
"platformAccountId": "acme_corp_account",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "987654321",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "BUSINESS",
"businessInfo": {
"legalName": "Acme Corporation, Inc.",
"taxId": "EIN-987654321"
},
"address": {
"line1": "456 Business Ave",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}
}
}
```
## Account status
Beneficiary data may be reviewed for risk and compliance. Only `ACTIVE` accounts can receive payments. Updates to account data may trigger account re-review.
| Status | Description |
| -------------- | ----------------------------------- |
| `PENDING` | Created, awaiting verification |
| `ACTIVE` | Verified and ready for transactions |
| `UNDER_REVIEW` | Additional review required |
| `INACTIVE` | Disabled, cannot be used |
## Listing external accounts
### List customer accounts
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
### List platform accounts
For platform-wide operations, list all platform-level external accounts:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Platform external accounts are used for platform-wide operations like
depositing funds from external sources.
## Best practices
Validate account details before submission:
```javascript theme={null}
// US accounts: 9-digit routing, 4-17 digit account number
if (!/^\d{9}$/.test(routingNumber)) {
throw new Error("Invalid routing number");
}
// CLABE: exactly 18 digits
if (!/^\d{18}$/.test(clabeNumber)) {
throw new Error("Invalid CLABE number");
}
```
Verify status before sending payments:
```javascript theme={null}
if (account.status !== "ACTIVE") {
throw new Error(`Account is ${account.status}, cannot process payment`);
}
```
Never expose full account numbers. Display only masked info:
```javascript theme={null}
function displaySafely(account) {
return {
id: account.id,
bankName: account.accountInfo.bankName,
lastFour: account.accountInfo.accountNumber.slice(-4),
status: account.status,
};
}
```
# Internal Accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/managing-accounts/internal-accounts
Create and manage internal accounts
Internal accounts are Lightspark managed accounts that hold funds within the Grid platform. They allow you to receive deposits and send payments to external bank accounts or other payment destinations.
They are useful for holding funds on behalf or the platform or customers which will be used for instant, 24/7 quotes and transfers out of the system.
Internal accounts are created for both:
* **Platform-level accounts**: Hold pooled funds for your platform operations (rewards distribution, reconciliation, etc.)
* **Customer accounts**: Hold individual customer funds for their transactions
Internal accounts are automatically created when you onboard a customer, based
on your platform's currency configuration. Platform-level internal accounts
are created when you configure your platform with supported currencies.
## How internal accounts work
Internal accounts act as an intermediary holding account in the payment flow:
1. **Deposit funds**: You or your customers deposit money into internal accounts using bank transfers (ACH, wire, PIX, etc.) or crypto transfers
2. **Hold balance**: Funds are held securely in the internal account until needed
3. **Send payments**: You initiate transfers from internal accounts to external destinations
Each internal account:
* Is denominated in a single currency (USD, EUR, etc.)
* Has a unique balance that you can query at any time
* Includes unique payment instructions for depositing funds
* Supports multiple funding methods depending on the currency
## Retrieving internal accounts
### List customer internal accounts
To retrieve all internal accounts for a specific customer, use the customer ID to filter the results:
```bash Request internal accounts for a customer theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json theme={null}
{
"data": [
{
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"fundingPaymentInstructions": [
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"reference": "FUND-ABC123",
"accountType": "US_ACCOUNT",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
},
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
],
"createdAt": "2025-10-03T12:00:00Z",
"updatedAt": "2025-10-03T14:30:00Z"
}
],
"hasMore": false,
"totalCount": 1
}
```
### Filter by currency
You can filter internal accounts by currency to find accounts for specific denominations:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001¤cy=USD' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
### List platform internal accounts
To retrieve platform-level internal accounts (not tied to individual customers), use the platform internal accounts endpoint:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Platform internal accounts are useful for managing pooled funds, distributing
rewards, or handling platform-level operations.
## Understanding funding payment instructions
Each internal account includes `fundingPaymentInstructions` that tell your customers how to deposit funds. The structure varies by payment rail and currency:
For USD accounts, instructions include routing and account numbers:
```json theme={null}
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"accountType": "US_ACCOUNT",
"reference": "FUND-ABC123",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
}
```
Each internal account has unique banking details in the `accountOrWalletInfo`
field, which ensures deposits are automatically credited to the correct
account.
For EUR accounts, instructions use SEPA IBAN numbers:
```json theme={null}
{
"instructionsNotes": "Include reference in SEPA transfer description",
"accountOrWalletInfo": {
"accountType": "IBAN",
"reference": "FUND-EUR789",
"iban": "DE89370400440532013000",
"swiftBic": "DEUTDEFF",
"accountHolderName": "Lightspark Payments FBO Maria Garcia",
"bankName": "Banco de México"
}
}
```
For stablecoin accounts, using a Spark wallet as the funding source:
```json theme={null}
{
"invoice": "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs",
"instructionsNotes": "Use the invoice when making Spark payment",
"accountOrWalletInfo": {
"accountType": "SPARK_WALLET",
"assetType": "USDB",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
```
For Solana wallet accounts, using a Solana wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
```
For Tron wallet accounts, using a Tron wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "TRON_WALLET",
"assetType": "USDT",
"address": "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL"
}
}
```
For Polygon wallet accounts, using a Polygon wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "POLYGON_WALLET",
"assetType": "USDC",
"address": "0xAbCDEF1234567890aBCdEf1234567890ABcDef12"
}
}
```
For Base wallet accounts, using a Base wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "BASE_WALLET",
"assetType": "USDC",
"address": "0xAbCDEF1234567890aBCdEf1234567890ABcDef12"
}
}
```
## Checking account balances
The internal account balance reflects all deposits and withdrawals. The balance includes:
* **amount**: The balance amount in the smallest currency unit (cents for USD, centavos for MXN/BRL, etc.)
* **currency**: Full currency details including code, name, symbol, and decimal places
### Example balance check
```bash Fetch the balance of an internal account theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts/InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json theme={null}
{
"data": {
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
}
}
}
```
Always check the `decimals` field in the currency object to correctly convert
between display amounts and API amounts. For example, USD has 2 decimals, so
an amount of 50000 represents \$500.00.
## Displaying funding instructions to customers
When customers need to deposit funds themselves, display the funding payment instructions in your application:
Fetch the customer's internal account for their desired currency using the API.
Parse the `fundingPaymentInstructions` array and select the appropriate instructions based on your customer's preferred payment method.
```javascript theme={null}
const instructions = account.fundingPaymentInstructions[0];
const bankInfo = instructions.accountOrWalletInfo;
```
Show the payment details prominently in your UI:
* Account holder name
* Bank name and routing information (account/routing number, CLABE, PIX key, etc.)
* Reference code (if provided)
* Any additional notes from `instructionsNotes`
The unique banking details in each internal account automatically route
deposits to the correct destination.
Set up webhook listeners to receive notifications when deposits are credited to the internal account. The account balance will update automatically.
You'll receive `ACCOUNT_STATUS` webhook events when the internal account balance changes.
## Best practices
Ensure your customers have all the information needed to make deposits. Consider implementing:
* Clear display of all banking details from `fundingPaymentInstructions`
* Copy-to-clipboard functionality for account numbers and reference codes
* Email/SMS confirmations with complete deposit instructions
Set up monitoring to alert customers when their balance is low:
```javascript theme={null}
if (account.balance.amount < minimumThreshold) {
await notifyCustomer({
type: 'LOW_BALANCE',
account: account.id,
instructions: account.fundingPaymentInstructions
});
}
```
If your platform supports multiple currencies, organize internal accounts by currency in your UI:
```javascript theme={null}
const accountsByCurrency = accounts.data.reduce((acc, account) => {
const code = account.balance.currency.code;
acc[code] = account;
return acc;
}, {});
// Quick lookup: accountsByCurrency['USD']
```
Internal account details (especially funding instructions) rarely change, so you can cache them safely. However, always fetch fresh balance data before initiating transfers.
# External Accounts with Plaid
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/managing-accounts/plaid
Simplify bank account linking with Plaid
Plaid integration allows your customers to securely connect their bank accounts without manually entering account numbers and routing information. Grid handles the complete Plaid Link flow, automatically creating external accounts when customers authenticate their banks.
Plaid integration requires Grid to manage your Plaid configuration. Contact
support to enable Plaid for your platform.
## Overview
The Plaid flow involves collaboration between your platform, Grid, Plaid, and the customer's bank:
1. **Request link token**: Your platform requests a Plaid Link token from Grid for a specific customer
2. **Initialize Plaid Link**: Display Plaid Link UI to your customer using the link token
3. **Customer authenticates**: Customer selects their bank and authenticates using Plaid Link
4. **Exchange tokens**: Plaid returns a public token; your platform sends it to Grid's callback URL
5. **Async processing**: Grid exchanges the public token with Plaid and retrieves account details
6. **External account created**: Grid creates the external account and sends a webhook notification. The external account is available for transfers and payments
## Request a Plaid Link token
To initiate the Plaid flow, request a link token from Grid:
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/plaid/link-tokens' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001"
}'
```
**Response:**
```json theme={null}
{
"linkToken": "link-sandbox-af1a0311-da53-4636-b754-dd15cc058176",
"expiration": "2025-10-05T18:30:00Z",
"callbackUrl": "https://api.lightspark.com/grid/2025-10-13/plaid/callback/link-sandbox-af1a0311-da53-4636-b754-dd15cc058176",
"requestId": "req_abc123def456"
}
```
Store the `callbackUrl` when you request the link token so you can retrieve it later when exchanging the public token.
### Key response fields:
* **`linkToken`**: Use this to initialize Plaid Link in your frontend
* **`callbackUrl`**: Where to POST the public token after Plaid authentication completes. The URL follows the pattern `https://api.lightspark.com/grid/{version}/plaid/callback/{linkToken}`. While you can construct this manually, we recommend using the provided URL for forward compatibility.
* **`expiration`**: Link tokens typically expire after 4 hours
* **`requestId`**: Unique identifier for debugging purposes
Link tokens are single-use and will expire. If the customer doesn't complete
the flow, you'll need to request a new link token.
## Initialize Plaid Link
Display the Plaid Link UI to your customer using the link token. The implementation varies by platform:
Install the appropriate Plaid SDK for your platform:
* React: `npm install react-plaid-link`
* React Native: `npm install react-native-plaid-link-sdk`
* Vanilla JS: Include the Plaid script tag as shown above
```javascript theme={null}
import { usePlaidLink } from 'react-plaid-link';
function BankAccountConnector({ linkToken, onSuccess }) {
const { open, ready } = usePlaidLink({
token: linkToken,
onSuccess: async (publicToken, metadata) => {
console.log('Plaid authentication successful');
// Send public token to YOUR backend endpoint
await fetch('/api/plaid/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: metadata.account_id, // Optional
}),
});
onSuccess();
},
onExit: (error, metadata) => {
if (error) {
console.error('Plaid Link error:', error);
}
console.log('User exited Plaid Link');
},
});
return (
); }
```
```javascript theme={null}
import { PlaidLink } from 'react-native-plaid-link-sdk';
function BankAccountConnector({ linkToken, onSuccess }) {
return (
{
console.log('Plaid authentication successful');
// Send public token to YOUR backend endpoint
await fetch('https://yourapi.com/api/plaid/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: metadata.account_id,
}),
});
onSuccess();
}}
onExit={(error, metadata) => {
if (error) {
console.error('Plaid Link error:', error);
}
}}
>
Connect your bank account
);
}
```
```html theme={null}
```
## Exchange the public token on your backend
Create a backend endpoint that receives the public token from your frontend and forwards it to Grid's callback URL:
```javascript Express theme={null}
// Backend endpoint: POST /api/plaid/exchange-token
app.post('/api/plaid/exchange-token', async (req, res) => {
const { publicToken, accountId } = req.body;
const customerId = req.user.gridCustomerId; // From your auth
try {
// Get the callback URL (you stored this when requesting the link token)
const callbackUrl = await getStoredCallbackUrl(customerId);
// Forward to Grid's callback URL with proper authentication
const response = await fetch(callbackUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: accountId,
}),
});
if (!response.ok) {
throw new Error(`Grid API error: ${response.status}`);
}
const result = await response.json();
res.json({ success: true, message: result.message });
} catch (error) {
console.error('Error exchanging token:', error);
res.status(500).json({ error: 'Failed to process bank account' });
}
});
```
**Response from Grid (HTTP 202 Accepted):**
```json theme={null}
{
"message": "External account creation initiated. You will receive a webhook notification when complete.",
"requestId": "req_def456ghi789"
}
```
A `202 Accepted` response indicates Grid has received the token and is
processing it asynchronously. The external account will be created in the
background.
## Handle webhook notification
After Grid creates the external account, you'll receive an `ACCOUNT_STATUS` webhook.
```json theme={null}
{
"type": "ACCOUNT_STATUS",
"timestamp": "2025-01-15T14:32:10Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-0000000000ac",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"account": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"status": "ACTIVE",
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}
}
```
## Error handling
Handle common error scenarios:
### User exits Plaid Link
```javascript theme={null}
const { open } = usePlaidLink({
token: linkToken,
onExit: (error, metadata) => {
if (error) {
console.error("Plaid error:", error);
// Show user-friendly error message
setError("Unable to connect to your bank. Please try again.");
} else {
// User closed the modal without completing
console.log("User exited without connecting");
}
},
});
```
# Configuring Customers
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/onboarding-customers/configuring-customers
Creating and managing customers for global P2P payments
This guide provides comprehensive information about creating customers in the Grid API, including customer types, onboarding models, registration, and management.
## Onboarding model
There are two models for regulated and unregulated platforms.
* Regulated platforms: Use your existing compliance processes. Create individual and business customers directly via `POST /customers`. The information you supply is used for beneficiary/counterparty compliance screening.
* Unregulated platforms: Grid performs KYC/KYB. Generate a hosted KYC/KYB link, redirect your customer to complete verification in their locale, receive a KYC result webhook. While KYC is pending, allow customers to finish account setup but block funding and money movement.
## Customer Types
The Grid API supports both individual and business customers. While the API schema itself makes most Personally Identifiable Information (PII) optional at initial creation, specific fields may become mandatory based on the currencies the customer will transact with.
Your platform’s configuration ( retrieved via `GET /config`) includes a supportedCurrencies array. Each currency object within this array has a providerRequiredCustomerFields list. If a customer is intended to use a specific currency, any fields listed for that currency must be provided when creating or updating the customer.
The base required information for all customers is only:
* Platform customer ID (your internal identifier)
* Customer type (`INDIVIDUAL` or `BUSINESS`)
If using sending and receiving to just-in-time UMA addresses, you'll also need to specify the bank account information
## Creating Customers
**Regulated platforms** have lighter KYC requirements since they handle compliance verification internally.
The KYC/KYB flow allows you to onboard customers through direct API calls.
Regulated financial institutions can:
* **Direct API Onboarding**: Create customers directly via API calls with minimal verification
* **Internal KYC/KYB**: Handle identity verification through your own compliance systems
* **Reduced Documentation**: Only provide essential customer information required by your payment counterparty or service provider.
* **Faster Onboarding**: Streamlined process for known, verified customers
#### Creating Customers via Direct API
For regulated platforms, you can create customers directly through the API without requiring external KYC verification:
To register a new customer in the system, use the `POST /customers` endpoint:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/customers" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"platformCustomerId": "customer_12345",
"customerType": "INDIVIDUAL",
"fullName": "Jane Doe",
"birthDate": "1992-03-25",
"nationality": "US",
"address": {
"line1": "123 Pine Street",
"city": "Seattle",
"state": "WA",
"postalCode": "98101",
"country": "US"
}
}'
```
The examples below show a more comprehensive set of data. Not all fields are strictly required by the API for customer creation itself, but become necessary based on currency and UMA provider requirements if using UMA.
```json theme={null}
{
"platformCustomerId": "9f84e0c2a72c4fa",
"customerType": "INDIVIDUAL",
"fullName": "John Sender",
"birthDate": "1985-06-15",
"address": {
"line1": "Paseo de la Reforma 222",
"line2": "Piso 15",
"city": "Ciudad de México",
"state": "Ciudad de México",
"postalCode": "06600",
"country": "MX"
}
}
```
```json theme={null}
{
"platformCustomerId": "b87d2e4a9c13f5b",
"customerType": "BUSINESS",
"businessInfo": {
"legalName": "Acme Corporation",
"registrationNumber": "789012345",
"taxId": "123-45-6789"
},
"address": {
"line1": "456 Oak Avenue",
"line2": "Floor 12",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}
```
**Unregulated platforms** require full KYC/KYB verification of customers through hosted flows.
Unregulated platforms must:
* **Hosted KYC Flow**: Use the hosted KYC link for complete identity verification
* **Extended Review**: Customers may require manual review and approval in some cases
### Hosted KYC Link Flow
The hosted KYC flow provides a secure, hosted interface where customers can complete their identity verification and onboarding process.
#### Generate KYC Link
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/customers/kyc-link?redirectUri=https://yourapp.com/onboarding-complete&platformCustomerId=019542f5-b3e7-1d02-0000-000000000001" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
**Response:**
```json theme={null}
{
"kycUrl": "https://kyc.lightspark.com/onboard/abc123def456",
"platformCustomerId": "019542f5-b3e7-1d02-0000-000000000001"
}
```
#### Complete KYC Process
Call the `/customers/kyc-link` endpoint with your `redirectUri` parameter to generate a hosted KYC URL for your customer.
The `redirectUri` parameter is embedded in the generated KYC URL and will be used to automatically redirect the customer back to your application after they complete verification.
Redirect your customer to the returned `kycUrl` where they can complete their identity verification in the hosted interface.
The KYC link is single-use and expires after a limited time period for security.
The customer completes the identity verification process in the hosted KYC interface, providing required documents and information.
The hosted interface handles document collection, verification checks, and compliance requirements automatically.
After verification processing, you'll receive a KYC status webhook notification indicating the final verification result.
Upon successful KYC completion, the customer is automatically redirected to your specified `redirectUri` URL.
The customer account will be automatically created by the system upon successful KYC completion. You can identify the new customer using your `platformCustomerId` or other identifiers.
On your redirect page, handle the completed KYC flow and integrate the new customer into your application.
### Individual customers
In some cases, only the above fields are required at customer creation. Beyond those base requirements, additional fields commonly associated with individual customers include:
* Full name
* Date of birth (YYYY-MM-DD format)
* Physical address (including country, state, city, postalCode)
**Note:** Check the `providerRequiredCustomerFields` for each relevant currency in your platform's configuration to determine which of these fields are strictly mandatory at creation/update time for that customer to transact in those currencies.
### Business customers
Beyond the base requirements, additional fields commonly associated with business customers include:
* Business information:
* Legal name (this is often required, check `providerRequiredCustomerFields`)
* Registration number (optional, unless specified by `providerRequiredCustomerFields`)
* Tax ID (optional, unless specified by `providerRequiredCustomerFields`)
* Physical address (including country, state, city, postalCode)
**Note:** Check the `providerRequiredCustomerFields` for each relevant currency in your platform's configuration to determine which of these fields are strictly mandatory at creation/update time for that customer to transact in those currencies.
When creating or updating customers, the `customerType` field must be specified as either `INDIVIDUAL` or `BUSINESS`.
There can be multiple customers with the same platformCustomerId but different UMA addresses. This is useful if you want to track multiple UMA addresses and/or bank accounts for the same customer in your platform.
## Customer Creation Process
### Creating a new individual customer (regulated institutions)
To register a new customer directly, use the `POST /customers` endpoint (regulated institutions):
```bash theme={null}
curl -sS -X POST "https://api.lightspark.com/grid/2025-10-13/customers" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"platformCustomerId": "9f84e0c2a72c4fa",
"customerType": "INDIVIDUAL",
"fullName": "Jane Doe",
}'
```
The API allows creating a customer with minimal PII. However, to enable transactions for a customer in specific currencies, you must include any PII fields mandated by the `providerRequiredCustomerFields` for those currencies (found in your platform's configuration via `GET /config`).
The examples below show a more comprehensive set of data. Not all fields are strictly required by the API for customer creation itself, but become necessary based on currency and provider requirements.
Example request body for an individual customer with UMA instant payments enabled (ensure all `providerRequiredCustomerFields` for intended currencies are included):
Typically bank account information is provided separately via internal and external account management. However, when using UMA for instant payments, since funding and withdrawals are instant, bank account information can be provided at time of customer creation.
```json theme={null}
{
"umaAddress": "$john.sender@mycompany.com",
"platformCustomerId": "9f84e0c2a72c4fa",
"customerType": "INDIVIDUAL",
"fullName": "John Sender",
"birthDate": "1985-06-15",
"address": {
"line1": "Paseo de la Reforma 222",
"line2": "Piso 15",
"city": "Ciudad de México",
"state": "Ciudad de México",
"postalCode": "06600",
"country": "MX"
}
}
```
UMA addresses follow the format `$username@domain`. For your platform:
1. The `domain` part will be your configured UMA domain (set in platform configuration)
2. The `username` part can be chosen by you or your customers, following these rules:
* Must start with a \$ symbol. This is to differentiate from email addresses and clearly identify an uma address.
* The `username` portion is limited to a-z0-9-\_.+
* Addresses are case-insensitive, but by convention are written only with lowercase letters
* Like email addresses, the maximum number of characters for the `username` portion of the address is 64 characters (including the \$).
The Grid API validates these requirements and will return an error if they are not met.
### Creating a new business customer (regulated institutions)
```bash theme={null}
curl -sS -X POST "https://api.lightspark.com/grid/2025-10-13/customers" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"umaAddress": "$acme.corp@mycompany.com",
"platformCustomerId": "b87d2e4a9c13f5b",
"customerType": "BUSINESS",
"businessInfo": {
"legalName": "Acme Corporation",
"registrationNumber": "789012345",
"taxId": "123-45-6789"
},
"address": {
"line1": "456 Oak Avenue",
"line2": "Floor 12",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}'
```
### Onboarding customers (unregulated institutions)
Unregulated institutions should initiate a hosted KYC/KYB flow. Generate a link and redirect the customer to complete verification. While KYC is pending, allow account setup but block funding and money movement.
1. Request a hosted KYC link for a customer using your `platformCustomerId` (optional `redirectUri` to return the user to your app when finished):
```bash theme={null}
curl -sS -G "https://api.lightspark.com/grid/2025-10-13/customers/kyc-link" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
--data-urlencode "platformCustomerId=9f84e0c2a72c4fa" \
--data-urlencode "redirectUri=https://app.example.com/onboarding/completed"
```
Response:
```json theme={null}
{
"kycUrl": "https://kyc.grid.example/onboard/abc123",
"platformCustomerId": "9f84e0c2a72c4fa",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001"
}
```
2. Redirect the customer to `kycUrl` to complete KYC/KYB in their locale.
3. After the user is redirected back to your app, they can continue with account setup until KYC review is complete.
4. Handle the KYC status webhook. Grid notifies you when a decision is reached; update your records and unlock funding on APPROVED.
### Handling KYC/KYB Webhooks
After a customer completes the KYC/KYB verification process, you'll receive webhook notifications about their KYC status. These notifications are sent to your configured webhook endpoint.
For regulated platforms, customers are created with `APPROVED` KYC status by default.
**Webhook Payload (sent to your endpoint):**
```json theme={null}
{
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
"type": "KYC_STATUS",
"timestamp": "2023-07-21T17:32:28Z",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"kycStatus": "APPROVED",
"platformCustomerId": "1234567"
}
```
**Webhook Headers:**
* `Content-Type: application/json`
* `X-Webhook-Signature: sha256=abc123...`
System-generated unique identifier of the customer whose KYC status has changed.
Final KYC verification status. Webhooks are only sent for final states:
* `APPROVED`: Customer verification completed successfully
* `REJECTED`: Customer verification was rejected
* `EXPIRED`: KYC verification has expired and needs renewal
* `CANCELED`: Verification process was canceled
* `MANUALLY_APPROVED`: Customer was manually approved by platform
* `MANUALLY_REJECTED`: Customer was manually rejected by platform
Intermediate states like `PENDING_REVIEW` do not trigger webhook notifications. Only final resolution states will send webhook notifications.
```javascript theme={null}
// Example webhook handler for KYC status updates
// Note: Only final states trigger webhook notifications
app.post('/webhooks/kyc-status', (req, res) => {
const { customerId, kycStatus } = req.body;
switch (kycStatus) {
case 'APPROVED':
// Activate customer account
await activateCustomer(customerId);
await sendWelcomeEmail(customerId);
break;
case 'REJECTED':
// Notify support and customer
await notifySupport(customerId, 'KYC_REJECTED');
await sendRejectionEmail(customerId);
break;
case 'MANUALLY_APPROVED':
// Handle manual approval
await activateCustomer(customerId);
await sendWelcomeEmail(customerId);
break;
case 'MANUALLY_REJECTED':
// Handle manual rejection
await notifySupport(customerId, 'KYC_MANUALLY_REJECTED');
await sendRejectionEmail(customerId);
break;
case 'EXPIRED':
// Handle expired KYC
await notifyCustomerForReKyc(customerId);
break;
case 'CANCELED':
// Handle canceled verification
await logKycCancelation(customerId);
break;
default:
// Log unexpected statuses
console.log(`Unexpected KYC status ${kycStatus} for customer ${customerId}`);
}
res.status(200).send('OK');
});
```
## Customer management
### Retrieving customer information
You can retrieve customer information using either the Grid-assigned customer ID or your platform's customer ID:
```bash theme={null}
curl -sS -X GET "https://api.lightspark.com/grid/2025-10-13/customers/{customerId}" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
or list customers with a filter:
```bash theme={null}
curl -sS -G "https://api.lightspark.com/grid/2025-10-13/customers" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
--data-urlencode "umaAddress={umaAddress}" \
--data-urlencode "platformCustomerId={platformCustomerId}" \
--data-urlencode "customerType={customerType}" \
--data-urlencode "createdAfter={createdAfter}" \
--data-urlencode "createdBefore={createdBefore}" \
--data-urlencode "cursor={cursor}" \
--data-urlencode "limit={limit}"
```
Note that this example shows all available filters. You can use any combination of them.
## Data validation
The Grid API performs validation on all customer data. Common validation rules include:
* All required fields must be present based on customer type
* Date of birth must be in YYYY-MM-DD format and represent a valid date
* Names must not contain special characters
* Bank account information must follow country-specific formats
* Addresses must include all required fields including country code
If validation fails, the API will return a 400 Bad Request response with detailed error information.
## Bulk customer import operations
For scenarios where you need to add many customers to the system at once, the API provides a CSV file upload endpoint.
### CSV file upload
For large-scale customer imports, you can upload a CSV file containing customer information:
```bash theme={null}
curl -sS -X POST "https://api.lightspark.com/grid/2025-10-13/customers/bulk/csv" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-F "file=@customers.csv"
```
The CSV file should follow a specific format with required and optional columns based on customer type. Here's an example:
```csv theme={null}
umaAddress,platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,accountType,accountNumber,bankName,platformAccountId,businessLegalName,routingNumber,accountCategory
$john.doe@uma.domain.com,cust_user123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US,US_ACCOUNT,123456789,Chase Bank,chase_primary_1234,,222888888,SAVINGS
$acme@uma.domain.com,cust_biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US,US_ACCOUNT,987654321,Bank of America,boa_business_5678,Acme Corp,121212121,CHECKING
```
CSV Upload Best Practices
1. Use a spreadsheet application to prepare your CSV file
2. Validate data before upload (e.g., date formats, required fields)
3. Include a header row with column names
4. Use UTF-8 encoding for special characters
5. Keep file size under 100MB for optimal processing
You can track the job status through:
1. Webhook notifications (if configured)
2. Status polling endpoint:
```bash theme={null}
curl -sS -X GET "https://api.lightspark.com/grid/2025-10-13/customers/bulk/jobs/{jobId}" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
Example job status response:
```json theme={null}
{
"jobId": "job_123456789",
"status": "PROCESSING",
"progress": {
"total": 5000,
"processed": 2500,
"successful": 2499,
"failed": 1
},
"errors": [
{
"platformCustomerId": "cust_biz456",
"error": {
"code": "validation_error",
"message": "Invalid bank account number"
}
}
]
}
```
# Invitations
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/onboarding-customers/invitations
The Grid API provides an invitation system that allows platform customers to invite others to create accounts and receive payments. This guide explains how to use the invitation system effectively.
See the full [UMA invitations guide](https://docs.lightspark.com/uma-invitations/introduction) for details outside of the context of the Grid API.
## Overview
The invitation system enables:
* Platform users to create invitations with optional payments for others
* Direct prospects to sign up for your services by claiming an invitation to receive payment
* Tracking of invitation status (pending, claimed, expired)
* Webhook notifications when invitations are claimed
## Setup
Before starting your implementation, we recommend configuring UMAs and invitations in the dashboard.
You will need to provide:
* A name and a logo to be displayed in the invitation page.
* A list of countries you operate UMA in.
* An onboarding URL for people who want to create an account with you (this URL must support invitation codes). Example: `http://myplatform.com/uma?code=INVITATION_CODE`
* A webhook URL to get notified when your invitations get claimed.
## Creating Invitations
To create an invitation, make a POST request to `/invitations` with the inviter's UMA address:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/invitations" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"inviterUma": "$inviter@uma.domain",
"expiresAt": "2024-12-31T23:59:59Z"
}'
```
The response will include a unique invitation code that can be shared with the invitee:
```json theme={null}
{
"code": "019542f5",
"createdAt": "2023-09-01T14:30:00Z",
"inviterUma": "$inviter@uma.domain",
"url": "https://uma.me/i/019542f5",
"status": "PENDING"
}
```
The `url` field is the URL where the invitee can claim the invitation. For example, for the response above, you might want to generate a share message
for the user with text like: "Get an UMA address so that I can send you some money! `https://uma.me/i/019542f5`". The inviter can then share this URL with the invitee.
When the invitee clicks the URL, they will be presented with a list of UMA providers available in their region. The invitee can select one of the providers and onboard to create their UMA address.
## Pay-by-Link Invitations
The Grid API supports a "pay-by-link" feature that allows users to create invitations that include a payment amount. This is useful for scenarios where you want to send money to someone who doesn't yet have an UMA address or where the sender doesn't know the receiver's UMA address. They can simply share a link via email, SMS, whatsapp, or other channels to send money.
To create a pay-by-link invitation, include the `amountToSend` field when creating the invitation:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/invitations" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"inviterUma": "$inviter@uma.domain",
"amountToSend": 5000,
"expiresAt": "2024-12-31T23:59:59Z"
}'
```
Assuming the user's currency is USD, this example creates an invitation that will send \$50 USD to the invitee when
they claim it. The response will include the payment amount in the invitation details:
```json theme={null}
{
"code": "019542f5",
"createdAt": "2023-09-01T14:30:00Z",
"inviterUma": "$inviter@uma.domain",
"url": "https://uma.me/i/019542f5",
"status": "PENDING",
"amountToSend": {
"amount": 5000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
}
}
```
When the invitee claims the invitation, your platform will receive an `INVITATION_CLAIMED` webhook. At this point, you should:
1. Check the `amountToSend` field in the webhook payload
2. Create a quote for the payment amount (sender-locked)
3. Execute the payment to the invitee's UMA address
Note that the actual sending of the payment must be done by your platform after receiving the webhook. If your platform either does not send the payment or the payment fails, the invitee will not receive the amount. The `amountToSend` field is primarily used for display purposes on the claiming side of the invitation.
These payments can only be sender-locked, meaning that the sender will not know ahead of time how much the receiver will receive in their local currency. The exchange rate will be determined at the time the payment is executed. If you'd like, you can also send a push notification to your sending user when you receive the `INVITATION_CLAIMED` webhook and have them approve the payment interactively instead.
### Best practices
1. Always set an expiration time for invitations with a payment amount to avoid huge swings in expected exchange rates or leaked links.
2. Allow the inviter to cancel the invitation if they want to avoid sending a payment to the wrong person if the link is leaked.
3. Notify the inviter when the invitation is claimed so that they can see the amount received by the invitee.
## Claiming Invitations
Once onboarding (or login from an invite link) is complete, the invitee's new VASP (which may or may not be the same as the inviter's Grid API platform) will need to claim the invitation by making a
POST request to `/invitations/{invitationCode}/claim` including the invitee's newly-created UMA address:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/invitations/019542f5/claim" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"inviteeUma": "$invitee@uma.domain"
}'
```
A successful claim will:
1. Associate the invitee's UMA address with the invitation
2. Change the invitation status from `PENDING` to `CLAIMED`
3. Trigger an `INVITATION_CLAIMED` webhook to notify the inviter's platform of the claim
## Cancelling Invitations
To cancel a pending invitation, make a POST request to `/invitations/{invitationCode}/cancel`:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/invitations/019542f5/cancel" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
A successful cancellation will:
1. Change the invitation status from `PENDING` to `CANCELLED`
2. Make the invitation URL show as cancelled when accessed
3. Prevent the invitation from being claimed
Only the inviter or platform can cancel an invitation, and only pending invitations can be cancelled. Attempting to cancel an invitation that is already claimed, expired, or cancelled will result in an error.
Example error response for an already claimed invitation:
```json theme={null}
{
"code": "invitation_already_claimed",
"message": "This invitation has already been claimed and cannot be cancelled",
"details": {
"status": "CLAIMED",
"inviteeUma": "$invitee@uma.domain"
}
}
```
## Invitation Status
An invitation can be in one of four states:
* `PENDING`: The invitation has been created but not yet claimed
* `CLAIMED`: The invitation has been successfully claimed by an invitee
* `EXPIRED`: The invitation has expired and can no longer be claimed
* `CANCELLED`: The invitation has been cancelled by the inviter or platform
You can check the status of an invitation at any time by making a GET request to `/invitations/{invitationCode}`.
## Webhook Integration
When an invitation is claimed, the Grid API will send an `INVITATION_CLAIMED` webhook to your configured webhook endpoint. This allows you to:
* Track invitation usage and conversion rates
* Apply referral bonuses or rewards to the inviter
* Update your UI to reflect the claimed status
Example webhook payload:
```json theme={null}
{
"invitation": {
"code": "019542f5",
"createdAt": "2023-09-01T14:30:00Z",
"claimedAt": "2023-09-01T15:45:00Z",
"inviterUma": "$inviter@uma.domain",
"inviteeUma": "$invitee@uma.domain",
"url": "https://uma.me/i/019542f5",
"status": "CLAIMED"
},
"timestamp": "2023-09-01T15:45:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000008",
"type": "INVITATION_CLAIMED"
}
```
See the [Webhooks Guide](../platform-tools/webhooks) for more information about webhook security and implementation.
## Best Practices
1. **Expiration Times**: Consider setting appropriate expiration times for invitations based on your use case
2. **User Experience**: Provide clear feedback to users about invitation status and next steps
3. **Monitoring**: Track invitation metrics to understand user acquisition patterns
## Error Handling
Common error scenarios to handle:
* Invalid invitation code
* Expired invitation
* Already claimed invitation
* Rate limit exceeded
* Missing required fields
Example error response:
```json theme={null}
{
"code": "invitation_expired",
"message": "This invitation has expired and cannot be claimed",
"details": {
"expirationTime": "2023-08-31T23:59:59Z"
}
}
```
# Postman Collection
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/platform-tools/postman-collection
Use our hosted Postman collection to explore endpoints and send test requests quickly.
Launch the collection in Postman.
# Sandbox Testing
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/platform-tools/sandbox-testing
The Grid sandbox environment allows you to test your integration without making real payments. When you set up your account, you can configure production and sandbox API tokens. The sandbox token is specifically for testing and development purposes.
It corresponds to a separate platform instance in "sandbox" mode, which can only transact with the sandbox UMA addresses for testing.
## Overview
The sandbox environment provides:
1. A dedicated sandbox platform for testing
2. Test UMA addresses for simulating payments
3. Endpoints to simulate sending and receiving payments
4. All the same webhooks and flows as production, but with simulated funds
## Test UMA Addresses
The sandbox provides several test UMA addresses you can use to simulate different scenarios:
| UMA Address | Description |
| ---------------------------------------- | ---------------------------------- |
| `$success.usd@sandbox.uma.money` | Always succeeds, sends USD |
| `$success.eur@sandbox.uma.money` | Always succeeds, sends EUR |
| `$success.mxn@sandbox.uma.money` | Always succeeds, sends MXN |
| `$pending.long.usd@sandbox.uma.money` | Simulates a long-pending payment |
| `$fail.compliance.usd@sandbox.uma.money` | Simulates compliance check failure |
## Testing Outgoing Payments
To test sending payments from your platform, follow these steps:
```mermaid theme={null}
sequenceDiagram
participant Client as Your Platform
participant Grid as Grid Sandbox
participant Test as Test UMA Address
Note over Client, Grid: Testing Outgoing Payments
Client->>Grid: GET /receiver/$success.usd@sandbox.uma.money
Grid-->>Client: Supported currencies and requirements
Client->>Grid: POST /quotes
Grid-->>Client: Quote with payment instructions
Client->>Grid: POST /sandbox/send
Grid-->>Client: Payment simulated
Grid->>Client: Webhook: OUTGOING_PAYMENT (COMPLETED)
Note over Client, Grid: Testing Incoming Payments
Client->>Grid: POST /sandbox/uma/receive
Grid->>Client: Webhook: INCOMING_PAYMENT (PENDING)
Client-->>Grid: HTTP 200 OK (approve payment)
Grid->>Client: Webhook: INCOMING_PAYMENT (COMPLETED)
```
1. Look up a sandbox UMA address:
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$success.usd@sandbox.uma.money" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
2. Create a quote as normal:
```bash theme={null}
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 '{
"lookupId": "Lookup:019542f5-b3e7-1d02-0000-000000000009",
"sendingCurrencyCode": "MXN",
"receivingCurrencyCode": "USD",
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000
}'
```
3. Instead of making a real bank transfer, use the sandbox send endpoint:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/sandbox/send" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"reference": "UMA-Q12345-REF",
"currencyCode": "USD",
"currencyAmount": 10000
}'
```
The sandbox will simulate the payment and send appropriate webhooks just like in production.
## Testing Incoming Payments
To test receiving payments to your platform's users, use the sandbox receive endpoint:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/sandbox/uma/receive" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"senderUmaAddress": "$success.usd@sandbox.uma.money",
"receiverUmaAddress": "$your.user@your.domain",
"receivingCurrencyCode": "USD",
"receivingCurrencyAmount": 5000
}'
```
This will trigger the same webhook flow as a real incoming payment:
1. You'll receive an `INCOMING_PAYMENT` webhook with `status: "PENDING"`
2. Your platform should approve/reject the payment
3. On approval, you'll receive another webhook with `status: "COMPLETED"`
## Example Testing Flow
Here's a complete example of testing both directions of payments:
1. First, register a test user:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/users" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"umaAddress": "$test.user@your.domain",
"platformUserId": "test_123",
"userType": "INDIVIDUAL",
"fullName": "Test User",
"birthDate": "1990-01-01",
"address": {
"line1": "123 Test St",
"city": "Testville",
"state": "TS",
"postalCode": "12345",
"country": "US"
}
}'
```
2. Test receiving a payment:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/sandbox/uma/receive" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"senderUmaAddress": "$success.usd@sandbox.uma.money",
"receiverUmaAddress": "$test.user@your.domain",
"receivingCurrencyCode": "USD",
"receivingCurrencyAmount": 5000
}'
```
3. Test sending a payment:
```bash theme={null}
# 1. Look up recipient
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$success.usd@sandbox.uma.money" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
# 2. Create quote
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 '{
"lookupId": "Lookup:019542f5-b3e7-1d02-0000-000000000009",
"sendingCurrencyCode": "MXN",
"receivingCurrencyCode": "USD",
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000
}'
# 3. Simulate sending payment
curl -X POST "https://api.lightspark.com/grid/2025-10-13/sandbox/send" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"reference": "UMA-Q12345-REF",
"currencyCode": "USD",
"currencyAmount": 10000
}'
```
## Testing Error Scenarios
You can test various error scenarios using the special sandbox UMA addresses:
1. Test compliance failures:
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$fail.compliance.usd@sandbox.uma.money" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
# ... create quote and attempt payment
```
2. Test long-pending payments:
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$pending.long.usd@sandbox.uma.money" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
# ... create quote and attempt payment
```
3. Non-existent UMA address:
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$non.existent.usd@sandbox.uma.money" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
# ... should return 404 Not Found
```
Each of these will trigger appropriate error webhooks and status updates to help you test your error handling.
## Production vs Sandbox
Here are the key differences between production and sandbox environments:
1. **API Tokens**: Sandbox tokens only work in the sandbox environment and vice versa
2. **Bank Transfers**: In sandbox, you use `/sandbox/send` instead of real bank transfers
3. **Test UMA Addresses**: Special sandbox addresses for testing different scenarios
4. **Money**: No real money is moved in sandbox
Always test thoroughly in sandbox before moving to production!
# UMA Test Wallet
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/platform-tools/uma-test-wallet
Test UMA payment flows with a real counterparty
Grid provides an UMA Test Wallet to help you test UMA payment flows with a real counterparty.
The UMA Test Wallet is an external tool that demonstrates UMA payment flows end to end and gives you a realistic counterparty for development and QA. It helps you understand flows through hands-on interaction, explore recommended UX patterns, and develop against a live UMA FI.
Open the hosted test wallet to try UMA flows in your browser.
Browse the code, file issues, and contribute improvements.
### What UMA Test Wallet can do
* **Experience UMA flows**: Send and receive cross currency UMA payments
* **Preview UX best practices**: See recommended entry points, confirmations, and error handling.
* **Develop and test**: Use the wallet as a counterparty FI when building UMA integrations
For background on UMA itself, see the UMA Standard: [UMA Standard—Introduction](https://docs.uma.me/uma-standard/introduction).
# Webhooks
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/platform-tools/webhooks
Security best practices for webhook verification
All webhooks sent by the Grid API include a signature in the `X-Grid-Signature` header, which allows you to verify the authenticity of the webhook. This is critical for security, as it ensures that only legitimate webhooks from Grid are processed by your system.
## Signature Verification Process
1. **Obtain your Grid public key**
* This is provided to you during the integration process. Reach out to us at [support@lightspark.com](mailto:support@lightspark.com) or over Slack to get the public key.
* The key is in PEM format and can be used with standard cryptographic libraries
2. **Verify incoming webhooks**
* Extract the signature from the `X-Grid-Signature` header
* Decode the base64 signature
* Create a SHA-256 hash of the entire request body
* Verify the signature using the Grid webhook public key and the hash
* Only process the webhook if the signature verification succeeds
## Verification Examples
### Node.js Example
```javascript theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();
// Your Grid public key provided during integration
const GRID_WEBHOOK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----`;
app.post('/webhooks/uma', (req, res) => {
const signatureHeader = req.header('X-Grid-Signature');
if (!signatureHeader) {
return res.status(401).json({ error: 'Signature missing' });
}
try {
let signature: Buffer;
try {
// Parse the signature as JSON. It's in the format {"v": "1", "s": "base64_signature"}
const signatureObj = JSON.parse(signatureHeader);
if (signatureObj.v && signatureObj.s) {
// The signature is in the 's' field
signature = Buffer.from(signatureObj.s, "base64");
} else {
throw new Error("Invalid JSON signature format");
}
} catch {
// If JSON parsing fails, treat as direct base64
signature = Buffer.from(signatureHeader, "base64");
}
// Create verifier with the public key and correct algorithm
const verifier = crypto.createVerify("SHA256");
const payload = await request.text();
verifier.update(payload);
verifier.end();
// Verify the signature using the webhook public key
const isValid = verifier.verify(
{
key: GRID_WEBHOOK_PUBLIC_KEY,
format: "pem",
type: "spki",
},
signature,
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Webhook is verified, process it based on type
const webhookData = req.body;
if (webhookData.type === 'INCOMING_PAYMENT') {
// Process incoming payment webhook
// ...
} else if (webhookData.type === 'OUTGOING_PAYMENT') {
// Process outgoing payment webhook
// ...
}
// Acknowledge receipt of the webhook
return res.status(200).json({ received: true });
} catch (error) {
console.error('Signature verification error:', error);
return res.status(401).json({ error: 'Signature verification failed' });
}
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
```
### Python Example
```python theme={null}
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
from flask import Flask, request, jsonify
import base64
app = Flask(__name__)
# Your Grid public key provided during integration
GRID_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----"""
# Load the public key
public_key = serialization.load_pem_public_key(
GRID_PUBLIC_KEY.encode('utf-8')
)
@app.route('/webhooks/uma', methods=['POST'])
def handle_webhook():
# Get signature from header
signature = request.headers.get('X-Grid-Signature')
if not signature:
return jsonify({'error': 'Signature missing'}), 401
try:
# Get the raw request body
request_body = request.get_data()
# Create a SHA-256 hash of the request body
hash_obj = hashes.Hash(hashes.SHA256())
hash_obj.update(request_body)
digest = hash_obj.finalize()
# Decode the base64 signature
signature_bytes = base64.b64decode(signature)
# Verify the signature
try:
public_key.verify(
signature_bytes,
request_body,
ec.ECDSA(hashes.SHA256())
)
except Exception as e:
return jsonify({'error': 'Invalid signature'}), 401
# Webhook is verified, process it based on type
webhook_data = request.json
if webhook_data['type'] == 'INCOMING_PAYMENT':
# Process incoming payment webhook
# ...
pass
elif webhook_data['type'] == 'OUTGOING_PAYMENT':
# Process outgoing payment webhook
# ...
pass
# Acknowledge receipt of the webhook
return jsonify({'received': True}), 200
except Exception as e:
print(f'Signature verification error: {e}')
return jsonify({'error': 'Signature verification failed'}), 401
if __name__ == '__main__':
app.run(port=3000)
```
## Testing
To test your webhook implementation, you can trigger a test webhook from the Grid dashboard. This will send a test webhook to the endpoint you provided during the integration process. The test webhook will also be sent automatically when you update your platform configuration with a new webhook URL.
An example of the test webhook payload is shown below:
```json theme={null}
{
"test": true,
"timestamp": "2023-08-15T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
"type": "TEST"
}
```
You should verify the signature of the webhook using the Grid public key and the process outlined in the [Signature Verification Process](#signature-verification-process) section and then reply with a 200 OK response to acknowledge receipt of the webhook.
## Security Considerations
* **Always verify signatures**: Never process webhooks without verifying their signatures.
* **Use HTTPS**: Ensure your webhook endpoint uses HTTPS to prevent man-in-the-middle attacks.
* **Implement idempotency**: Use the `webhookId` field to prevent processing duplicate webhooks.
* **Timeout handling**: Implement proper timeout handling and respond to webhooks promptly.
## Retry Policy
The Grid API will retry webhooks with the following policy based on the webhook type:
| Webhook Type | Retry Policy | Notes |
| ------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| TEST | No retries | Used for testing webhook configuration |
| OUTGOING\_PAYMENT | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| INCOMING\_PAYMENT | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on: 409 (duplicate webhook) or PENDING status since it is served as an approval mechanism in-flow |
| BULK\_UPLOAD | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| INVITATION\_CLAIMED | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| KYC\_STATUS | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| ACCOUNT\_STATUS | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
# Quickstart
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/quickstart
Send your first cross-border payment
With Global P2P you can send and receive payments in any supported fiat or crypto currency. This quickstart guides you through a regulated FI sending an individual customer payment from the US to a bank account in Mexico using USDC just-in-time funding. For examples funding with real time fiat rails, see the [Sending Payments](/global-p2p/sending-receiving-payments/sending-payments) guide.
## Understanding Entity Mapping for Remittances
In this guide, the entities map as follows:
| Entity Type | Who They Are | In This Example |
| -------------------- | ----------------------------------- | ------------------------------------------------------------------ |
| **Platform** | Your remittance service | Your money transfer app |
| **Customer** | Both sender and recipient | Alice (sender in US), Carlos (recipient in Mexico) |
| **External Account** | Bank accounts for funding/receiving | Alice's US bank (funding), Carlos's Mexican bank (receiving funds) |
**Flow**: Alice (customer) funds a transfer from her external account → to Carlos (also a customer) → who receives funds in his external bank account. Alternatively, Alice could send directly to Carlos's UMA address if he has one.
## Get API credentials
Create a Sandbox API credentialsin the dashboard, then set environment variables for local use.
```bash theme={null}
export GRID_BASE_URL="https://api.lightspark.com/grid/2025-10-13"
export GRID_CLIENT_ID="YOUR_SANDBOX_CLIENT_ID"
export GRID_CLIENT_SECRET="YOUR_SANDBOX_CLIENT_SECRET"
```
Use Basic Auth in cURL with `-u "$GRID_CLIENT_ID:$GRID_API_SECRET"`.
## Create a customer
Register a customer who will send the payment. You can provide your own UMA handle or let the system generate one.
```bash theme={null}
curl -sS -X POST "https://api.lightspark.com/grid/2025-10-13/customers" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"platformCustomerId": "cust_7b3c5a89d2f1e0",
"customerType": "INDIVIDUAL",
"umaAddress": "$alice@yourapp.example",
"fullName": "Alice Smith",
"address": {
"line1": "123 Pine Street",
"city": "Seattle",
"state": "WA",
"postalCode": "98101",
"country": "US"
}
}'
```
Response:
```json theme={null}
{
"id": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"customerType": "INDIVIDUAL",
"umaAddress": "$alice@yourapp.example",
"platformCustomerId": "cust_7b3c5a89d2f1e0"
}
```
## Create an external receiving bank account (CLABE in MX)
Add a beneficiary account in Mexico using their CLABE. We attach it to the same customer for this example.
```bash theme={null}
curl -sS -X POST "https://api.lightspark.com/grid/2025-10-13/customers/external-accounts" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"currency": "MXN",
"platformAccountId": "mx_beneficiary_001",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"accountInfo": {
"accountType": "CLABE",
"clabeNumber": "123456789012345678",
"bankName": "BBVA Mexico",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Carlos Pérez",
"birthDate": "1985-03-15",
"nationality": "MX",
"address": {
"line1": "Av. Reforma 123",
"city": "Ciudad de México",
"state": "CDMX",
"postalCode": "06600",
"country": "MX"
}
}
}
}'
```
Response:
```json theme={null}
{
"id": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "MXN",
"status": "ACTIVE",
"platformAccountId": "mx_beneficiary_001",
"accountInfo": {
"accountType": "CLABE",
"clabeNumber": "123456789012345678",
"bankName": "BBVA Mexico",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Carlos Pérez",
"birthDate": "1985-03-15",
"nationality": "MX",
"address": {
"line1": "Av. Reforma 123",
"city": "Ciudad de México",
"state": "CDMX",
"postalCode": "06600",
"country": "MX"
}
}
}
}
```
## Create a quote (just‑in‑time funding)
Quote a transfer of 100 USD from the customer to the MXN CLABE account. The quote returns the exchange rate, fees, and `paymentInstructions` to fund in real time.
```bash theme={null}
curl -sS -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"source": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "USDC"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "MXN"
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000,
"description": "Remittance to MX beneficiary"
}'
```
Response:
```json theme={null}
{
"id": "Quote:019542f5-b3e7-1d02-0000-000000000006",
"exchangeRate": 16.85,
"fees": { "amount": 50, "currency": { "code": "USD", "decimals": 2 } },
"paymentInstructions": [
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"address": "0x1234567890123456789012345678901234567890",
"assetType": "USDC"
}
},
{
"accountOrWalletInfo": {
"accountType": "BASE_WALLET",
"address": "0x1234567890123456789012345678901234567890",
"assetType": "USDC"
}
}
],
"status": "PENDING"
}
```
## Fund the quote (Sandbox simulation)
In production, you would trigger a payment on one of the supported blockchains to the provided address. In Sandbox, you can mock funding using the simulate send endpoint.
```bash theme={null}
curl -sS -X POST "https://api.lightspark.com/grid/2025-10-13/sandbox/send" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"reference": "UMA-Q12345-REF",
"currencyCode": "USDC",
"currencyAmount": 10000
}'
```
Response:
```json theme={null}
{
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000005",
"status": "PROCESSING",
"type": "OUTGOING",
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006"
}
```
## Handle the outgoing payment webhook
Implement a webhook endpoint to receive status updates as the payment moves from pending to completed (or failed). Verify the `X-Grid-Signature` header using the public key provided during onboarding.
Webhook event:
```json theme={null}
{
"transaction": {
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000005",
"status": "COMPLETED",
"type": "OUTGOING",
"senderUmaAddress": "$alice@yourapp.example",
"receivedAmount": { "amount": 9706, "currency": { "code": "MXN", "decimals": 2 } },
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006",
},
"timestamp": "2025-01-15T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
"type": "OUTGOING_PAYMENT"
}
```
# Depositing Funds
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/sending-receiving-payments/depositing-funds
Depositing funds into internal accounts
Grid provides two options to fund an account:
* Prefund
* Just-in-time funding
With prefunding, you'll deposit funds into internal accounts via Wire, PIX, or crypto transfers. You can then use the balances as the source of funds for quotes and transfers.
With just-in-time funding, you'll receive payment instructions as part of the quote. Once funds arrive, the payment to the receiver is automatically initiated.
Just-in-time funding supports instant payment rails only (for example: RTP,
PIX, SEPA Instant).
## Prerequisites
* You have created a customer (for customer-scoped internal accounts)
* You have `GRID_CLIENT_ID` and `GRID_CLIENT_SECRET`
Export your credentials for use with cURL:
```bash theme={null}
export GRID_CLIENT_ID="your_client_id"
export GRID_CLIENT_SECRET="your_client_secret"
```
## Prefunding an account via push payments (Wire, SEPA, PIX, etc.)
When customers need to deposit funds themselves, display the funding payment instructions in your application:
Fetch the customer's internal account for their desired currency using the API.
```bash cURL (Customer accounts) theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```bash cURL (Platform internal accounts) theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Parse the `fundingPaymentInstructions` array and select the appropriate instructions based on your customer's preferred payment method.
```javascript theme={null}
const instructions = account.fundingPaymentInstructions[0];
const bankInfo = instructions.accountOrWalletInfo;
```
Show the payment details prominently in your UI and enable copy / paste:
* Account holder name
* Bank name and routing information (account/routing number, CLABE, PIX key, etc.)
* Reference code (if provided)
* Any additional notes from `instructionsNotes`
Customer initiates a push payment from their bank or wallet to the account/address specified.
Set up webhook listeners to receive updates for the deposit transaction and account balance updates. The account balance will update automatically.
You'll receive `ACCOUNT_STATUS` webhook events when the internal account balance changes.
## Just-in-time funding (payment instructions from a quote)
With just-in-time funding, you request a quote and receive payment instructions (for example, a bank account or instant rail details). When your customer confirms the transaction, you trigger payment from your app.
More details of just-in-time funding can be found in the Sending Payments guides.
# Error Handling
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/sending-receiving-payments/error-handling
Handle payment failures, API errors, and transaction issues in Global P2P
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 transaction, quote, or customer 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 |
| `409 Conflict` | Resource conflict | Quote already executed, external account already exists |
| `412 Precondition Failed` | UMA version mismatch | Counterparty doesn't support required UMA version |
| `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 support) |
| `501 Not Implemented` | Not implemented | Feature not yet supported |
## API error responses
All error responses include a structured format:
```json theme={null}
{
"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 format
**Solution:** Check request parameters match API specification
```javascript theme={null}
// 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 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;
}
}
```
**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 createTransferOut({ accountId, amount });
}
```
**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;
}
```
## 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",
"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
```json theme={null}
{
"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
```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) {
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
```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
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;
}
}
```
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,
};
}
```
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.",
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:
```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
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;
}
```
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 createTransferOut(params);
processedTransactions.add(idempotencyKey);
return result;
} catch (error) {
// Don't mark as processed on error
throw error;
}
}
```
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
Learn how to send payments from internal accounts
Query and filter payment history
Match payments with your internal systems
# Receiving Payments
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/sending-receiving-payments/receiving-payments
Receiving payments from UMA addresses
This guide explains how to enable your customers to receive UMA payments. When an external sender initiates a payment to your customer's UMA address, the Grid API requests the counterparty fields you defined in your platform configuration. You can review the counterparty and transaction details for risk before approving the payment.
Before you begin, make sure you:
* Configure UMA, supported currencies, and required counterparty fields in Platform Configuration
* Create customers and capture any provider-required user fields in Creating Customers
* Set up and verify webhooks in Webhooks
## Receive webhook for initiated payment
When someone initiates a payment to one of your users' UMA addresses, you'll receive a webhook call with a pending transaction:
```json theme={null}
{
"transaction": {
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000005",
"status": "PENDING",
"type": "INCOMING",
"senderUmaAddress": "$mary.sender@thelessgoodbank.com",
"receiverUmaAddress": "$john.receiver@thegoodbank.com",
"receivedAmount": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"userId": "User:019542f5-b3e7-1d02-0000-000000000001",
"platformUserId": "9f84e0c2a72c4fa",
"description": "Payment for services",
"counterpartyInformation": {
"FULL_NAME": "Mary Sender",
"BIRTH_DATE": "1985-06-15"
},
"reconciliationInstructions": {
"reference": "REF-123456789"
}
},
"requestedReceiverUserInfoFields": [
{ "name": "COUNTRY_OF_RESIDENCE", "mandatory": true },
{ "name": "FULL_NAME", "mandatory": true },
{ "name": "NATIONALITY", "mandatory": false }
],
"timestamp": "2023-08-15T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
"type": "INCOMING_PAYMENT"
}
```
The `counterpartyInformation` object contains PII about the sender, provided by their FI, based on your configured `requiredCounterpartyFields`. If present, `requestedReceiverUserInfoFields` lists information needed about your user to proceed. Provide those fields when approving.
You can approve or reject the payment synchronously (recommended) or asynchronously:
### Option 1: Synchronous (recommended)
To approve the payment synchronously, respond with a `200 OK` status:
* If the `requestedReceiverUserInfoFields` array was present in the webhook request and contained mandatory fields, your `200 OK` response **must** include a JSON body containing a `receiverUserInfo` object. This object should contain the key-value pairs for the information fields that were requested.
* If `requestedReceiverUserInfoFields` was not present, was empty, or contained only non-mandatory fields for which you have no information, your `200 OK` response can have an empty body.
Example `200 OK` response body when information was requested and provided:
```json theme={null}
{
"receiverUserInfo": {
"COUNTRY_OF_RESIDENCE": "US",
"FULL_NAME": "John Receiver"
}
}
```
To reject the payment, respond with a 403 Forbidden status and a JSON body with the following fields:
```json theme={null}
{
"code": "payment_rejected",
"message": "Payment rejected due to compliance policy",
"details": {
"reason": "failed_counterparty_check",
"rejectionReason": "User is in a restricted jurisdiction"
}
}
```
### Option 2: Asynchronous Processing
If your platform's architecture requires asynchronous processing before approving or rejecting the payment, you can:
1. Return a `202 Accepted` response to acknowledge receipt of the webhook
2. Process the payment asynchronously
3. Call either the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint *within 5 seconds*
Example of approving asynchronously:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/transactions/Transaction:019542f5-b3e7-1d02-0000-000000000005/approve" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"receiverUserInfo": {
"COUNTRY_OF_RESIDENCE": "US",
"FULL_NAME": "John Receiver"
}
}'
```
Example of rejecting asynchronously:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/transactions/Transaction:019542f5-b3e7-1d02-0000-000000000005/reject" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"reason": "RESTRICTED_JURISDICTION"
}'
```
If you choose the asynchronous path, you must call the approve/reject endpoint within 5 seconds, or the payment will be automatically rejected.
## Receive completion notification and credit
When the payment completes, you'll receive another webhook notifying you of the completion:
```json theme={null}
{
"transaction": {
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000005",
"status": "COMPLETED",
"type": "INCOMING",
"senderUmaAddress": "$mary.sender@thelessgoodbank.com",
"receiverUmaAddress": "$john.receiver@thegoodbank.com",
"receivedAmount": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"userId": "User:019542f5-b3e7-1d02-0000-000000000001",
"platformUserId": "9f84e0c2a72c4fa",
"settlementTime": "2023-08-15T14:30:00Z",
"createdAt": "2023-08-15T14:25:18Z",
"description": "Payment for services",
"counterpartyInformation": {
"FULL_NAME": "Mary Sender",
"BIRTH_DATE": "1985-06-15"
},
"reconciliationInstructions": {
"reference": "REF-123456789"
}
},
"timestamp": "2023-08-15T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
"type": "INCOMING_PAYMENT"
}
```
On completion, the received funds are immediately credited to the account associated with the UMA customer. By default, funds
land in the customer's primary internal account. However, you can also set an external account as the default UMA deposit account
for the customer, which will cause incoming UMA payments to be deposited into that external account instead. This is useful for
customers who prefer to receive UMA payments into a specific bank account directly, for example.
To set an external account as the default UMA deposit account for a customer, you can set the `defaultUmaDepositAccount` flag to true when creating the external account.
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/customers/external-accounts" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "USD",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "9876543210",
"routingNumber": "123456789",
"accountCategory": "CHECKING",
"bankName": "Chase Bank"
},
"defaultUmaDepositAccount": true
}'
```
## Test the inbound flow
Use the UMA Test Wallet to send a regtest payment to your customer's UMA address.
# Reconciliation
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/sending-receiving-payments/reconciliation
Reconciliation of payments
This guide explains how to reconcile transactions between your internal systems and the Grid API using two complementary mechanisms: real-time webhooks and periodic queries.
Use webhooks for real-time updates and daily queries as a backstop to detect missed events or data drift.
## Handling webhooks
Listen for transaction webhooks to track transaction status change until a terminal state is reached.
Terminal statuses: `COMPLETED`, `REJECTED`, `FAILED`, `REFUNDED`, `EXPIRED`.
All other statuses will progress until reaching one of the above.
* **Outbound transactions**: The originating account is debited at transaction creation. If the transaction ultimately fails, a refund is posted back to the originating account.
* **Inbound transactions**: The receiving account is credited only on success. Failures do not change balances.
Grid retries failed webhooks up to 160 times over 7 days with exponential backoff. Use the dashboard to review and remediate webhook delivery issues.
Configure your webhook endpoint and verify signatures. See Webhooks.
Sample webhook payload:
```json theme={null}
{
"transaction": {
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
"status": "COMPLETED",
"type": "OUTGOING",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "EUR"
},
"sentAmount": {
"amount": 10000,
"currency": { "code": "USD", "symbol": "$", "decimals": 2 }
},
"receivedAmount": {
"amount": 9200,
"currency": { "code": "EUR", "symbol": "€", "decimals": 2 }
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"platformCustomerId": "customer_12345",
"createdAt": "2025-10-03T15:00:00Z",
"settledAt": "2025-10-03T15:30:00Z",
"description": "Payment for services - Invoice #1234"
},
"timestamp": "2025-10-03T15:30:01Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-0000000000ab",
"type": "OUTGOING_PAYMENT"
}
```
Use the `webhookId`, `transaction.id`, and `timestamp` to ensure idempotent handling, updating your internal ledger on each status transition.
When a transaction reaches a terminal state, finalize your reconciliation for that transaction.
## Reconcile via queries
Additionally, you can list transactions for a time window and compare with your internal records.
We recommend querying days from `00:00:00.000` to `23:59:59.999` in your preferred timezone.
```bash cURL theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?startDate=2025-10-01T00:00:00.000Z&endDate=2025-10-01T23:59:59.999Z&limit=100' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Response
```json theme={null}
{
"data": [
{
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
"status": "COMPLETED",
"type": "OUTGOING",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "EUR"
},
"sentAmount": {
"amount": 10000,
"currency": { "code": "USD", "symbol": "$", "decimals": 2 }
},
"receivedAmount": {
"amount": 9200,
"currency": { "code": "EUR", "symbol": "€", "decimals": 2 }
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"platformCustomerId": "customer_12345",
"description": "Payment for services - Invoice #1234",
"exchangeRate": 0.92,
"settledAt": "2025-10-03T15:30:00Z",
"createdAt": "2025-10-03T15:00:00Z"
}
],
"hasMore": true,
"nextCursor": "eyJpZCI6IlRyYW5zYWN0aW9uOjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAzMCJ9",
"totalCount": 45
}
```
## Troubleshooting
* **Missing webhook**: Check delivery logs in the dashboard and ensure your endpoint returns `2xx`. Retries continue for 7 days.
* **Mismatched balances**: Re-query the date range and verify terminal statuses; remember outbound failures are refunded, inbound failures do not change balances.
* **Pagination gaps**: Always follow `nextCursor` until `hasMore` is `false`.
# Sending Payments
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/sending-receiving-payments/sending-payments
This guide covers three methods to send payments:
1. Same-currency transfer to an external account
2. Cross-currency transfer with a quote
3. Sending to an UMA address
## Choosing the right method
* **Same-currency**: Best for domestic payouts when sender and recipient use the same currency. Uses local payment rails (e.g., RTP, SEPA Instant, PIX, FPS) for low cost and fast settlement.
* **Cross-currency**: Use when conversion is required or when paying globally across borders. Also supports sending to a crypto wallet address when configured.
* **UMA**: Send using a Universal Money Address. Ideal for global counterparties on networks.
## 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
Retrieve the internal account (source) and external account (destination) IDs:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Note the `id` fields from both the internal and external accounts you want to use.
Create the transfer by specifying the source and destination accounts:
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/transfer-out' \
-u "$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
}'
```
```json Success (201 Created) theme={null}
{
"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", "decimals": 2 } },
"receivedAmount": { "amount": 12550, "currency": { "code": "USD", "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.
The transaction is created with a `PENDING` status. Monitor the status by:
**Option 1: Webhook notifications** (recommended)
```json theme={null}
{
"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**
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions/Transaction:019542f5-b3e7-1d02-0000-000000000015' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
When the transaction status changes to `COMPLETED`, the funds have been successfully transferred to the external account.
## 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, USDC, USDT to EUR, MXN, BRL, BTC, or other supported fiat and crypto currencies
* Sending international payments with automatic currency conversion
* Need to lock in a specific exchange rate for the transfer
### Create and execute a quote
Request a quote to lock in the exchange rate and get transfer details:
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
-u "$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"
}'
```
```json Success (201 Created) theme={null}
{
"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", "decimals": 2 } },
"receivingAmount": { "amount": 9200, "currency": { "code": "EUR", "decimals": 2 } },
"exchangeRate": 0.92,
"fee": { "amount": 50, "currency": { "code": "USD", "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)
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 a short period. If expired, create a new quote to get an updated exchange rate.
Confirm and execute the quote to initiate the transfer:
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes/Quote:019542f5-b3e7-1d02-0000-000000000025/execute' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json Success (200 OK) theme={null}
{
"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", "decimals": 2 } },
"receivingAmount": { "amount": 9200, "currency": { "code": "EUR", "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.
Track the transfer using webhooks or by polling the transaction:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions/Transaction:019542f5-b3e7-1d02-0000-000000000030' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
You'll receive a webhook when the transaction completes:
```json theme={null}
{
"type": "OUTGOING_PAYMENT",
"transaction": {
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
"status": "COMPLETED",
"sentAmount": { "amount": 10000, "currency": { "code": "USD", "decimals": 2 } },
"receivedAmount": { "amount": 9200, "currency": { "code": "EUR", "decimals": 2 } },
"exchangeRate": 0.92,
"settledAt": "2025-10-03T15:30:00Z",
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000025"
},
"timestamp": "2025-10-03T15:31:00Z"
}
```
### Funding with cryptocurrencies
Cross-currency transfers support funding via USDC and BTC on popular blockchains including Solana, Base, Lightning and Spark. When you create a quote specifying the source currency as USDC or BTC, the response includes payment instructions for multiple funding options.
#### Supported blockchains
| Blockchain Network | Cryptocurrencies |
| ------------------ | ---------------- |
| Solana | USDC |
| Base | USDC |
| Tron | USDT |
| Polygon | USDC |
| Lightning | BTC |
| Spark | BTC |
#### Create a quote for USDC-funded transfer
Request a quote that provides blockchain funding options:
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"source": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "USDC"
},
"destination": { "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123", "currency": "EUR" },
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000,
"description": "Payment for services - Invoice #1234"
}'
```
The response includes an array of payment instructions, including blockchain wallet addresses for USDC and invoices for BTC:
```json Success (201 Created) theme={null}
{
"id": "Quote:019542f5-b3e7-1d02-0000-000000000025",
"status": "PENDING",
"source": { "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001", "currency": "USDC" },
"destination": { "accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123", "currency": "EUR" },
"sendingAmount": { "amount": 10000, "currency": { "code": "USDC", "decimals": 2 } },
"receivingAmount": { "amount": 9200, "currency": { "code": "EUR", "decimals": 2 } },
"exchangeRate": 0.92,
"fee": { "amount": 50, "currency": { "code": "USD", "decimals": 2 } },
"expiresAt": "2025-10-03T15:15:00Z",
"createdAt": "2025-10-03T15:00:00Z",
"paymentInstructions": [
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
},
{
"accountOrWalletInfo": {
"accountType": "BASE_WALLET",
"assetType": "USDC",
"address": "0x1234567890abcdef1234567890abcdef12345678"
}
}
]
}
```
#### Transaction processing
Grid automatically detects blockchain deposits and processes the transfer once funds are received:
Transfer the exact amount of USDC specified in `sendingAmount` to your chosen blockchain wallet address.
Grid monitors the blockchain for incoming deposits. You'll receive an `ACCOUNT_STATUS` webhook when the deposit is confirmed:
```json theme={null}
{
"type": "ACCOUNT_STATUS",
"accountId": "InternalAccount:019542f5-b3e7-1d02-0000-000000000025",
"oldBalance": { "amount": 0, "currency": { "code": "USDC", "decimals": 2 } },
"newBalance": { "amount": 10000, "currency": { "code": "USDC", "decimals": 2 } },
"timestamp": "2025-10-03T15:05:00Z"
}
```
Once the deposit is confirmed, Grid executes the cross-currency transfer. You'll receive `OUTGOING_PAYMENT` webhooks as the transfer progresses and completes:
```json theme={null}
{
"type": "OUTGOING_PAYMENT",
"transaction": {
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
"status": "COMPLETED",
"sentAmount": { "amount": 10000, "currency": { "code": "USD", "decimals": 2 } },
"receivedAmount": { "amount": 9200, "currency": { "code": "EUR", "decimals": 2 } },
"exchangeRate": 0.92,
"settledAt": "2025-10-03T15:30:00Z",
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000025"
},
"timestamp": "2025-10-03T15:31:00Z"
}
```
## Sending to an UMA Address
Send to an UMA address when the receiver is identified by their UMA handle eg \$[alice@example.com](mailto:alice@example.com). You'll look up the receiver, create a quote, and then fund.
### Look up the recipient
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/receiver/\$recipient@example.com?platformUserId=9f84e0c2a72c4fa" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
#### Response
```json Success (200 OK) theme={null}
{
"id": "Lookup:019542f5-b3e7-1d02-0000-000000000009",
"receiver": {
"handle": "$recipient@example.com",
"vaspName": "ExampleBank",
"network": "UMA"
},
"supportedCurrencies": [
{ "code": "EUR", "decimals": 2 },
{ "code": "BRL", "decimals": 2 }
],
"requiredPayerFields": [
"FULL_NAME",
"BIRTH_DATE"
],
"constraints": {
"minAmount": { "amount": 100, "currency": { "code": "USD", "decimals": 2 } },
"maxAmount": { "amount": 10000000, "currency": { "code": "USD", "decimals": 2 } }
},
"createdAt": "2025-10-03T15:00:00Z"
}
```
The response includes supported currencies and any required payer information fields.
If the receiver's VASP requires payer data, include it in `senderUserInfo` (applies to either tab).
### Create a quote
```bash Just-in-time funding theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"lookupId": "Lookup:019542f5-b3e7-1d02-0000-000000000009",
"sendingCurrencyCode": "USD",
"receivingCurrencyCode": "EUR",
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000,
"description": "Invoice #1234 payment",
"senderUserInfo": { "FULL_NAME": "John Sender", "BIRTH_DATE": "1985-06-15" }
}'
```
```bash Prefunded (use internal account as source) theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"lookupId": "Lookup:019542f5-b3e7-1d02-0000-000000000009",
"source": { "accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965" },
"sendingCurrencyCode": "USD",
"receivingCurrencyCode": "EUR",
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000,
"description": "UMA payment from prefunded balance"
}'
```
### Execute payment (just-in-time)
Use the `paymentInstructions` from the quote to instruct your bank to push funds. Include the exact `reference` provided.
### Execute payment (prefunded)
Existing internal account balances will be used to fund the payment. Use the lookup Id above to confirm the payment and execute the quote.
#### Execute the quote
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes/Quote:019542f5-b3e7-1d02-0000-000000000025/execute" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Executing the quote creates a transaction that draws from your internal account and delivers to the recipient associated with the UMA address.
#### Track status
Listen for `OUTGOING_PAYMENT` webhooks until the transaction reaches `COMPLETED` or `FAILED`.
You can also query for the transaction with the following snippet:
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/transactions/Transaction:019542f5-b3e7-1d02-0000-000000000030" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
# Core Concepts
Source: https://ramps-feat-building-with-ai.mintlify.app/global-p2p/terminology
Core concepts and terminology for the Grid API
There are several key entities in the Grid API: **Platform**, **Customers**, **Internal Accounts**, **External Accounts**, **Quotes**, **Transactions**, and **UMA Addresses**.
## Businesses, People, and Accounts
### Platform
Your **platform** is you! It's the top-level entity that integrates with the Grid API. The platform:
* Has its own configuration (webhook endpoint, supported currencies, API tokens, etc.)
* A platform can have many customers both business and individual
* Manages multiple customers and their accounts
* Can hold platform-owned internal accounts for settlement and liquidity management
* Acts as the integration point between your application and the open Money Grid
### Customers
**Customers** are your end users who send and receive payments through your platform. Each customer:
* Can be an individual or business entity
* Has a KYC/KYB status that determines their ability to transact. If you are a regulated financial institution, this will typically be `APPROVED` since you do the KYC/KYB yourself.
* Is identified by both a system-generated ID and optionally your platform-specific customer ID
* May have associated internal accounts and external accounts
* May have a unique **UMA address** (e.g., `$john.doe@yourdomain.com`). If you don't assign an UMA address when creating a customer, they will be assigned a system-generated one.
### Internal Accounts
**Internal accounts** are Grid-managed accounts that hold balances in specific currencies. They can belong to either:
* **Platform internal accounts** - Owned by the platform for settlement, liquidity, and float management
* **Customer internal accounts** - Associated with specific customers for holding funds
Internal accounts:
* Have balances in a single currency (USD, EUR, MXN, etc.)
* Can be funded via bank transfers or crypto deposits using payment instructions
* Are used as sources or destinations for transactions instantly 24/7/365
* Track available balance for sending payments or receiving funds
### External Accounts
**External accounts** are traditional bank accounts, crypto wallets, or other payment instruments connected to customers
for on-ramping or off-ramping funds. Each external account:
* Are associated with a specific customer or the platform
* Represents a real-world bank account (with routing number, account number, IBAN, etc.), wallet, or payment instrument
* Has an associated beneficiary (individual or business) who receives payments from the customer or platform
* Has a status indicating screening status (ACTIVE, PENDING, INACTIVE, etc.)
* Can be used as a destination for quote-based transfers or same currency transfers like withdrawals
* For pullable sources like debit cards or ACH pulls, an external account can be used as a source for transfers-in to
fund internal accounts or to fund cross-border transfers via quotes.
## Entity Examples by Use Case
Understanding how entities map to your specific use case helps clarify your integration architecture. Here are common examples:
### B2B Payouts Platform (e.g., Bill.com, Routable)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------- | ------------------------------------------ |
| **Platform** | The payouts platform itself | Your company providing AP automation |
| **Customer** | Businesses sending payments to vendors | Acme Corp (your client company) |
| **External Account** | Vendors/suppliers receiving payments | Office supply vendor, freelance contractor |
**Flow**: Acme Corp (customer) uses your platform to pay their vendor invoices → funds move from Acme's internal account → to vendor's external bank account
### Direct Rewards Platform (Platform-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | ------------------------------------------- | ----------------------------------- |
| **Platform** | The app paying rewards directly to users | Your cashback app |
| **Customer** | (Not used in this model) | N/A |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Your platform sends micro-payouts directly from platform internal accounts → to users' external crypto wallets at scale. Common for cashback apps where the platform earns affiliate commissions and shares them with users.
### White-Label Rewards Platform (Customer-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------------- | ----------------------------------- |
| **Platform** | The rewards infrastructure provider | Your white-label rewards API |
| **Customer** | Brands or merchants running reward campaigns | Nike, Starbucks |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Nike (customer) funds their internal account → your platform sends rewards on their behalf → to users' external crypto wallets. Common for brand loyalty programs where merchants manage their own reward budgets.
### Remittance/P2P App (e.g., Wise, Remitly)
| Entity Type | Who They Are | Example |
| -------------------- | ----------------------------------- | ---------------------------------------------------------------- |
| **Platform** | The remittance service | Your money transfer app |
| **Customer** | Both sender and recipient of funds | Maria (sender in US), Juan (recipient in Mexico) |
| **External Account** | Bank accounts for funding/receiving | Maria's US bank (funding), Juan's Mexican bank (receiving funds) |
**Flow**: Maria (customer) funds transfer from her external account → to Juan (also a customer) → who receives funds in his external bank account. Alternatively, Maria could send to Juan's UMA address directly.
## Transactions and Addressing Entities
### Quotes
**Quotes** provide locked-in exchange rates and payment instructions for transfers. A quote:
* Specifies a source (internal account, customer ID, or the platform itself) and destination (internal/external account or UMA address)
* Locks an exchange rate for a short period (typically 1-5 minutes) or can be immediately executed with the `immediatelyExecute` flag
* Calculates total fees and amounts for currency conversion
* Provides payment instructions for funding the transfer if needed, or can be funded via an internal account balance.
* Must be executed before it expires
* Creates a transaction when executed
### Transactions
**Transactions** represent completed or in-progress payment transfers. Each transaction:
* Has a type (INCOMING or OUTGOING from the platform's perspective)
* Has a status (PENDING, COMPLETED, FAILED, etc.)
* References a customer (sender for outgoing, recipient for incoming) or a platform internal account
* Specifies source and destination (accounts or UMA addresses)
* Includes amounts, currencies, and settlement information
* May include counterparty information for compliance purposes if required by your platform configuration
Transactions are created when:
* A quote is executed (either incoming or outgoing)
* A same currency transfer is initiated (transfer-in or transfer-out)
### UMA Addresses (optional)
**UMA addresses** are human-readable payment identifiers that follow the format `$username@domain.com`. They:
* Uniquely identify entities on the Grid network
* Enable sending and receiving payments across different platforms without knowing the recipient's underlying account details or personal information
* Support currency negotiation and cross-border transfers
* Work similar to email addresses but for payments
* Are an optional UX improvement for some use cases. Use of UMA addresses is not required in order to use the Grid API.
# null
Source: https://ramps-feat-building-with-ai.mintlify.app/index
Documentation
Build applications and financial products that move money globally across fiat, stablecoins, and Bitcoin with a single API.
# Depositing Funds
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/depositing-funds/depositing-funds
Depositing funds into internal accounts
Grid provides two options to fund an account:
* Prefund
* Just-in-time funding
With prefunding, you'll deposit funds into internal accounts via Wire, PIX, or crypto transfers. You can then use the balances as the source of funds for quotes and transfers.
With just-in-time funding, you'll receive payment instructions as part of the quote. Once funds arrive, the payment to the receiver is automatically initiated.
Just-in-time funding supports instant payment rails only (for example: RTP,
PIX, SEPA Instant).
## Prerequisites
* You have created a customer (for customer-scoped internal accounts)
* You have `GRID_CLIENT_ID` and `GRID_CLIENT_SECRET`
Export your credentials for use with cURL:
```bash theme={null}
export GRID_CLIENT_ID="your_client_id"
export GRID_CLIENT_SECRET="your_client_secret"
```
## Prefunding an account via push payments (Wire, SEPA, PIX, etc.)
When customers need to deposit funds themselves, display the funding payment instructions in your application:
Fetch the customer's internal account for their desired currency using the API.
```bash cURL (Customer accounts) theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```bash cURL (Platform internal accounts) theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Parse the `fundingPaymentInstructions` array and select the appropriate instructions based on your customer's preferred payment method.
```javascript theme={null}
const instructions = account.fundingPaymentInstructions[0];
const bankInfo = instructions.accountOrWalletInfo;
```
Show the payment details prominently in your UI and enable copy / paste:
* Account holder name
* Bank name and routing information (account/routing number, CLABE, PIX key, etc.)
* Reference code (if provided)
* Any additional notes from `instructionsNotes`
Customer initiates a push payment from their bank or wallet to the account/address specified.
Set up webhook listeners to receive updates for the deposit transaction and account balance updates. The account balance will update automatically.
You'll receive `ACCOUNT_STATUS` webhook events when the internal account balance changes.
## Just-in-time funding (payment instructions from a quote)
With just-in-time funding, you request a quote and receive payment instructions (for example, a bank account or instant rail details). When your customer confirms the transaction, you trigger payment from your app.
More details of just-in-time funding can be found in the Sending Payments guides.
# External Accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/depositing-funds/external-accounts
Add and manage external bank accounts, wallets, and payment destinations for withdrawals and payouts
External accounts are bank accounts, cryptocurrency wallets, or payment destinations outside Grid where you can send funds. Grid supports two types:
* **Customer external accounts** - Scoped to individual customers, used for withdrawals and customer-specific payouts
* **Platform external accounts** - Scoped to your platform, used for platform-wide operations like receiving funds from external sources
Customer external accounts often require some basic beneficiary information for compliance.
Platform accounts are managed at the organization level.
## Create external accounts by region or wallet
**ACH, Wire, RTP**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}'
```
Category must be `CHECKING` or `SAVINGS`. Routing number must be 9 digits.
**CLABE/SPEI**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "MXN",
"platformAccountId": "mx_beneficiary_001",
"accountInfo": {
"accountType": "CLABE",
"clabeNumber": "123456789012345678",
"bankName": "BBVA Mexico",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "María García",
"birthDate": "1985-03-15",
"nationality": "MX",
"address": {
"line1": "Av. Reforma 123",
"city": "Ciudad de México",
"state": "CDMX",
"postalCode": "06600",
"country": "MX"
}
}
}
}'
```
**PIX**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "BRL",
"platformAccountId": "br_pix_001",
"accountInfo": {
"accountType": "PIX",
"pixKey": "user@email.com",
"pixKeyType": "EMAIL",
"bankName": "Nubank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "João Silva",
"birthDate": "1988-07-22",
"nationality": "BR",
"address": {
"line1": "Rua das Flores 456",
"city": "São Paulo",
"state": "SP",
"postalCode": "01234-567",
"country": "BR"
}
}
}
}'
```
Key types: `CPF`, `CNPJ`, `EMAIL`, `PHONE`, or `RANDOM`
**IBAN/SEPA**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "EUR",
"platformAccountId": "eu_iban_001",
"accountInfo": {
"accountType": "IBAN",
"iban": "DE89370400440532013000",
"swiftBic": "DEUTDEFF",
"bankName": "Deutsche Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Hans Schmidt",
"birthDate": "1982-11-08",
"nationality": "DE",
"address": {
"line1": "Hauptstraße 789",
"city": "Berlin",
"state": "Berlin",
"postalCode": "10115",
"country": "DE"
}
}
}
}'
```
**UPI**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "INR",
"platformAccountId": "in_upi_001",
"accountInfo": {
"accountType": "UPI",
"vpa": "user@okbank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Priya Sharma",
"birthDate": "1991-05-14",
"nationality": "IN",
"address": {
"line1": "123 MG Road",
"city": "Mumbai",
"state": "Maharashtra",
"postalCode": "400001",
"country": "IN"
}
}
}
}'
```
**Bitcoin Lightning (Spark Wallet)**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "BTC",
"platformAccountId": "btc_spark_001",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}'
```
Spark wallets don't require beneficiary information as they are self-custody wallets.
Use `platformAccountId` to tie your internal id with the external account.
**Sample Response:**
```json theme={null}
{
"id": "ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"status": "ACTIVE",
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}
```
### Business beneficiaries
For business accounts, include business information:
```json theme={null}
{
"currency": "USD",
"platformAccountId": "acme_corp_account",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "987654321",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "BUSINESS",
"businessInfo": {
"legalName": "Acme Corporation, Inc.",
"taxId": "EIN-987654321"
},
"address": {
"line1": "456 Business Ave",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}
}
}
```
## Account status
Beneficiary data may be reviewed for risk and compliance. Only `ACTIVE` accounts can receive payments. Updates to account data may trigger account re-review.
| Status | Description |
| -------------- | ----------------------------------- |
| `PENDING` | Created, awaiting verification |
| `ACTIVE` | Verified and ready for transactions |
| `UNDER_REVIEW` | Additional review required |
| `INACTIVE` | Disabled, cannot be used |
## Listing external accounts
### List customer accounts
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
### List platform accounts
For platform-wide operations, list all platform-level external accounts:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Platform external accounts are used for platform-wide operations like
depositing funds from external sources.
## Best practices
Validate account details before submission:
```javascript theme={null}
// US accounts: 9-digit routing, 4-17 digit account number
if (!/^\d{9}$/.test(routingNumber)) {
throw new Error("Invalid routing number");
}
// CLABE: exactly 18 digits
if (!/^\d{18}$/.test(clabeNumber)) {
throw new Error("Invalid CLABE number");
}
```
Verify status before sending payments:
```javascript theme={null}
if (account.status !== "ACTIVE") {
throw new Error(`Account is ${account.status}, cannot process payment`);
}
```
Never expose full account numbers. Display only masked info:
```javascript theme={null}
function displaySafely(account) {
return {
id: account.id,
bankName: account.accountInfo.bankName,
lastFour: account.accountInfo.accountNumber.slice(-4),
status: account.status,
};
}
```
## Next steps
Simplify external account setup with Plaid Link for instant bank verification
Learn how to send international payments using external accounts
View complete API documentation for external accounts
# Internal Accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/depositing-funds/internal-accounts
Learn how to manage internal accounts for holding platform and customer funds
Internal accounts are Lightspark managed accounts that hold funds within the Grid platform. They allow you to receive deposits and send payments to external bank accounts or other payment destinations.
They are useful for holding funds on behalf or the platform or customers which will be used for instant, 24/7 quotes and transfers out of the system.
Internal accounts are created for both:
* **Platform-level accounts**: Hold pooled funds for your platform operations (rewards distribution, reconciliation, etc.)
* **Customer accounts**: Hold individual customer funds for their transactions
Internal accounts are automatically created when you onboard a customer, based
on your platform's currency configuration. Platform-level internal accounts
are created when you configure your platform with supported currencies.
## How internal accounts work
Internal accounts act as an intermediary holding account in the payment flow:
1. **Deposit funds**: You or your customers deposit money into internal accounts using bank transfers (ACH, wire, PIX, etc.) or crypto transfers
2. **Hold balance**: Funds are held securely in the internal account until needed
3. **Send payments**: You initiate transfers from internal accounts to external destinations
Each internal account:
* Is denominated in a single currency (USD, EUR, etc.)
* Has a unique balance that you can query at any time
* Includes unique payment instructions for depositing funds
* Supports multiple funding methods depending on the currency
## Retrieving internal accounts
### List customer internal accounts
To retrieve all internal accounts for a specific customer, use the customer ID to filter the results:
```bash Request internal accounts for a customer theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json theme={null}
{
"data": [
{
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"fundingPaymentInstructions": [
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"reference": "FUND-ABC123",
"accountType": "US_ACCOUNT",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
},
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
],
"createdAt": "2025-10-03T12:00:00Z",
"updatedAt": "2025-10-03T14:30:00Z"
}
],
"hasMore": false,
"totalCount": 1
}
```
### Filter by currency
You can filter internal accounts by currency to find accounts for specific denominations:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001¤cy=USD' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
### List platform internal accounts
To retrieve platform-level internal accounts (not tied to individual customers), use the platform internal accounts endpoint:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Platform internal accounts are useful for managing pooled funds, distributing
rewards, or handling platform-level operations.
## Understanding funding payment instructions
Each internal account includes `fundingPaymentInstructions` that tell your customers how to deposit funds. The structure varies by payment rail and currency:
For USD accounts, instructions include routing and account numbers:
```json theme={null}
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"accountType": "US_ACCOUNT",
"reference": "FUND-ABC123",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
}
```
Each internal account has unique banking details in the `accountOrWalletInfo`
field, which ensures deposits are automatically credited to the correct
account.
For EUR accounts, instructions use SEPA IBAN numbers:
```json theme={null}
{
"instructionsNotes": "Include reference in SEPA transfer description",
"accountOrWalletInfo": {
"accountType": "IBAN",
"reference": "FUND-EUR789",
"iban": "DE89370400440532013000",
"swiftBic": "DEUTDEFF",
"accountHolderName": "Lightspark Payments FBO Maria Garcia",
"bankName": "Banco de México"
}
}
```
For stablecoin accounts, using a Spark wallet as the funding source:
```json theme={null}
{
"invoice": "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs",
"instructionsNotes": "Use the invoice when making Spark payment",
"accountOrWalletInfo": {
"accountType": "SPARK_WALLET",
"assetType": "USDB",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
```
For Solana wallet accounts, using a Solana wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
```
For Tron wallet accounts, using a Tron wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "TRON_WALLET",
"assetType": "USDT",
"address": "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL"
}
}
```
For Polygon wallet accounts, using a Polygon wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "POLYGON_WALLET",
"assetType": "USDC",
"address": "0xAbCDEF1234567890aBCdEf1234567890ABcDef12"
}
}
```
For Base wallet accounts, using a Base wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "BASE_WALLET",
"assetType": "USDC",
"address": "0xAbCDEF1234567890aBCdEf1234567890ABcDef12"
}
}
```
## Checking account balances
The internal account balance reflects all deposits and withdrawals. The balance includes:
* **amount**: The balance amount in the smallest currency unit (cents for USD, centavos for MXN/BRL, etc.)
* **currency**: Full currency details including code, name, symbol, and decimal places
### Example balance check
```bash Fetch the balance of an internal account theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts/InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json theme={null}
{
"data": {
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
}
}
}
```
Always check the `decimals` field in the currency object to correctly convert
between display amounts and API amounts. For example, USD has 2 decimals, so
an amount of 50000 represents \$500.00.
## Displaying funding instructions to customers
When customers need to deposit funds themselves, display the funding payment instructions in your application:
Fetch the customer's internal account for their desired currency using the API.
Parse the `fundingPaymentInstructions` array and select the appropriate instructions based on your customer's preferred payment method.
```javascript theme={null}
const instructions = account.fundingPaymentInstructions[0];
const bankInfo = instructions.accountOrWalletInfo;
```
Show the payment details prominently in your UI:
* Account holder name
* Bank name and routing information (account/routing number, CLABE, PIX key, etc.)
* Reference code (if provided)
* Any additional notes from `instructionsNotes`
The unique banking details in each internal account automatically route
deposits to the correct destination.
Set up webhook listeners to receive notifications when deposits are credited to the internal account. The account balance will update automatically.
You'll receive `ACCOUNT_STATUS` webhook events when the internal account balance changes.
## Best practices
Ensure your customers have all the information needed to make deposits. Consider implementing:
* Clear display of all banking details from `fundingPaymentInstructions`
* Copy-to-clipboard functionality for account numbers and reference codes
* Email/SMS confirmations with complete deposit instructions
Set up monitoring to alert customers when their balance is low:
```javascript theme={null}
if (account.balance.amount < minimumThreshold) {
await notifyCustomer({
type: 'LOW_BALANCE',
account: account.id,
instructions: account.fundingPaymentInstructions
});
}
```
If your platform supports multiple currencies, organize internal accounts by currency in your UI:
```javascript theme={null}
const accountsByCurrency = accounts.data.reduce((acc, account) => {
const code = account.balance.currency.code;
acc[code] = account;
return acc;
}, {});
// Quick lookup: accountsByCurrency['USD']
```
Internal account details (especially funding instructions) rarely change, so you can cache them safely. However, always fetch fresh balance data before initiating transfers.
## Next steps
Learn how to add customer bank accounts as withdrawal destinations
Simplify bank account verification with Plaid Link
Use internal account balances to send international payments
View complete API documentation for internal accounts
# External Accounts with Plaid
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/depositing-funds/plaid
Simplify bank account verification with Plaid Link for external account setup
Plaid integration allows your customers to securely connect their bank accounts without manually entering account numbers and routing information. Grid handles the complete Plaid Link flow, automatically creating external accounts when customers authenticate their banks.
Plaid integration requires Grid to manage your Plaid configuration. Contact
support to enable Plaid for your platform.
## Overview
The Plaid flow involves collaboration between your platform, Grid, Plaid, and the customer's bank:
1. **Request link token**: Your platform requests a Plaid Link token from Grid for a specific customer
2. **Initialize Plaid Link**: Display Plaid Link UI to your customer using the link token
3. **Customer authenticates**: Customer selects their bank and authenticates using Plaid Link
4. **Exchange tokens**: Plaid returns a public token; your platform sends it to Grid's callback URL
5. **Async processing**: Grid exchanges the public token with Plaid and retrieves account details
6. **External account created**: Grid creates the external account and sends a webhook notification. The external account is available for transfers and payments
## Request a Plaid Link token
To initiate the Plaid flow, request a link token from Grid:
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/plaid/link-tokens' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001"
}'
```
**Response:**
```json theme={null}
{
"linkToken": "link-sandbox-af1a0311-da53-4636-b754-dd15cc058176",
"expiration": "2025-10-05T18:30:00Z",
"callbackUrl": "https://api.lightspark.com/grid/2025-10-13/plaid/callback/link-sandbox-af1a0311-da53-4636-b754-dd15cc058176",
"requestId": "req_abc123def456"
}
```
Store the `callbackUrl` when you request the link token so you can retrieve it later when exchanging the public token.
### Key response fields:
* **`linkToken`**: Use this to initialize Plaid Link in your frontend
* **`callbackUrl`**: Where to POST the public token after Plaid authentication completes. The URL follows the pattern `https://api.lightspark.com/grid/{version}/plaid/callback/{linkToken}`. While you can construct this manually, we recommend using the provided URL for forward compatibility.
* **`expiration`**: Link tokens typically expire after 4 hours
* **`requestId`**: Unique identifier for debugging purposes
Link tokens are single-use and will expire. If the customer doesn't complete
the flow, you'll need to request a new link token.
## Initialize Plaid Link
Display the Plaid Link UI to your customer using the link token. The implementation varies by platform:
Install the appropriate Plaid SDK for your platform:
* React: `npm install react-plaid-link`
* React Native: `npm install react-native-plaid-link-sdk`
* Vanilla JS: Include the Plaid script tag as shown above
```javascript theme={null}
import { usePlaidLink } from 'react-plaid-link';
function BankAccountConnector({ linkToken, onSuccess }) {
const { open, ready } = usePlaidLink({
token: linkToken,
onSuccess: async (publicToken, metadata) => {
console.log('Plaid authentication successful');
// Send public token to YOUR backend endpoint
await fetch('/api/plaid/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: metadata.account_id, // Optional
}),
});
onSuccess();
},
onExit: (error, metadata) => {
if (error) {
console.error('Plaid Link error:', error);
}
console.log('User exited Plaid Link');
},
});
return (
); }
```
```javascript theme={null}
import { PlaidLink } from 'react-native-plaid-link-sdk';
function BankAccountConnector({ linkToken, onSuccess }) {
return (
{
console.log('Plaid authentication successful');
// Send public token to YOUR backend endpoint
await fetch('https://yourapi.com/api/plaid/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: metadata.account_id,
}),
});
onSuccess();
}}
onExit={(error, metadata) => {
if (error) {
console.error('Plaid Link error:', error);
}
}}
>
Connect your bank account
);
}
```
```html theme={null}
```
## Exchange the public token on your backend
Create a backend endpoint that receives the public token from your frontend and forwards it to Grid's callback URL:
```javascript Express theme={null}
// Backend endpoint: POST /api/plaid/exchange-token
app.post('/api/plaid/exchange-token', async (req, res) => {
const { publicToken, accountId } = req.body;
const customerId = req.user.gridCustomerId; // From your auth
try {
// Get the callback URL (you stored this when requesting the link token)
const callbackUrl = await getStoredCallbackUrl(customerId);
// Forward to Grid's callback URL with proper authentication
const response = await fetch(callbackUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: accountId,
}),
});
if (!response.ok) {
throw new Error(`Grid API error: ${response.status}`);
}
const result = await response.json();
res.json({ success: true, message: result.message });
} catch (error) {
console.error('Error exchanging token:', error);
res.status(500).json({ error: 'Failed to process bank account' });
}
});
```
**Response from Grid (HTTP 202 Accepted):**
```json theme={null}
{
"message": "External account creation initiated. You will receive a webhook notification when complete.",
"requestId": "req_def456ghi789"
}
```
A `202 Accepted` response indicates Grid has received the token and is
processing it asynchronously. The external account will be created in the
background.
## Handle webhook notification
After Grid creates the external account, you'll receive an `ACCOUNT_STATUS` webhook.
```json theme={null}
{
"type": "ACCOUNT_STATUS",
"timestamp": "2025-01-15T14:32:10Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-0000000000ac",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"account": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"status": "ACTIVE",
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}
}
```
## Error handling
Handle common error scenarios:
### User exits Plaid Link
```javascript theme={null}
const { open } = usePlaidLink({
token: linkToken,
onExit: (error, metadata) => {
if (error) {
console.error("Plaid error:", error);
// Show user-friendly error message
setError("Unable to connect to your bank. Please try again.");
} else {
// User closed the modal without completing
console.log("User exited without connecting");
}
},
});
```
## Next steps
Learn more about managing external accounts
Transfer funds between internal and external accounts
Set up webhook handling for account notifications
View complete Plaid API documentation
# Payouts & B2B
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/index
With , you can send and receive low cost real-time payments to bank accounts worldwide through a single, simple API. automatically routes each payment across its network of switches, handling FX, blockchain settlement, and instant banking off-ramps for you.
Single API, global reach. interacts with the Money Grid to route your payments globally.
No crypto handling. converts between fiat and crypto instantly to simplify your implementation and minimize FX costs.
Real-time settlement. Leverages local instant banking rails and global low latency crypto rails to settle payments in real-time.
## Payment Flow
You can either prefund an internal account with fiat or receive just-in-time payment instructions as part of the quote.
Create a quote to lock exchange rate to the receiving foreign account and parse payment instructions.
Execute the quote or send funds as per payment instructions to initiate the transfer from the internal account to the external bank account.
## Features
Customers interact with through two main interfaces.
Programmatic access to create customers, quotes, fund the account, send payments and reconcile via webhooks.
Your development and operations team can use the dashboard to monitor payments and webhooks, manage API keys and environments, and troubleshoot with logs.
Implementing cross-border payments with is simple. Here's a quick overview of the main steps.
### Onboarding customers
has two customer onboarding options - one for non regulated entities where handles the KYC/KYB process and one for regulated entities where you handle the KYC/KYB process.
When creating customers, you'll be able to also connect external accounts or internal accounts to a specific customer and also add accounts for counterparties on the receiving end.
To learn more about accounts read our [internal accounts guide](/payouts-and-b2b/depositing-funds/internal-accounts) or setting up [external accounts guide](/payouts-and-b2b/depositing-funds/external-accounts)
### Funding Payments
supports multiple transaction funding options including prefunded accounts and real-time funding. You can prefund an account using several payment rails such as ACH, SEPA Instant, wire transfers, Lightning, and more.
With real-time funding, you'll receive payment instructions as part of the quote. Once payment is received by our services, we'll initiate the payment to the receiver.
### Sending Payments
To send with , onboard an account for a customer and the counter party, then execute and fund a quote. resolves the receiver by external bank details, returns min/max and an exchange rate, and provides funding instructions. Once funded, handles FX and delivery to the receiving account.
### Environments
supports two environments: Sandbox and Production. Sandbox mirrors production behavior and webhooks so you can test receiver resolution, quotes and funding instructions, settlement status changes, and full end‑to‑end flows without moving real funds. Production uses live credentials and base URLs for real payments once you’re ready to launch.
***
### Country Availability
Bitcoin (BTC) and Stablecoin transactions supported worldwide with no geographic restrictions.
Onboard as a platform with complete access to APIs, hosted KYC/KYB, dashboard, and business integrations.
Send payments to 65 countries on local banking rails, in addition to BTC and stablecoins.
Receive payments via local rails like SEPA, PIX, UPI, and more.
| Country | ISO Code | Payment Rails |
| ------------------- | -------- | -----------------------------------: |
| 🇦🇹 Austria | AT | `SEPA` `SEPA Instant` |
| 🇧🇪 Belgium | BE | `SEPA` `SEPA Instant` |
| 🇧🇯 Benin | BJ | `Bank Transfer` |
| 🇧🇼 Botswana | BW | `Bank Transfer` |
| 🇧🇷 Brazil | BR | `PIX` |
| 🇧🇬 Bulgaria | BG | `SEPA` `SEPA Instant` |
| 🇧🇫 Burkina Faso | BF | `Bank Transfer` |
| 🇨🇲 Cameroon | CM | `Bank Transfer` |
| 🇨🇦 Canada | CA | `Bank Transfer` |
| 🇨🇳 China | CN | `Bank Transfer` |
| 🇨🇷 Costa Rica | CR | `Bank Transfer` |
| 🇭🇷 Croatia | HR | `SEPA` `SEPA Instant` |
| 🇨🇾 Cyprus | CY | `SEPA` `SEPA Instant` |
| 🇨🇿 Czech Republic | CZ | `SEPA` `SEPA Instant` |
| 🇩🇰 Denmark | DK | `SEPA` `SEPA Instant` |
| 🇨🇩 DR Congo | CD | `Bank Transfer` |
| 🇪🇪 Estonia | EE | `SEPA` `SEPA Instant` |
| 🇫🇮 Finland | FI | `SEPA` `SEPA Instant` |
| 🇫🇷 France | FR | `SEPA` `SEPA Instant` |
| 🇩🇪 Germany | DE | `SEPA` `SEPA Instant` |
| 🇬🇭 Ghana | GH | `Bank Transfer` |
| 🇬🇷 Greece | GR | `SEPA` `SEPA Instant` |
| 🇭🇰 Hong Kong | HK | `Bank Transfer` |
| 🇭🇺 Hungary | HU | `SEPA` `SEPA Instant` |
| 🇮🇸 Iceland | IS | `SEPA` `SEPA Instant` |
| 🇮🇳 India | IN | `UPI` `IMPS` |
| 🇮🇩 Indonesia | ID | `Bank Transfer` |
| 🇮🇪 Ireland | IE | `SEPA` `SEPA Instant` |
| 🇮🇹 Italy | IT | `SEPA` `SEPA Instant` |
| 🇨🇮 Ivory Coast | CI | `Bank Transfer` |
| 🇰🇪 Kenya | KE | `Bank Transfer` |
| 🇱🇻 Latvia | LV | `SEPA` `SEPA Instant` |
| 🇱🇮 Liechtenstein | LI | `SEPA` `SEPA Instant` |
| 🇱🇹 Lithuania | LT | `SEPA` `SEPA Instant` |
| 🇱🇺 Luxembourg | LU | `SEPA` `SEPA Instant` |
| 🇲🇼 Malawi | MW | `Bank Transfer` |
| 🇲🇾 Malaysia | MY | `Bank Transfer` |
| 🇲🇱 Mali | ML | `Bank Transfer` |
| 🇲🇹 Malta | MT | `SEPA` `SEPA Instant` |
| 🇲🇽 Mexico | MX | `SPEI` |
| 🇳🇱 Netherlands | NL | `SEPA` `SEPA Instant` |
| 🇳🇬 Nigeria | NG | `Bank Transfer` |
| 🇳🇴 Norway | NO | `SEPA` `SEPA Instant` |
| 🇵🇭 Philippines | PH | `Bank Transfer` |
| 🇵🇱 Poland | PL | `SEPA` `SEPA Instant` |
| 🇵🇹 Portugal | PT | `SEPA` `SEPA Instant` |
| 🇷🇴 Romania | RO | `SEPA` `SEPA Instant` |
| 🇸🇳 Senegal | SN | `Bank Transfer` |
| 🇸🇬 Singapore | SG | `PayNow` `FAST` `Bank Transfer` |
| 🇸🇰 Slovakia | SK | `SEPA` `SEPA Instant` |
| 🇸🇮 Slovenia | SI | `SEPA` `SEPA Instant` |
| 🇿🇦 South Africa | ZA | `Bank Transfer` |
| 🇰🇷 South Korea | KR | `Bank Transfer` |
| 🇪🇸 Spain | ES | `SEPA` `SEPA Instant` |
| 🇱🇰 Sri Lanka | LK | `Bank Transfer` |
| 🇸🇪 Sweden | SE | `SEPA` `SEPA Instant` |
| 🇨🇭 Switzerland | CH | `SEPA` `SEPA Instant` |
| 🇹🇿 Tanzania | TZ | `Bank Transfer` |
| 🇹🇭 Thailand | TH | `Bank Transfer` |
| 🇹🇬 Togo | TG | `Bank Transfer` |
| 🇺🇬 Uganda | UG | `Bank Transfer` |
| 🇬🇧 United Kingdom | GB | `Faster Payments` `Bank Transfer` |
| 🇺🇸 United States | US | `ACH` `Wire Transfer` `RTP` `FedNow` |
| 🇻🇳 Vietnam | VN | `Bank Transfer` |
| 🇿🇲 Zambia | ZM | `Bank Transfer` |
Regional Summary
Primary: SEPA/SEPA Instant
Primary: Bank Transfer
Various instant payment systems
PIX, SPEI, ACH, FedNow
# Configuring Customers
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/onboarding/configuring-customers
Configuring customers for Payouts
This guide provides comprehensive information about customer configuration in the Grid API, including customer types, registration processes, management, and bank account information.
## Customer Types
The Grid API supports both individual and business customers. While the API schema itself makes most Personally Identifiable Information (PII) optional at the initial customer creation, specific fields may become mandatory based on the currencies the customer will transact with.
Your platform's configuration (retrieved via `GET /config`) includes a `supportedCurrencies` array. If a customer is intended to use a specific currency, any fields listed in for that currency **must** be provided when creating or updating the customer.
## Customer Registration Process
### Creating a New Customer
When creating or updating customers, the `customerType` field must be specified as either `INDIVIDUAL` or `BUSINESS`. Depending if you are a regulated or unregulated platform your KYC/KYB requirements will vary.
**Regulated platforms** have lighter KYC requirements since they handle compliance verification internally.
The KYC/KYB flow allows you to onboard customers through direct API calls.
Regulated financial institutions can:
* **Direct API Onboarding**: Create customers directly via API calls with minimal verification
* **Internal KYC/KYB**: Handle identity verification through your own compliance systems
* **Reduced Documentation**: Only provide essential customer information required by your payment counterparty or service provider.
* **Faster Onboarding**: Streamlined process for known, verified customers
#### Creating Customers via Direct API
For regulated platforms, you can create customers directly through the API without requiring external KYC verification:
To register a new customer in the system, use the `POST /customers` endpoint:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/customers" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"platformCustomerId": "customer_12345",
"customerType": "INDIVIDUAL",
"fullName": "Jane Doe",
"birthDate": "1992-03-25",
"nationality": "US",
"address": {
"line1": "123 Pine Street",
"city": "Seattle",
"state": "WA",
"postalCode": "98101",
"country": "US"
}
}'
```
The examples below show a more comprehensive set of data. Not all fields are strictly required by the API for customer creation itself, but become necessary based on currency and UMA provider requirements if using UMA.
```json theme={null}
{
"platformCustomerId": "9f84e0c2a72c4fa",
"customerType": "INDIVIDUAL",
"fullName": "John Sender",
"birthDate": "1985-06-15",
"address": {
"line1": "Paseo de la Reforma 222",
"line2": "Piso 15",
"city": "Ciudad de México",
"state": "Ciudad de México",
"postalCode": "06600",
"country": "MX"
}
}
```
```json theme={null}
{
"platformCustomerId": "b87d2e4a9c13f5b",
"customerType": "BUSINESS",
"businessInfo": {
"legalName": "Acme Corporation",
"registrationNumber": "789012345",
"taxId": "123-45-6789"
},
"address": {
"line1": "456 Oak Avenue",
"line2": "Floor 12",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}
```
**Unregulated platforms** require full KYC/KYB verification of customers through hosted flows.
Unregulated platforms must:
* **Hosted KYC Flow**: Use the hosted KYC link for complete identity verification
* **Extended Review**: Customers may require manual review and approval in some cases
### Hosted KYC Link Flow
The hosted KYC flow provides a secure, hosted interface where customers can complete their identity verification and onboarding process.
#### Generate KYC Link
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/customers/kyc-link?redirectUri=https://yourapp.com/onboarding-complete&platformCustomerId=019542f5-b3e7-1d02-0000-000000000001" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
**Response:**
```json theme={null}
{
"kycUrl": "https://kyc.lightspark.com/onboard/abc123def456",
"platformCustomerId": "019542f5-b3e7-1d02-0000-000000000001"
}
```
#### Complete KYC Process
Call the `/customers/kyc-link` endpoint with your `redirectUri` parameter to generate a hosted KYC URL for your customer.
The `redirectUri` parameter is embedded in the generated KYC URL and will be used to automatically redirect the customer back to your application after they complete verification.
Redirect your customer to the returned `kycUrl` where they can complete their identity verification in the hosted interface.
The KYC link is single-use and expires after a limited time period for security.
The customer completes the identity verification process in the hosted KYC interface, providing required documents and information.
The hosted interface handles document collection, verification checks, and compliance requirements automatically.
After verification processing, you'll receive a KYC status webhook notification indicating the final verification result.
Upon successful KYC completion, the customer is automatically redirected to your specified `redirectUri` URL.
The customer account will be automatically created by the system upon successful KYC completion. You can identify the new customer using your `platformCustomerId` or other identifiers.
On your redirect page, handle the completed KYC flow and integrate the new customer into your application.
## Customer Management
### Retrieving Customer Information
You can retrieve customer information using either the Grid-assigned customer ID or your platform's customer ID:
```bash Grid-assigned customer ID theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/customers/{customerId}" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```bash List customers with a filter theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/customers?platformCustomerId={platformCustomerId}&customerType={customerType}&createdAfter={createdAfter}&createdBefore={createdBefore}&cursor={cursor}&limit={limit}" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Note that this example shows all available filters. You can use any combination of them.
### Updating Customer Information
To update customer information:
```bash theme={null}
curl -X PATCH "https://api.lightspark.com/grid/2025-10-13/customers/{customerId}" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"bankAccountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "987654321",
"bankName": "Chase Bank"
}
}'
```
Note that not all customer information can be updated. Particularly for non-regulated platforms, you cannot update personal information after the customer has been created.
## Bank Account Information
The API supports various bank account formats based on country and funding type. There are two types of funding
mechanisms supported by Grid: an omnibus FBO (for benefit of) account owned by the platform, or direct customer-owned accounts. You must provide the correct format based on the customer's region and bank account type.
### Optional Platform Account ID
All bank account types support an optional `platformAccountId` field that allows you to link bank accounts to your internal systems. This field can be any string that helps identify the account in your platform (e.g., database IDs, custom references, etc.).
Example with platform account ID:
```json theme={null}
{
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "987654321",
"bankName": "Chase Bank",
"platformAccountId": "chase_primary_1234"
}
```
Common use cases for `platformAccountId`:
* Tracking multiple bank accounts and uma addresses for the same customer
* Linking accounts to internal accounting systems
* Maintaining consistency between the Grid API and your platform's account records
* Facilitating account reconciliation and reporting
FBO accounts are used when the platform has a single omnibus account that is used to fund all customers. Account details
must be provided manually at the platform level. For each customer, during you should simply provide:
```json theme={null}
"bankAccountInfo": {
"accountType": "FBO",
"currencyCode": "USD" // or any other currency code supported by the Grid API
}
```
Please contact us to set up FBO account for a specific currency.
```json theme={null}
{
"accountType": "CLABE",
"clabeNumber": "123456789012345678",
"bankName": "Banco de México",
"platformAccountId": "banco_mx_primary_5678"
}
```
```json theme={null}
{
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "987654321",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"platformAccountId": "chase_checking_1234"
}
```
```json theme={null}
{
"accountType": "PIX",
"pixKey": "12345678901",
"pixKeyType": "CPF",
"platformAccountId": "pix_main_9012"
}
```
PIX key types can be one of: `CPF`, `CNPJ`, `PHONE`, `EMAIL`, or `RANDOM`.
```json theme={null}
{
"accountType": "UPI",
"vpa": "somecustomer@okbank",
"platformAccountId": "upi_primary_1234"
}
```
```json theme={null}
{
"accountType": "IBAN",
"iban": "DE89370400440532013000",
"bankName": "Deutsche Bank",
"platformAccountId": "deutsche_primary_3456"
}
```
## Data Validation
The Grid API performs validation on all customer data. Common validation rules include:
* All required fields must be present based on customer type
* Date of birth must be in YYYY-MM-DD format and represent a valid date
* Names must not contain special characters or excessive spaces
* Bank account information must follow country-specific formats
* Addresses must include all required fields including country code
If validation fails, the API will return a 400 Bad Request response with detailed error information.
## Best Practices
1. **Identity Verification**: Choose a proper KYC/KYB identity verification flow as detailed in the [Quickstart Guide](/payouts-and-b2b/quickstart#choosing-your-onboarding-flow)
2. **Data Security**: Store and transmit customer data securely, following data protection regulations
3. **Regular Updates**: Keep customer information up to date, especially banking details
4. **Error Handling**: Implement proper error handling to manage validation failures gracefully
5. **Idempotent Operations**: Use your platformCustomerId consistently to avoid duplicate customer creation
## Bulk Customer Import Operations
For scenarios where you need to add many customers to the system at once, the API provides a CSV file upload endpoint.
### CSV File Upload
For large-scale customer imports, you can upload a CSV file containing customer information:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/customers/bulk/csv" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-F "file=@customers.csv"
```
The CSV file should follow a specific format with required and optional columns based on customer type. Here's an example:
```csv theme={null}
platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,accountType,accountNumber,bankName,platformAccountId,businessLegalName,routingNumber,accountCategory
customer123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US,US_ACCOUNT,123456789,Chase Bank,chase_primary_1234,,222888888,SAVINGS
biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US,US_ACCOUNT,987654321,Bank of America,boa_business_5678,Acme Corp,121212121,CHECKING
```
CSV Upload Best Practices
1. Use a spreadsheet application to prepare your CSV file
2. Validate data before upload (e.g., date formats, required fields)
3. Include a header row with column names
4. Use UTF-8 encoding for special characters
5. Keep file size under 100MB for optimal processing
You can track the job status through:
1. Webhook notifications (if configured)
2. Status polling endpoint:
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/customers/bulk/jobs/{jobId}" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Example job status response:
```json theme={null}
{
"jobId": "job_123456789",
"status": "PROCESSING",
"progress": {
"total": 5000,
"processed": 2500,
"successful": 2499,
"failed": 1
},
"errors": [
{
"platformCustomerId": "biz456",
"error": {
"code": "validation_error",
"message": "Invalid bank account number"
}
}
]
}
```
Best Practices for Bulk Operations
1. Use platform customer IDs to track individual customers in the bulk operation
2. Implement proper error handling for partial successes
3. Consider breaking very large datasets into multiple smaller jobs
4. Use webhooks for real-time status updates on asynchronous jobs
5. For CSV uploads, validate your data before submission
### CSV Format
The CSV file should have the following columns:
Required columns for all customers:
* platformCustomerId: Your platform's unique identifier for the customer
* customerType: Either "INDIVIDUAL" or "BUSINESS"
Required columns for individual customers:
* fullName: Individual's full name
* birthDate: Date of birth in YYYY-MM-DD format
* addressLine1: Street address line 1
* city: City
* state: State/Province/Region
* postalCode: Postal/ZIP code
* country: Country code (ISO 3166-1 alpha-2)
* accountType: Bank account type (CLABE, US\_ACCOUNT, PIX, IBAN, UPI)
* accountNumber: Bank account number
* bankName: Name of the bank
Required columns for business customers:
* businessLegalName: Legal name of the business
* addressLine1: Street address line 1
* city: City
* state: State/Province/Region
* postalCode: Postal/ZIP code
* country: Country code (ISO 3166-1 alpha-2)
* accountType: Bank account type (CLABE, US\_ACCOUNT, PIX, IBAN, UPI)
* accountNumber: Bank account number
* bankName: Name of the bank
Optional columns for all customers:
* addressLine2: Street address line 2
* platformAccountId: Your platform's identifier for the bank account
* description: Optional description for the customer
Optional columns for individual customers:
* email: Customer's email address
Optional columns for business customers:
* businessRegistrationNumber: Business registration number
* businessTaxId: Tax identification number
Additional required columns based on account type:
For US\_ACCOUNT:
* routingNumber: ACH routing number (9 digits)
* accountCategory: Either "CHECKING" or "SAVINGS"
For CLABE:
* clabeNumber: 18-digit CLABE number
For PIX:
* pixKey: PIX key value
* pixKeyType: Type of PIX key (CPF, CNPJ, EMAIL, PHONE, RANDOM)
For UPI:
* vpa: Virtual Payment Address for UPI payments
For IBAN:
* iban: International Bank Account Number
* swiftBic: SWIFT/BIC code (8 or 11 characters)
# Implementation Overview
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/onboarding/implementation-overview
This page gives you a 10,000‑ft view of an end‑to‑end implementation. It is intentionally generalized because the flow supports multiple customer types and external account types (e.g., CLABE, IBAN, US accounts, UPI). The detailed guides that follow provide concrete fields, edge cases, and step‑by‑step instructions.
This overview highlights the main building blocks: platform setup, onboarding, funding, payout accounts, sending and receiving flows, reconciliation, sandbox testing, and go‑live enablement.
## Platform configuration
Configure your platform once before building customer flows.
* Provide webhook endpoints for outgoing and incoming payment notifications
* Generate API credentials for Sandbox (and later Production)
* Review regional capabilities (rails, currencies, settlement windows)
## Onboarding customers
Onboard customers and accounts. There are two patterns:
* Regulated entities can directly create customers by providing KYC/KYB data via API
* Unregulated entities should request a KYC link and embed the hosted KYC flow; once completed, the customer can transact.
You'll also need to persist the Grid customer IDs for use in payment flows
## Account funding
Choose how transactions are funded based on your product design and region.
* Prefunded: Maintain balances in one or more currencies/cryptocurrencies and spend from those balances
* Just‑in‑time (JIT): Create a quote and fund it in real time using the payment instructions provided; ideal when you don’t wish to hold float
You can mix models as necessary. But it may make reconciliation more complex.
## External account creation
Register accounts your customers will send to or receive from, such as CLABE (MX), IBAN (EU/UK), ACH/RTP(US), UPI (IN), Spark address, and others.
* Capture beneficiary details (individual or business) and required banking fields
* Validate account formats where applicable and map them to your internal customer
## Sending payments
Sending consists of lookup, pricing, funding, and execution.
* Resolve the counterparty: look up receiver account for compliance review and to determine capabilities
* Create a quote: specify source/destination, currencies, and whether you lock sending or receiving amount; receive exchange rate, limits, fees, and (for JIT) funding instructions
* Fund and execute: for prefunded, confirm/execute; for JIT, push funds exactly as instructed (amount, reference) and the platform handles FX and delivery
* Observe status via webhooks and surface outcomes in your UI
## Receiving payments
Enable customers to receive funds to their linked bank account.
* Expose customer addressing to payers
* The platform handles conversion and offramping to the receiver’s account currency
* Approve or auto‑approve per your policy; update balances on completion via webhooks
## Reconciling transactions
Implement operational processes to keep your ledger in sync.
* Process webhooks idempotently; map statuses (pending, processing, completed, failed)
* Tie transactions back to quotes and customers; persist references
* Query for transactions by date range or other filters as necessary
## Testing in Sandbox
Use Sandbox to build and validate end‑to‑end without moving real funds.
* Exercise receiver lookup, quote creation, funding instructions, and webhook lifecycles
* Validate compliance decisioning with realistic but synthetic data
* Optionally use the Test Wallet as a counterparty for faster iteration (see Tools)
## Enabling Production
When you’re ready to go live:
* Complete corridor and provider onboarding as needed for your regions
* Confirm webhook security, monitoring, and alerting are in place
* Review rate limits, error handling, retries, and idempotency keys
* Run final UAT in Sandbox, then request Production access from our team
Contact our team to enable Production and finalize corridor activations.
# Platform Configuration
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/onboarding/platform-configuration
Configuring credentials, webhooks and currencies for your platform
## Supported currencies
During onboarding, choose the currencies your platform will support. For prefunded models, Grid automatically creates per‑currency accounts for each new customer. You can add or remove supported currencies anytime in the Grid dashboard.
## API credentials and authentication
Create API credentials in the Grid dashboard. Credentials are scoped to an environment (Sandbox or Production) and cannot be used across environments.
* Authentication: Use HTTP Basic Auth with your API key and secret in the `Authorization` header.
* Keys: Sandbox keys only work against Sandbox; Production keys only work against Production.
Never share or expose your API secret. Rotate credentials periodically and restrict access.
### Example: HTTP Basic Auth in cURL
```bash theme={null}
# Using cURL's Basic Auth shorthand (-u):
curl -sS -X GET "https://api.lightspark.com/grid/2025-10-13/config" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
## Base API path
The base API path is consistent across environments; your credentials determine the environment.
Base URL: `https://api.lightspark.com/grid/2025-10-13` (same for Sandbox and Production; your keys select the environment).
## Webhooks and signature verification
Configure your webhook endpoint to receive payment lifecycle events. Webhooks use asymmetric (public/private key) signatures; verify each webhook using the Grid public key available in your dashboard.
* Expose a public HTTPS endpoint (for development, reverse proxies like ngrok can help). You'll also need to set your webhook endpoint in the Grid dashboard.
* When receiving webhooks, verify the `X-Grid-Signature` header against the exact request body using the dashboard-provided public key
* Process events idempotently and respond with 2xx on success
You can trigger a test delivery from the API to validate your endpoint setup. The public key for verification is shown in the dashboard; rotate and update it when instructed by Lightspark.
### Test your webhook endpoint
Use the webhook test endpoint to send a synthetic event to your configured endpoint.
```bash theme={null}
curl -sS -X POST "https://api.lightspark.com/grid/2025-10-13/webhooks/test" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
Example test webhook payload:
```json theme={null}
{
"test": true,
"timestamp": "2023-08-15T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000001",
"type": "TEST"
}
```
For more details about webhooks like retry policy and examples, take a look at our Webhooks documentation.
# Error Handling
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/payment-flow/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 transaction, quote, or customer 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 |
| `409 Conflict` | Resource conflict | Quote already executed, external account already exists |
| `412 Precondition Failed` | UMA version mismatch | Counterparty doesn't support required UMA version |
| `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 support) |
| `501 Not Implemented` | Not implemented | Feature not yet supported |
## API error responses
All error responses include a structured format:
```json theme={null}
{
"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 format
**Solution:** Check request parameters match API specification
```javascript theme={null}
// 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 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;
}
}
```
**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 createTransferOut({ accountId, amount });
}
```
**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;
}
```
## 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",
"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
```json theme={null}
{
"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
```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) {
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
```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
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;
}
}
```
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,
};
}
```
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.",
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:
```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
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;
}
```
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 createTransferOut(params);
processedTransactions.add(idempotencyKey);
return result;
} catch (error) {
// Don't mark as processed on error
throw error;
}
}
```
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
Learn how to send payments from internal accounts
Query and filter payment history
Match payments with your internal systems
# List Transactions
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/payment-flow/list-transactions
Query and filter payment history with powerful filtering and pagination options
Retrieve transaction history with flexible filtering options. Transactions are returned in descending order (most recent first) with a default limit of 20 per page.
```bash cURL theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
{
"data": [
{
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
"status": "COMPLETED",
"type": "OUTGOING",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "EUR"
},
"sentAmount": {
"amount": 10000,
"currency": {
"code": "USD",
"symbol": "$",
"decimals": 2
}
},
"receivedAmount": {
"amount": 9200,
"currency": {
"code": "EUR",
"symbol": "€",
"decimals": 2
}
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"platformCustomerId": "customer_12345",
"description": "Payment for services - Invoice #1234",
"exchangeRate": 0.92,
"settledAt": "2025-10-03T15:30:00Z",
"createdAt": "2025-10-03T15:00:00Z"
}
],
"hasMore": true,
"nextCursor": "eyJpZCI6IlRyYW5zYWN0aW9uOjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAzMCJ9",
"totalCount": 45
}
```
## Filtering transactions
Use query parameters to filter and narrow down transaction results.
### Filter by customer
Get all transactions for a specific customer:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Or use your platform's customer ID:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?platformCustomerId=customer_12345' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
### Filter by transaction type
Get only incoming or outgoing transactions:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?type=OUTGOING' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?type=INCOMING' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
### Filter by status
Get transactions in a specific status:
```bash theme={null}
# Get all pending transactions
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?status=PENDING' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
# Get all completed transactions
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?status=COMPLETED' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
**Available statuses:**
* `PENDING` - Transaction initiated, awaiting processing
* `PROCESSING` - Transaction in progress
* `COMPLETED` - Transaction successfully completed
* `FAILED` - Transaction failed
* `REJECTED` - Transaction rejected
### Filter by date range
Get transactions within a specific date range:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?startDate=2025-10-01T00:00:00Z&endDate=2025-10-31T23:59:59Z' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Dates must be in ISO 8601 format (e.g., `2025-10-03T15:00:00Z`).
### Filter by account
Get transactions for a specific account:
```bash theme={null}
# Any transactions involving this account (sent or received)
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?accountIdentifier=InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
# Only transactions sent FROM this account
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?senderAccountIdentifier=InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
# Only transactions sent TO this account
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?receiverAccountIdentifier=ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
## Combining filters
You can combine multiple filters to narrow down results:
```bash theme={null}
# Get completed outgoing transactions for a customer in October 2025
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001&type=OUTGOING&status=COMPLETED&startDate=2025-10-01T00:00:00Z&endDate=2025-10-31T23:59:59Z' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
## Pagination
Handle large result sets with cursor-based pagination:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?limit=20' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Response:
```json theme={null}
{
"data": [
/* 20 transactions */
],
"hasMore": true,
"nextCursor": "eyJpZCI6IlRyYW5z...",
"totalCount": 150
}
```
Use the `nextCursor` from the previous response:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?limit=20&cursor=eyJpZCI6IlRyYW5z...' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Keep paginating while `hasMore` is `true`:
```javascript theme={null}
async function getAllTransactions() {
const allTransactions = [];
let cursor = null;
let hasMore = true;
while (hasMore) {
const url = cursor
? `https://api.lightspark.com/grid/2025-10-13/transactions?limit=100&cursor=${cursor}`
: `https://api.lightspark.com/grid/2025-10-13/transactions?limit=100`;
const response = await fetch(url, {
headers: { Authorization: `Basic ${credentials}` },
});
const { data, hasMore: more, nextCursor } = await response.json();
allTransactions.push(...data);
hasMore = more;
cursor = nextCursor;
}
return allTransactions;
}
```
The maximum `limit` is 100 transactions per request. Default is 20.
## Get a single transaction
Retrieve details for a specific transaction by ID:
```bash theme={null}
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'
```
## Best practices
Always implement pagination when fetching transactions to avoid timeouts and memory issues:
```javascript theme={null}
// Good: Paginated approach
async function fetchWithPagination(filters) {
const transactions = [];
let cursor = null;
do {
const page = await fetchTransactionPage(filters, cursor);
transactions.push(...page.data);
cursor = page.nextCursor;
} while (cursor);
return transactions;
}
// Bad: Trying to fetch everything at once
async function fetchAll() {
const response = await fetch("/transactions?limit=10000"); // Don't do this!
return response.json();
}
```
For data that doesn't change frequently (like completed transactions), implement caching:
```javascript theme={null}
const cache = new Map();
async function getCompletedTransactions(customerId, month) {
const cacheKey = `${customerId}-${month}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const transactions = await fetchMonthlyTransactions(customerId, month);
cache.set(cacheKey, transactions);
return transactions;
}
```
Apply filters to get only the data you need:
```javascript theme={null}
// Good: Specific filters
const recentFailed = await fetch(
"/transactions?status=FAILED&startDate=2025-10-01T00:00:00Z&type=OUTGOING"
);
// Bad: Fetch everything and filter in code
const all = await fetch("/transactions");
const filtered = all.data.filter(
(tx) =>
tx.status === "FAILED" &&
tx.type === "OUTGOING" &&
new Date(tx.createdAt) > new Date("2025-10-01")
);
```
Implement exponential backoff for rate limit errors:
```javascript theme={null}
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
const response = await fetch(url, {
headers: { Authorization: `Basic ${credentials}` },
});
if (response.status === 429) {
const retryAfter = parseInt(
response.headers.get("Retry-After") || "1",
10
);
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
continue;
}
return response.json();
}
throw new Error("Max retries exceeded");
}
```
## Next steps
Learn how to send payments from internal accounts
Match payments with your internal accounting systems
Handle transaction failures and errors
# Reconciliation
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/payment-flow/reconciliation
Match Grid transactions with your internal systems
This guide explains how to reconcile transactions between your internal systems and the Grid API using two complementary mechanisms: real-time webhooks and periodic queries.
Use webhooks for real-time updates and daily queries as a backstop to detect missed events or data drift.
## Handling webhooks
Listen for transaction webhooks to track transaction status change until a terminal state is reached.
Terminal statuses: `COMPLETED`, `REJECTED`, `FAILED`, `REFUNDED`, `EXPIRED`.
All other statuses will progress until reaching one of the above.
* **Outbound transactions**: The originating account is debited at transaction creation. If the transaction ultimately fails, a refund is posted back to the originating account.
* **Inbound transactions**: The receiving account is credited only on success. Failures do not change balances.
Grid retries failed webhooks up to 160 times over 7 days with exponential backoff. Use the dashboard to review and remediate webhook delivery issues.
Configure your webhook endpoint and verify signatures. See Webhooks.
Sample webhook payload:
```json theme={null}
{
"transaction": {
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
"status": "COMPLETED",
"type": "OUTGOING",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "EUR"
},
"sentAmount": {
"amount": 10000,
"currency": { "code": "USD", "symbol": "$", "decimals": 2 }
},
"receivedAmount": {
"amount": 9200,
"currency": { "code": "EUR", "symbol": "€", "decimals": 2 }
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"platformCustomerId": "customer_12345",
"createdAt": "2025-10-03T15:00:00Z",
"settledAt": "2025-10-03T15:30:00Z",
"description": "Payment for services - Invoice #1234"
},
"timestamp": "2025-10-03T15:30:01Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-0000000000ab",
"type": "OUTGOING_PAYMENT"
}
```
Use the `webhookId`, `transaction.id`, and `timestamp` to ensure idempotent handling, updating your internal ledger on each status transition.
When a transaction reaches a terminal state, finalize your reconciliation for that transaction.
## Reconcile via queries
Additionally, you can list transactions for a time window and compare with your internal records.
We recommend querying days from `00:00:00.000` to `23:59:59.999` in your preferred timezone.
```bash cURL theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?startDate=2025-10-01T00:00:00.000Z&endDate=2025-10-01T23:59:59.999Z&limit=100' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Response
```json theme={null}
{
"data": [
{
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000030",
"status": "COMPLETED",
"type": "OUTGOING",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "EUR"
},
"sentAmount": {
"amount": 10000,
"currency": { "code": "USD", "symbol": "$", "decimals": 2 }
},
"receivedAmount": {
"amount": 9200,
"currency": { "code": "EUR", "symbol": "€", "decimals": 2 }
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"platformCustomerId": "customer_12345",
"description": "Payment for services - Invoice #1234",
"exchangeRate": 0.92,
"settledAt": "2025-10-03T15:30:00Z",
"createdAt": "2025-10-03T15:00:00Z"
}
],
"hasMore": true,
"nextCursor": "eyJpZCI6IlRyYW5zYWN0aW9uOjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAzMCJ9",
"totalCount": 45
}
```
## Troubleshooting
* **Missing webhook**: Check delivery logs in the dashboard and ensure your endpoint returns `2xx`. Retries continue for 7 days.
* **Mismatched balances**: Re-query the date range and verify terminal statuses; remember outbound failures are refunded, inbound failures do not change balances.
* **Pagination gaps**: Always follow `nextCursor` until `hasMore` is `false`.
# Sending Payments
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/payment-flow/send-payment
Learn how to send payments from internal accounts to external bank accounts with same-currency and cross-currency transfers
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:
Send funds in the same currency from an internal account to an external account. Fast and straightforward.
Send funds with currency conversion using real-time exchange rates. Supports multiple fiat currencies and payment rails.
## 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](/payouts-and-b2b/depositing-funds/internal-accounts) and [External
Accounts](/payouts-and-b2b/depositing-funds/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
Retrieve the internal account (source) and external account (destination) IDs:
```bash theme={null}
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.
Create the transfer by specifying the source and destination accounts:
```bash cURL theme={null}
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
}'
```
```json Success (201 Created) theme={null}
{
"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.
The transaction is created with a `PENDING` status. Monitor the status by:
**Option 1: Webhook notifications** (recommended)
```json theme={null}
{
"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**
```bash theme={null}
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
Request a quote to lock in the exchange rate and get transfer details:
```bash cURL theme={null}
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"
}'
```
```json Success (201 Created) theme={null}
{
"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)
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.
Confirm and execute the quote to initiate the transfer:
```bash cURL theme={null}
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'
```
```json Success (200 OK) theme={null}
{
"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.
Track the transfer using webhooks or by polling the transaction:
```bash theme={null}
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:
```json theme={null}
{
"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:
```javascript theme={null}
// 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](/payouts-and-b2b/platform-tools/webhooks) for complete
webhook implementation details including signature verification.
### Polling transactions
If webhooks aren't available, poll the transaction endpoint:
```javascript theme={null}
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
Verify the internal account has sufficient balance to avoid failed transfers:
```javascript theme={null}
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.
Store quote IDs in your database to prevent accidental duplicate executions:
```javascript theme={null}
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;
}
```
Quotes expire after a short period. Always check expiration before executing:
```javascript theme={null}
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:
```javascript theme={null}
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:
```javascript theme={null}
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
Handle payment failures and error scenarios
Query and filter transaction history
Match payments with your internal systems
# Postman Collection
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/platform-tools/postman-collection
Use our hosted Postman collection to explore endpoints and send test requests quickly.
Launch the collection in Postman.
# Sandbox Testing
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/platform-tools/sandbox-testing
Test your payouts integration in the Grid sandbox environment
## Overview
The Grid sandbox environment allows you to test your payouts integration without moving real money. All API endpoints work the same way in sandbox as they do in production, but money movements are simulated and you can control test scenarios using special test values.
## Getting Started with Sandbox
### Sandbox Credentials
To use the sandbox environment:
1. Contact Lightspark to get your inital sandbox credentials configured. Email [support@lightspark.com](mailto:support@lightspark.com) to get started.
2. Add your sandbox API token and secret to your environment variables.
3. Use the normal production base URL: `https://api.lightspark.com/grid/2025-10-13`
4. Authenticate using your sandbox token with HTTP Basic Auth
## Simulating Money Movements
### Funding Internal Accounts
In production, internal accounts are funded by following the payment instructions (bank transfer, wire, etc.). In sandbox, you can instantly add funds to any internal account using the following endpoint:
```bash theme={null}
POST /sandbox/internal-accounts/{accountId}/fund
{
"amount": 100000 # $1,000 in cents
}
```
**Example:**
```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/sandbox/internal-accounts/InternalAccount:abc123/fund \
-u "sandbox_token_id:sandbox_token_secret" \
-H "Content-Type: application/json" \
-d '{
"amount": 100000
}'
```
This endpoint returns the updated `InternalAccount` object with the new balance.
Alternatively, you can also fund internal accounts using the `/quotes` or `/transfer-in` endpoints as described below.
## Testing Transfer Scenarios
### Adding Test External Accounts
The flows for creating external accounts in sandbox are the same as in production.
However, when creating external accounts in sandbox, you can use special account number patterns to simulate different
transfer behaviors. The **last 3 digits** of the account number determine the test scenario:
| Last Digits | Behavior | Use Case |
| ------------- | ----------------------- | ------------------------------------------- |
| **002** | Insufficient funds | Transfer-in fails immediately |
| **003** | Account closed/invalid | All transfers fail immediately |
| **004** | Transfer rejected | Bank rejects the transfer |
| **005** | Timeout/delayed failure | Transaction stays pending \~30s, then fails |
| **Any other** | Success | All transfers complete normally |
**Example - Creating a Test Account with Insufficient Funds:**
```bash theme={null}
POST /customers/external-accounts
{
"customerId": "Customer:123",
"currency": "USD",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "000000002", // Will trigger insufficient funds
"routingNumber": "110000000",
"accountCategory": "CHECKING",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Test User",
"address": {
"line1": "123 Test St",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}
```
These patterns apply to the primary identifier for any account type: US account numbers, IBANs, CLABEs, Spark wallet addresses, etc. Just ensure the identifier ends with the appropriate test digits.
For scenarios like PIX and UPI, where there's a domain part involved, append the test digits to the user name part. For example, if testing email addresses as a PIX key, the full identifier would be
"[testuser.002@pix.com.br](mailto:testuser.002@pix.com.br)" to trigger the insufficient funds scenario.
### Testing Transfer-In (Pull from External Account)
When you call `/transfer-in` with an external account created using test patterns, the transfer will complete instantly in sandbox with the behavior determined by the account number:
```bash theme={null}
POST /transfer-in
{
"source": {
"accountId": "ExternalAccount:abc123" // Uses test pattern from creation
},
"destination": {
"accountId": "InternalAccount:xyz789"
},
"amount": 10000 // $100 in cents
}
```
**Expected Behaviors:**
* **Success (default)**: Transaction completes immediately with status `COMPLETED`
* **Insufficient funds (002)**: Transaction fails immediately with appropriate error
* **Account closed (003)**: Transaction fails immediately with account validation error
* **Transfer rejected (004)**: Transaction fails immediately with rejection error
* **Timeout (009)**: Transaction shows `PENDING` status for \~30 seconds, then transitions to `FAILED`
### Testing Transfer-Out (Push to External Account)
Transfer-out works the same way - the destination external account's test pattern determines the outcome:
```bash theme={null}
POST /transfer-out
{
"source": {
"accountId": "InternalAccount:xyz789"
},
"destination": {
"accountId": "ExternalAccount:abc123" // Uses test pattern
},
"amount": 10000
}
```
The transfer will instantly simulate the bank transfer process and complete with the appropriate status based on the external account's test pattern.
## Testing Cross-Currency Quotes
### Creating Quotes with Test Accounts
When creating quotes with the `externalAccountDetails` destination type, you can provide test account patterns inline:
```bash theme={null}
POST /quotes
{
"source": {
"accountId": "InternalAccount:abc123"
},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:123",
"currency": "EUR",
"accountInfo": {
"accountType": "IBAN_ACCOUNT",
"iban": "DE89370400440532013003", // Ends in 003 = account closed
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Test User"
}
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100000
}
```
### Executing Quotes in Sandbox
For quotes from an external account source, execute as in production via `/quotes/{quoteId}/execute`. The sandbox will:
1. Instantly process the currency conversion
2. Apply the test behavior based on any external accounts involved
3. Update transaction statuses immediately (no waiting for bank processing)
4. Trigger webhooks for state changes
For quotes with payment instructions (no source account), use the existing `/sandbox/send` endpoint to simulate payment:
```bash theme={null}
POST /sandbox/send
{
"reference": "UMA-Q12345-REF", // From quote payment instructions
"currencyCode": "USD",
"currencyAmount": 100000
}
```
## Testing Webhooks
All webhook events fire normally in sandbox. To test your webhook endpoint:
1. Configure your webhook URL in the dashboard
2. Perform actions that trigger webhooks (transfers, quote execution, etc.)
3. Receive webhook events at your endpoint
4. Verify signature using the sandbox public key
You can also manually trigger a test webhook:
```bash theme={null}
POST /webhooks/test
{
"url": "https://your-app.com/webhooks"
}
```
## Common Testing Workflows
### Complete Payout Flow Test
Here's a complete test workflow for a USD → EUR payout:
1. **Create customer and internal accounts** (via regular API)
2. **Fund customer's USD internal account:**
```bash theme={null}
POST /sandbox/internal-accounts/InternalAccount:customer-usd/fund
{ "amount": 100000 } # $1,000
```
3. **Create a test external EUR account:**
```bash theme={null}
POST /customers/external-accounts
# Use default account number for success case
```
4. **Create and execute a quote:**
```bash theme={null}
POST /quotes
# USD internal → EUR external
POST /quotes/{quoteId}/execute
```
5. **Verify transaction status and webhooks**
### Testing Error Scenarios
Test each failure mode systematically:
```bash theme={null}
# 1. Test insufficient funds
# Create external account ending in 002
POST /customers/external-accounts { "accountNumber": "000000002" }
# Attempt transfer-in - should fail immediately
POST /transfer-in
# 2. Test account closed
# Create external account ending in 003
POST /customers/external-accounts { "accountNumber": "000000003" }
# Attempt transfer-out - should fail immediately
POST /transfer-out
# 3. Test timeout scenario
# Create external account ending in 005
POST /customers/external-accounts { "accountNumber": "000000005" }
# Attempt transfer - should pend then fail after ~30s
POST /transfer-in
# Check status immediately - will show PENDING
GET /transactions/{transactionId}
# Wait 30s, check again - will show FAILED
```
## Sandbox Limitations
While sandbox closely mimics production, there are some differences:
* **Instant settlement**: All transfers complete immediately (success cases) or fail immediately (error cases), except timeout scenarios (005)
* **No real bank validation**: Account numbers aren't validated against real banking networks
* **Simplified KYC**: KYC processes are simulated and complete instantly. You must add customers via the `/customers` endpoint, rather than using the KYC link flow.
* **Fixed exchange rates**: Currency conversion rates may not reflect real-time market rates.
Do not try sending money to any sandbox addresses or accounts. These are not real addresses and will not receive money.
## Moving to Production
When you're ready to move to production:
1. Generate production API tokens in the dashboard
2. Swap those credentials for the sandbox credentials in your environment variables
3. Remove any sandbox-specific test patterns from your code
4. Configure production webhook endpoints
5. Test with small amounts first
## Next Steps
* Review [Webhooks](/payouts-and-b2b/platform-tools/webhooks) for event handling
* Check out the [Postman Collection](/payouts-and-b2b/platform-tools/postman-collection) for API examples
* See [Error Handling](/payouts-and-b2b/payment-flow/error-handling) for production error strategies
# Webhooks
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/platform-tools/webhooks
All webhooks sent by the Grid API include a signature in the `X-Grid-Signature` header, which allows you to verify the authenticity of the webhook. This is critical for security, as it ensures that only legitimate webhooks from Grid are processed by your system.
## Signature Verification Process
1. **Obtain your Grid public key**
* This is provided to you during the integration process. Reach out to us at [support@lightspark.com](mailto:support@lightspark.com) or over Slack to get the public key.
* The key is in PEM format and can be used with standard cryptographic libraries
2. **Verify incoming webhooks**
* Extract the signature from the `X-Grid-Signature` header
* Decode the base64 signature
* Create a SHA-256 hash of the entire request body
* Verify the signature using the Grid webhook public key and the hash
* Only process the webhook if the signature verification succeeds
## Verification Examples
### Node.js Example
```javascript theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();
// Your Grid public key provided during integration
const GRID_WEBHOOK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----`;
app.post('/webhooks/uma', (req, res) => {
const signatureHeader = req.header('X-Grid-Signature');
if (!signatureHeader) {
return res.status(401).json({ error: 'Signature missing' });
}
try {
let signature: Buffer;
try {
// Parse the signature as JSON. It's in the format {"v": "1", "s": "base64_signature"}
const signatureObj = JSON.parse(signatureHeader);
if (signatureObj.v && signatureObj.s) {
// The signature is in the 's' field
signature = Buffer.from(signatureObj.s, "base64");
} else {
throw new Error("Invalid JSON signature format");
}
} catch {
// If JSON parsing fails, treat as direct base64
signature = Buffer.from(signatureHeader, "base64");
}
// Create verifier with the public key and correct algorithm
const verifier = crypto.createVerify("SHA256");
const payload = await request.text();
verifier.update(payload);
verifier.end();
// Verify the signature using the webhook public key
const isValid = verifier.verify(
{
key: GRID_WEBHOOK_PUBLIC_KEY,
format: "pem",
type: "spki",
},
signature,
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Webhook is verified, process it based on type
const webhookData = req.body;
if (webhookData.type === 'INCOMING_PAYMENT') {
// Process incoming payment webhook
// ...
} else if (webhookData.type === 'OUTGOING_PAYMENT') {
// Process outgoing payment webhook
// ...
}
// Acknowledge receipt of the webhook
return res.status(200).json({ received: true });
} catch (error) {
console.error('Signature verification error:', error);
return res.status(401).json({ error: 'Signature verification failed' });
}
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
```
### Python Example
```python theme={null}
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
from flask import Flask, request, jsonify
import base64
app = Flask(__name__)
# Your Grid public key provided during integration
GRID_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----"""
# Load the public key
public_key = serialization.load_pem_public_key(
GRID_PUBLIC_KEY.encode('utf-8')
)
@app.route('/webhooks/uma', methods=['POST'])
def handle_webhook():
# Get signature from header
signature = request.headers.get('X-Grid-Signature')
if not signature:
return jsonify({'error': 'Signature missing'}), 401
try:
# Get the raw request body
request_body = request.get_data()
# Create a SHA-256 hash of the request body
hash_obj = hashes.Hash(hashes.SHA256())
hash_obj.update(request_body)
digest = hash_obj.finalize()
# Decode the base64 signature
signature_bytes = base64.b64decode(signature)
# Verify the signature
try:
public_key.verify(
signature_bytes,
request_body,
ec.ECDSA(hashes.SHA256())
)
except Exception as e:
return jsonify({'error': 'Invalid signature'}), 401
# Webhook is verified, process it based on type
webhook_data = request.json
if webhook_data['type'] == 'INCOMING_PAYMENT':
# Process incoming payment webhook
# ...
pass
elif webhook_data['type'] == 'OUTGOING_PAYMENT':
# Process outgoing payment webhook
# ...
pass
# Acknowledge receipt of the webhook
return jsonify({'received': True}), 200
except Exception as e:
print(f'Signature verification error: {e}')
return jsonify({'error': 'Signature verification failed'}), 401
if __name__ == '__main__':
app.run(port=3000)
```
## Testing
To test your webhook implementation, you can trigger a test webhook from the Grid dashboard. This will send a test webhook to the endpoint you provided during the integration process. The test webhook will also be sent automatically when you update your platform configuration with a new webhook URL.
An example of the test webhook payload is shown below:
```json theme={null}
{
"test": true,
"timestamp": "2023-08-15T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
"type": "TEST"
}
```
You should verify the signature of the webhook using the Grid public key and the process outlined in the [Signature Verification Process](#signature-verification-process) section and then reply with a 200 OK response to acknowledge receipt of the webhook.
## Security Considerations
* **Always verify signatures**: Never process webhooks without verifying their signatures.
* **Use HTTPS**: Ensure your webhook endpoint uses HTTPS to prevent man-in-the-middle attacks.
* **Implement idempotency**: Use the `webhookId` field to prevent processing duplicate webhooks.
* **Timeout handling**: Implement proper timeout handling and respond to webhooks promptly.
## Retry Policy
The Grid API will retry webhooks with the following policy based on the webhook type:
| Webhook Type | Retry Policy | Notes |
| ------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| TEST | No retries | Used for testing webhook configuration |
| OUTGOING\_PAYMENT | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| INCOMING\_PAYMENT | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on: 409 (duplicate webhook) or PENDING status since it is served as an approval mechanism in-flow |
| BULK\_UPLOAD | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| INVITATION\_CLAIMED | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| KYC\_STATUS | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| ACCOUNT\_STATUS | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
# Quickstart
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/quickstart
Send your first cross-border payment
This quickstart covers an example of sending a prefunded cross-border payout for a business customer on an unregulated platform.
## Understanding Entity Mapping for B2B Payouts
In this guide, the entities map as follows:
| Entity Type | Who They Are | In This Example |
| -------------------- | -------------------------- | --------------------------------------------- |
| **Platform** | Your payouts platform | Your company providing AP automation |
| **Customer** | Business sending payments | Your client company (e.g., Acme Corp) |
| **External Account** | Vendors receiving payments | Maria Garcia (freelance contractor in Mexico) |
**Flow**: Your customer (a business) funds their internal account → uses your platform to send payments → to their vendors' external bank accounts.
## Get API credentials
Create Sandbox API credentials in the dashboard, then set environment variables for local use.
```bash theme={null}
export GRID_BASE_URL="https://api.lightspark.com/grid/2025-10-13"
export GRID_CLIENT_ID="YOUR_SANDBOX_CLIENT_ID"
export GRID_CLIENT_SECRET="YOUR_SANDBOX_CLIENT_SECRET"
```
Use Basic Auth in cURL with `-u "$GRID_CLIENT_ID:$GRID_API_SECRET"`.
## Onboard a Customer
Onboard a customer using the hosted KYC/KYB link flow.
### Generate KYC Link
Call the `/customers/kyc-link` endpoint with your `redirectUri` parameter to generate a hosted KYC URL for your customer.
The `redirectUri` parameter is embedded in the generated KYC URL and will be used to automatically redirect the customer back to your application after they complete verification.
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/customers/kyc-link?redirectUri=https://yourapp.com/onboarding-complete&platformCustomerId=019542f5-b3e7-1d02-0000-000000000001" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
**Response:**
```json theme={null}
{
"kycUrl": "https://kyc.lightspark.com/onboard/abc123def456",
"platformCustomerId": "019542f5-b3e7-1d02-0000-000000000001"
}
```
### Redirect Customer
Redirect your customer to the returned `kycUrl` where they can complete their identity verification in the hosted interface.
The KYC link is single-use and expires after a limited time period for security.
### Customer Completes Verification
The customer completes the identity verification process in the hosted KYC interface, providing required documents and information.
The hosted interface handles document collection, verification checks, and compliance requirements automatically.
After verification processing, you'll receive a KYC status webhook notification indicating the final verification result.
### Redirect back to your app
Upon successful KYC completion, the customer is automatically redirected to your specified `redirectUri` URL.
On your redirect page, handle the completed KYC flow and integrate the new customer into your application.
The customer account will be automatically created by the system upon successful KYC completion. You can identify the new customer using your `platformCustomerId` or other identifiers.
## Get the Customer's Internal Account
Once the customer is created, internal accounts will automatically be created on their behalf. Get their internal account in the desired currency for funding instructions.
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/internal-account?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001¤cy=USD" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
**Response:**
```json theme={null}
{
"data": [
{
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 0, // USD balance in cents
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"fundingPaymentInstructions": [
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"reference": "FUND-ABC123",
"accountType": "US_ACCOUNT",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
},
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
],
"createdAt": "2025-10-03T12:00:00Z",
"updatedAt": "2025-10-03T12:00:00Z"
}
],
"hasMore": false,
"totalCount": 1
}
```
The `fundingPaymentInstructions` provide the bank account details and reference code needed to fund
this internal account via ACH or wire transfer from the customer's bank. It might also include stablecoin
funding details for instant funding of the internal account.
## Fund the Internal Account
For this quickstart, we'll fund the account using the `/sandbox/internal-accounts/{accountId}/fund` endpoint to simulate receiving funds.
In production, your customer would initiate a transfer from their bank or wallet to the account details provided in the funding instructions,
making sure to include the reference code `FUND-ABC123` in the transfer memo if applicable.
```bash theme={null}
# Sandbox: fund internal account directly
curl -X POST "https://api.lightspark.com/grid/2025-10-13/sandbox/internal-accounts/InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965/fund" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"currencyCode": "USD",
"currencyAmount": 100000,
"reference": "FUND-ABC123"
}'
```
During the funding process, you'll receive transaction status update webhooks.
**Webhook Notification:**
```json theme={null}
{
"transaction": {
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000010",
"status": "COMPLETED",
"type": "INCOMING",
"receivedAmount": {
"amount": 100000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"settledAt": "2025-10-03T14:30:00Z",
"createdAt": "2025-10-03T14:25:00Z",
"description": "Internal account funding"
},
"timestamp": "2025-10-03T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
"type": "INCOMING_PAYMENT"
}
```
The internal account now has a balance of \$1,000.00 (100000 cents).
## Add the beneficiary as an External Account
Now add the beneficiary bank account where you want to send the funds. In this example, we'll add a
Mexican CLABE account as the external account.
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/customers/external-accounts" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "MXN",
"platformAccountId": "maria_garcia_account",
"accountInfo": {
"accountType": "CLABE",
"clabeNumber": "123456789012345678",
"bankName": "BBVA Mexico",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Maria Garcia",
"birthDate": "1990-01-01",
"nationality": "MX",
"address": {
"line1": "Av. Reforma 123",
"city": "Ciudad de México",
"state": "CDMX",
"postalCode": "06600",
"country": "MX"
}
}
}
}'
```
**Response:**
```json theme={null}
{
"id": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"status": "ACTIVE",
"currency": "MXN",
"platformAccountId": "maria_garcia_account",
"accountInfo": {
"accountType": "CLABE",
"clabeNumber": "123456789012345678",
"bankName": "BBVA Mexico",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Maria Garcia",
"birthDate": "1990-01-01",
"nationality": "MX",
"address": {
"line1": "Av. Reforma 123",
"city": "Ciudad de México",
"state": "CDMX",
"postalCode": "06600",
"country": "MX"
}
}
}
}
```
## Create a quote
Create a quote to lock in the exchange rate, fees, and get the transfer details.
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"lookupId": "LookupRequest:019542f5-b3e7-1d02-0000-000000000009", # ID from the lookup step
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965" # USD internal account
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "MXN"
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 50000,
"description": "Payment to Maria Garcia for services"
}'
```
**Amount Locking**: You can lock either the sending amount (`SENDING`) or receiving amount (`RECEIVING`).
In this example, we're locking the sending amount to exactly \$500.00 USD (50000 cents). Alternatively,
you can lock the receiving amount to ensure that the receiver receives exactly some amount of the
destination currency.
**Response:**
```json theme={null}
{
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006",
"status": "PENDING",
"createdAt": "2025-10-03T15:00:00Z",
"expiresAt": "2025-10-03T15:05:00Z",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "MXN"
},
"sendingCurrency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
},
"receivingCurrency": {
"code": "MXN",
"name": "Mexican Peso",
"symbol": "$",
"decimals": 2
},
"totalSendingAmount": 50000,
"totalReceivingAmount": 861250,
"exchangeRate": 17.25,
"feesIncluded": 250,
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000015"
}
```
The quote shows:
* **Sending**: \$500.00 USD (including \$2.50 fee)
* **Receiving**: \$8,612.50 MXN
* **Exchange rate**: 17.25 MXN per USD
* **Quote expires**: In 5 minutes
Quotes typically expire in 1-5 minutes. Make sure to execute the quote before the `expiresAt` timestamp, or you'll need to create a new quote.
## Execute the quote
Execute the quote to initiate the transfer from the internal account to the external bank account.
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes/Quote:019542f5-b3e7-1d02-0000-000000000006/execute" \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
**Response:**
```json theme={null}
{
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006",
"status": "PROCESSING",
"createdAt": "2025-10-03T15:00:00Z",
"expiresAt": "2025-10-03T15:05:00Z",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "MXN"
},
"sendingCurrency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
},
"receivingCurrency": {
"code": "MXN",
"name": "Mexican Peso",
"symbol": "$",
"decimals": 2
},
"totalSendingAmount": 50000,
"totalReceivingAmount": 861250,
"exchangeRate": 17.25,
"feesIncluded": 250,
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000015"
}
```
The quote status changes to `PROCESSING` and the transfer is initiated. You can track the status by:
1. Polling the quote endpoint: `GET /quotes/{quoteId}`
2. Waiting for webhook notifications
**Completion Webhook:**
```json theme={null}
{
"transaction": {
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000015",
"status": "COMPLETED",
"type": "OUTGOING",
"sentAmount": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"receivedAmount": {
"amount": 861250,
"currency": {
"code": "MXN",
"name": "Mexican Peso",
"symbol": "$",
"decimals": 2
}
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"settledAt": "2025-10-03T15:02:30Z",
"createdAt": "2025-10-03T15:00:00Z",
"description": "Payment to Maria Garcia for services",
"exchangeRate": 17.25,
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006"
},
"timestamp": "2025-10-03T15:03:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000025",
"type": "OUTGOING_PAYMENT"
}
```
Congrats, you've sent a real time cross-border payout to a Mexican bank account!
# Core Concepts
Source: https://ramps-feat-building-with-ai.mintlify.app/payouts-and-b2b/terminology
Core concepts and terminology for the Grid API
There are several key entities in the Grid API: **Platform**, **Customers**, **Internal Accounts**, **External Accounts**, **Quotes**, **Transactions**, and **UMA Addresses**.
## Businesses, People, and Accounts
### Platform
Your **platform** is you! It's the top-level entity that integrates with the Grid API. The platform:
* Has its own configuration (webhook endpoint, supported currencies, API tokens, etc.)
* A platform can have many customers both business and individual
* Manages multiple customers and their accounts
* Can hold platform-owned internal accounts for settlement and liquidity management
* Acts as the integration point between your application and the open Money Grid
### Customers
**Customers** are your end users who send and receive payments through your platform. Each customer:
* Can be an individual or business entity
* Has a KYC/KYB status that determines their ability to transact. If you are a regulated financial institution, this will typically be `APPROVED` since you do the KYC/KYB yourself.
* Is identified by both a system-generated ID and optionally your platform-specific customer ID
* May have associated internal accounts and external accounts
* May have a unique **UMA address** (e.g., `$john.doe@yourdomain.com`). If you don't assign an UMA address when creating a customer, they will be assigned a system-generated one.
### Internal Accounts
**Internal accounts** are Grid-managed accounts that hold balances in specific currencies. They can belong to either:
* **Platform internal accounts** - Owned by the platform for settlement, liquidity, and float management
* **Customer internal accounts** - Associated with specific customers for holding funds
Internal accounts:
* Have balances in a single currency (USD, EUR, MXN, etc.)
* Can be funded via bank transfers or crypto deposits using payment instructions
* Are used as sources or destinations for transactions instantly 24/7/365
* Track available balance for sending payments or receiving funds
### External Accounts
**External accounts** are traditional bank accounts, crypto wallets, or other payment instruments connected to customers
for on-ramping or off-ramping funds. Each external account:
* Are associated with a specific customer or the platform
* Represents a real-world bank account (with routing number, account number, IBAN, etc.), wallet, or payment instrument
* Has an associated beneficiary (individual or business) who receives payments from the customer or platform
* Has a status indicating screening status (ACTIVE, PENDING, INACTIVE, etc.)
* Can be used as a destination for quote-based transfers or same currency transfers like withdrawals
* For pullable sources like debit cards or ACH pulls, an external account can be used as a source for transfers-in to
fund internal accounts or to fund cross-border transfers via quotes.
## Entity Examples by Use Case
Understanding how entities map to your specific use case helps clarify your integration architecture. Here are common examples:
### B2B Payouts Platform (e.g., Bill.com, Routable)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------- | ------------------------------------------ |
| **Platform** | The payouts platform itself | Your company providing AP automation |
| **Customer** | Businesses sending payments to vendors | Acme Corp (your client company) |
| **External Account** | Vendors/suppliers receiving payments | Office supply vendor, freelance contractor |
**Flow**: Acme Corp (customer) uses your platform to pay their vendor invoices → funds move from Acme's internal account → to vendor's external bank account
### Direct Rewards Platform (Platform-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | ------------------------------------------- | ----------------------------------- |
| **Platform** | The app paying rewards directly to users | Your cashback app |
| **Customer** | (Not used in this model) | N/A |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Your platform sends micro-payouts directly from platform internal accounts → to users' external crypto wallets at scale. Common for cashback apps where the platform earns affiliate commissions and shares them with users.
### White-Label Rewards Platform (Customer-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------------- | ----------------------------------- |
| **Platform** | The rewards infrastructure provider | Your white-label rewards API |
| **Customer** | Brands or merchants running reward campaigns | Nike, Starbucks |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Nike (customer) funds their internal account → your platform sends rewards on their behalf → to users' external crypto wallets. Common for brand loyalty programs where merchants manage their own reward budgets.
### Remittance/P2P App (e.g., Wise, Remitly)
| Entity Type | Who They Are | Example |
| -------------------- | ----------------------------------- | ---------------------------------------------------------------- |
| **Platform** | The remittance service | Your money transfer app |
| **Customer** | Both sender and recipient of funds | Maria (sender in US), Juan (recipient in Mexico) |
| **External Account** | Bank accounts for funding/receiving | Maria's US bank (funding), Juan's Mexican bank (receiving funds) |
**Flow**: Maria (customer) funds transfer from her external account → to Juan (also a customer) → who receives funds in his external bank account. Alternatively, Maria could send to Juan's UMA address directly.
## Transactions and Addressing Entities
### Quotes
**Quotes** provide locked-in exchange rates and payment instructions for transfers. A quote:
* Specifies a source (internal account, customer ID, or the platform itself) and destination (internal/external account or UMA address)
* Locks an exchange rate for a short period (typically 1-5 minutes) or can be immediately executed with the `immediatelyExecute` flag
* Calculates total fees and amounts for currency conversion
* Provides payment instructions for funding the transfer if needed, or can be funded via an internal account balance.
* Must be executed before it expires
* Creates a transaction when executed
### Transactions
**Transactions** represent completed or in-progress payment transfers. Each transaction:
* Has a type (INCOMING or OUTGOING from the platform's perspective)
* Has a status (PENDING, COMPLETED, FAILED, etc.)
* References a customer (sender for outgoing, recipient for incoming) or a platform internal account
* Specifies source and destination (accounts or UMA addresses)
* Includes amounts, currencies, and settlement information
* May include counterparty information for compliance purposes if required by your platform configuration
Transactions are created when:
* A quote is executed (either incoming or outgoing)
* A same currency transfer is initiated (transfer-in or transfer-out)
### UMA Addresses (optional)
**UMA addresses** are human-readable payment identifiers that follow the format `$username@domain.com`. They:
* Uniquely identify entities on the Grid network
* Enable sending and receiving payments across different platforms without knowing the recipient's underlying account details or personal information
* Support currency negotiation and cross-border transfers
* Work similar to email addresses but for payments
* Are an optional UX improvement for some use cases. Use of UMA addresses is not required in order to use the Grid API.
# Building with AI
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/building-with-ai
Use AI coding assistants like Claude Code, Cursor, and Codex to explore, build, and debug with the Grid API
Grid's documentation, OpenAPI spec, and CLI are designed to work with AI coding assistants like Claude Code, Cursor, and Codex. Whether you're exploring the API for the first time or building a production integration, AI tools can help you move faster.
## What AI assistants can do with Grid
Ask questions about endpoints, request/response schemas, supported currencies, and payment rails
Use the Grid Skill to create customers, manage accounts, get quotes, and send payments directly from your AI assistant
Get step-by-step guidance for multi-step flows like international payouts, on/off-ramps, and KYC onboarding
Troubleshoot error codes, validate account details, and diagnose failed transactions
## AI-accessible documentation
These Grid docs are automatically available to LLMs and AI tools in machine-readable formats — no configuration needed.
### llms.txt
Grid docs generate [llms.txt](https://llmstxt.org/) files that give AI tools a structured index of all documentation:
* [`/llms.txt`](/llms.txt) — Concise index of all pages with titles and descriptions
* [`/llms-full.txt`](/llms-full.txt) — Complete documentation content in a single file
These are generated automatically and always up to date. Use `llms-full.txt` when you want to give an AI assistant full context about the Grid API in one shot.
### Markdown export
Each page in the Grid docs is automatically available as a Markdown file simply by adding `.md` to the end of the URL.
For example, the [Building with AI](/platform-overview/building-with-ai) page is available as [`/platform-overview/building-with-ai.md`](/platform-overview/building-with-ai.md).
You can also copy any page's content as Markdown with the keyboard shortcut Command + C (Ctrl + C on Windows) and paste it directly into ChatGPT, Claude, or any AI assistant for context-aware help with your specific question.
## Install the Grid API agent skill
The Grid API skill gives [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or another Skill-compatible agent full access to the Grid API via a built-in CLI. Install it with:
```bash theme={null}
npx skills add lightsparkdev/grid-api
```
Make sure to install it for whichever agent you're using. For example, if you're using Claude Code,
you'll need to explicitly select Claude Code in the agent installation selection screen.
Once installed, you can start asking questions immediately. To execute API operations, you'll need to
configure your credentials.
### Configure your credentials
To set your grid credentials, simply ask the agent to help you configure them:
```
Help me configure my Grid API credentials.
```
It'll prompt you for your API Token ID and Client Secret, validate them, and save to `~/.grid-credentials` for future use.
Start in the sandbox environment to experiment safely. The Skill is great at generating fake account data to help you test different flows.
## Example prompts
Try these prompts in Claude Code or paste them into your AI assistant of choice:
### Getting started
```
What currencies does Grid support? Show me the coverage by country.
```
```
Walk me through the steps to send a payout from USD to MXN via CLABE.
```
### Payouts
```
Create an external account for a bank in Mexico using CLABE, then create
a quote to send $500 USD and show me the exchange rate before executing.
```
```
Send $100 to $alice@example.com via UMA. Show me the exchange rate first.
```
### On/off-ramps
```
Help me set up a fiat-to-crypto on-ramp. I want to convert USD to BTC
using a Spark wallet.
```
```
I need to off-ramp USDC to a US bank account. Walk me through the full flow.
```
### Account management
```
List all my customers and their KYC status in a table.
```
```
Create a new individual customer and generate a KYC onboarding link.
```
### Debugging
```
I'm getting QUOTE_EXPIRED errors. What's happening and how do I fix it?
```
```
My external account creation is failing for a Nigerian bank account.
What fields am I missing?
```
## Tips for best results
1. **Be specific about your use case** — "Send a payout to Brazil via PIX" gets better results than "help me send money"
2. **Start with the sandbox** — Ask the AI to use sandbox mode so you can experiment without real funds
3. **Give context** — Paste the relevant docs page or point the AI to `/llms-full.txt` for full API context
4. **Iterate on errors** — If an API call fails, paste the error and ask the AI to diagnose it
# Capabilities
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/capabilities
Grid exposes low-level primitives that you combine to build any payment flow. Fiat, crypto, or both.
Push funds to bank accounts in 65+ countries, crypto wallets, or UMA addresses. You choose the destination, Grid handles delivery.
Accept incoming payments via bank transfer, crypto deposit, or UMA. Webhooks notify you in real-time.
Exchange between fiat currencies or convert fiat to crypto. Lock in rates before execution.
Maintain balances in USD, EUR, BRL, BTC, USDC, and more. Fund and manage accounts via API.
On-ramp from bank accounts to BTC or stablecoins. Off-ramp crypto to local bank rails instantly.
Generate payment requests and collect funds. Track payment status through completion.
Automate flows with webhooks, approval logic, and idempotent operations. Build any payment experience.
KYC/KYB verification—use Grid's hosted flows or bring your own compliance.
# Configuration
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/configuration
Configure your Grid integration
Learn how to configure Grid for your specific use case.
## Platform Configuration
Before you can start using Grid, you'll need to configure your platform settings. This includes:
* Setting up your API credentials
* Configuring webhooks for real-time updates
* Defining your supported currencies and payment rails
* Setting up compliance and KYC requirements
## Environment Setup
Grid provides two environments:
Test your integration with simulated payments
Process real payments with live credentials
## Next Steps
Learn how to authenticate API requests
Set up real-time event notifications
# Account Model
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/core-concepts/account-model
Internal accounts, external accounts, and how they work together
Grid uses two types of accounts: **internal accounts** (Grid-managed balances) and **external accounts** (connected bank accounts and wallets). Understanding when to use each type is key to building efficient payment flows.
## Internal Accounts
Internal accounts are Lightspark managed accounts that hold funds within the Grid platform. They allow you to receive deposits and send payments to external bank accounts or other payment destinations.
They are useful for holding funds on behalf or the platform or customers which will be used for instant, 24/7 quotes and transfers out of the system.
Internal accounts are created for both:
* **Platform-level accounts**: Hold pooled funds for your platform operations (rewards distribution, reconciliation, etc.)
* **Customer accounts**: Hold individual customer funds for their transactions
Internal accounts are automatically created when you onboard a customer, based
on your platform's currency configuration. Platform-level internal accounts
are created when you configure your platform with supported currencies.
## How internal accounts work
Internal accounts act as an intermediary holding account in the payment flow:
1. **Deposit funds**: You or your customers deposit money into internal accounts using bank transfers (ACH, wire, PIX, etc.) or crypto transfers
2. **Hold balance**: Funds are held securely in the internal account until needed
3. **Send payments**: You initiate transfers from internal accounts to external destinations
Each internal account:
* Is denominated in a single currency (USD, EUR, etc.)
* Has a unique balance that you can query at any time
* Includes unique payment instructions for depositing funds
* Supports multiple funding methods depending on the currency
## Retrieving internal accounts
### List customer internal accounts
To retrieve all internal accounts for a specific customer, use the customer ID to filter the results:
```bash Request internal accounts for a customer theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json theme={null}
{
"data": [
{
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"fundingPaymentInstructions": [
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"reference": "FUND-ABC123",
"accountType": "US_ACCOUNT",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
},
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
],
"createdAt": "2025-10-03T12:00:00Z",
"updatedAt": "2025-10-03T14:30:00Z"
}
],
"hasMore": false,
"totalCount": 1
}
```
### Filter by currency
You can filter internal accounts by currency to find accounts for specific denominations:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001¤cy=USD' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
### List platform internal accounts
To retrieve platform-level internal accounts (not tied to individual customers), use the platform internal accounts endpoint:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Platform internal accounts are useful for managing pooled funds, distributing
rewards, or handling platform-level operations.
## Understanding funding payment instructions
Each internal account includes `fundingPaymentInstructions` that tell your customers how to deposit funds. The structure varies by payment rail and currency:
For USD accounts, instructions include routing and account numbers:
```json theme={null}
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"accountType": "US_ACCOUNT",
"reference": "FUND-ABC123",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
}
```
Each internal account has unique banking details in the `accountOrWalletInfo`
field, which ensures deposits are automatically credited to the correct
account.
For EUR accounts, instructions use SEPA IBAN numbers:
```json theme={null}
{
"instructionsNotes": "Include reference in SEPA transfer description",
"accountOrWalletInfo": {
"accountType": "IBAN",
"reference": "FUND-EUR789",
"iban": "DE89370400440532013000",
"swiftBic": "DEUTDEFF",
"accountHolderName": "Lightspark Payments FBO Maria Garcia",
"bankName": "Banco de México"
}
}
```
For stablecoin accounts, using a Spark wallet as the funding source:
```json theme={null}
{
"invoice": "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs",
"instructionsNotes": "Use the invoice when making Spark payment",
"accountOrWalletInfo": {
"accountType": "SPARK_WALLET",
"assetType": "USDB",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
```
For Solana wallet accounts, using a Solana wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
```
For Tron wallet accounts, using a Tron wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "TRON_WALLET",
"assetType": "USDT",
"address": "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL"
}
}
```
For Polygon wallet accounts, using a Polygon wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "POLYGON_WALLET",
"assetType": "USDC",
"address": "0xAbCDEF1234567890aBCdEf1234567890ABcDef12"
}
}
```
For Base wallet accounts, using a Base wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "BASE_WALLET",
"assetType": "USDC",
"address": "0xAbCDEF1234567890aBCdEf1234567890ABcDef12"
}
}
```
## Checking account balances
The internal account balance reflects all deposits and withdrawals. The balance includes:
* **amount**: The balance amount in the smallest currency unit (cents for USD, centavos for MXN/BRL, etc.)
* **currency**: Full currency details including code, name, symbol, and decimal places
### Example balance check
```bash Fetch the balance of an internal account theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts/InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json theme={null}
{
"data": {
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
}
}
}
```
Always check the `decimals` field in the currency object to correctly convert
between display amounts and API amounts. For example, USD has 2 decimals, so
an amount of 50000 represents \$500.00.
## Displaying funding instructions to customers
When customers need to deposit funds themselves, display the funding payment instructions in your application:
Fetch the customer's internal account for their desired currency using the API.
Parse the `fundingPaymentInstructions` array and select the appropriate instructions based on your customer's preferred payment method.
```javascript theme={null}
const instructions = account.fundingPaymentInstructions[0];
const bankInfo = instructions.accountOrWalletInfo;
```
Show the payment details prominently in your UI:
* Account holder name
* Bank name and routing information (account/routing number, CLABE, PIX key, etc.)
* Reference code (if provided)
* Any additional notes from `instructionsNotes`
The unique banking details in each internal account automatically route
deposits to the correct destination.
Set up webhook listeners to receive notifications when deposits are credited to the internal account. The account balance will update automatically.
You'll receive `ACCOUNT_STATUS` webhook events when the internal account balance changes.
## Best practices
Ensure your customers have all the information needed to make deposits. Consider implementing:
* Clear display of all banking details from `fundingPaymentInstructions`
* Copy-to-clipboard functionality for account numbers and reference codes
* Email/SMS confirmations with complete deposit instructions
Set up monitoring to alert customers when their balance is low:
```javascript theme={null}
if (account.balance.amount < minimumThreshold) {
await notifyCustomer({
type: 'LOW_BALANCE',
account: account.id,
instructions: account.fundingPaymentInstructions
});
}
```
If your platform supports multiple currencies, organize internal accounts by currency in your UI:
```javascript theme={null}
const accountsByCurrency = accounts.data.reduce((acc, account) => {
const code = account.balance.currency.code;
acc[code] = account;
return acc;
}, {});
// Quick lookup: accountsByCurrency['USD']
```
Internal account details (especially funding instructions) rarely change, so you can cache them safely. However, always fetch fresh balance data before initiating transfers.
## External Accounts
**External accounts** represent bank accounts, crypto wallets, or other payment instruments outside of Grid for on-ramping or off-ramping funds.
### Characteristics
* Represent real-world accounts (bank accounts, crypto wallets)
* Used as funding sources or payout destinations
* Subject to verification and compliance screening
* Have associated beneficiary information
* Status indicates readiness for use
### Supported Account Types
```json theme={null}
{
"accountType": "US_ACCOUNT",
"currency": "USD",
"accountNumber": "1234567890",
"routingNumber": "110000000",
"accountCategory": "CHECKING",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Alice Johnson",
"address": {
"line1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
```
```json theme={null}
{
"accountType": "IBAN",
"currency": "EUR",
"iban": "DE89370400440532013000",
"swiftBic": "DEUTDEFF",
"bankName": "Deutsche Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Hans Schmidt"
}
}
```
```json theme={null}
{
"accountType": "PIX",
"currency": "BRL",
"pixKey": "user@example.com",
"pixKeyType": "EMAIL",
"bankName": "Nubank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Maria Silva",
"taxId": "12345678900"
}
}
```
```json theme={null}
{
"accountType": "CLABE",
"clabeNumber": "012345678901234567",
"bankName": "BBVA Mexico",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Carlos Rodriguez"
}
}
```
```json theme={null}
{
"accountType": "UPI",
"currency": "INR",
"vpa": "user@paytm",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Priya Sharma"
}
}
```
```json theme={null}
{
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu",
"currency": "BTC"
}
```
### Creating External Accounts
```bash Creating a US bank account theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/customers/external-accounts \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "USD",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "9876543210",
"routingNumber": "110000000",
"accountCategory": "CHECKING",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Alice Johnson",
"address": {
"line1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}'
```
**Response:**
```json theme={null}
{
"id": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "USD",
"status": "PENDING",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "****3210",
"routingNumber": "110000000"
}
}
```
### External Account Status
External accounts progress through verification:
```
PENDING → ACTIVE
```
* **PENDING**: Created, undergoing verification/screening
* **ACTIVE**: Verified, ready for transactions
* **INACTIVE**: Disabled (can be reactivated)
* **REJECTED**: Failed verification
You'll receive `ACCOUNT_STATUS` webhooks as status changes.
### Using External Accounts
Send funds from internal account to external account:
```bash theme={null}
POST /transfer-out
{
"source": {"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"},
"destination": {"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123"},
"amount": 100000
}
```
Or via quote for cross-currency:
```bash theme={null}
POST /quotes
{
"source": {"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"},
"destination": {"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123"},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100000
}
```
Pull funds from external account to internal account:
```bash theme={null}
POST /transfer-in
{
"source": {"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123"},
"destination": {"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"},
"amount": 100000
}
```
Only works for "pullable" external accounts (e.g., linked via Plaid, debit cards).
***
## Account Combinations
Common patterns for combining internal and external accounts:
### Pattern 1: Prefunded Payouts
**Use case:** Send payouts from customer's prefunded balance
```
Customer Internal Account (USD) → External Bank Account (EUR)
```
1. Customer funds internal account via ACH
2. Create quote: Internal USD → External EUR
3. Execute quote
4. Recipient receives EUR in their bank
### Pattern 2: JIT Funded Payouts
**Use case:** On-demand payments without maintaining balance
```
Customer → Payment Instructions → Internal Account → External Account
```
1. Create quote with `source: {customerId}`
2. Grid provides payment instructions
3. Customer sends funds to instructions
4. Quote auto-executes when received
5. Recipient receives payout
### Pattern 3: Platform Rewards
**Use case:** Platform distributes Bitcoin rewards
```
Platform Internal Account (USD) → Customer External Wallet (BTC)
```
1. Platform funds its own internal USD account
2. Create quote: Platform Internal USD → Customer Spark Wallet
3. Execute with `immediatelyExecute: true`
4. Customer receives BTC instantly
### Pattern 4: Crypto On-Ramp
**Use case:** Customer buys Bitcoin
```
Customer External Bank → Customer Internal Account (USD) → Customer External Wallet (BTC)
```
1. Customer links bank via Plaid
2. Pull funds to internal USD account
3. Create quote: Internal USD → External Spark Wallet
4. Execute quote
5. BTC delivered to customer's wallet
# Currencies & Payment Rails
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/core-concepts/currencies-and-rails
Supported currencies, countries, and payment methods
Grid supports a wide range of fiat currencies and cryptocurrencies, with automatic routing across optimal payment rails based on currency, destination, and speed requirements.
## Supported Currencies
Bitcoin (BTC) and Stablecoin transactions supported worldwide with no geographic restrictions.
Onboard as a platform with complete access to APIs, hosted KYC/KYB, dashboard, and business integrations.
Send payments to 65 countries on local banking rails, in addition to BTC and stablecoins.
Receive payments via local rails like SEPA, PIX, UPI, and more.
| Country | ISO Code | Payment Rails |
| ------------------- | -------- | -----------------------------------: |
| 🇦🇹 Austria | AT | `SEPA` `SEPA Instant` |
| 🇧🇪 Belgium | BE | `SEPA` `SEPA Instant` |
| 🇧🇯 Benin | BJ | `Bank Transfer` |
| 🇧🇼 Botswana | BW | `Bank Transfer` |
| 🇧🇷 Brazil | BR | `PIX` |
| 🇧🇬 Bulgaria | BG | `SEPA` `SEPA Instant` |
| 🇧🇫 Burkina Faso | BF | `Bank Transfer` |
| 🇨🇲 Cameroon | CM | `Bank Transfer` |
| 🇨🇦 Canada | CA | `Bank Transfer` |
| 🇨🇳 China | CN | `Bank Transfer` |
| 🇨🇷 Costa Rica | CR | `Bank Transfer` |
| 🇭🇷 Croatia | HR | `SEPA` `SEPA Instant` |
| 🇨🇾 Cyprus | CY | `SEPA` `SEPA Instant` |
| 🇨🇿 Czech Republic | CZ | `SEPA` `SEPA Instant` |
| 🇩🇰 Denmark | DK | `SEPA` `SEPA Instant` |
| 🇨🇩 DR Congo | CD | `Bank Transfer` |
| 🇪🇪 Estonia | EE | `SEPA` `SEPA Instant` |
| 🇫🇮 Finland | FI | `SEPA` `SEPA Instant` |
| 🇫🇷 France | FR | `SEPA` `SEPA Instant` |
| 🇩🇪 Germany | DE | `SEPA` `SEPA Instant` |
| 🇬🇭 Ghana | GH | `Bank Transfer` |
| 🇬🇷 Greece | GR | `SEPA` `SEPA Instant` |
| 🇭🇰 Hong Kong | HK | `Bank Transfer` |
| 🇭🇺 Hungary | HU | `SEPA` `SEPA Instant` |
| 🇮🇸 Iceland | IS | `SEPA` `SEPA Instant` |
| 🇮🇳 India | IN | `UPI` `IMPS` |
| 🇮🇩 Indonesia | ID | `Bank Transfer` |
| 🇮🇪 Ireland | IE | `SEPA` `SEPA Instant` |
| 🇮🇹 Italy | IT | `SEPA` `SEPA Instant` |
| 🇨🇮 Ivory Coast | CI | `Bank Transfer` |
| 🇰🇪 Kenya | KE | `Bank Transfer` |
| 🇱🇻 Latvia | LV | `SEPA` `SEPA Instant` |
| 🇱🇮 Liechtenstein | LI | `SEPA` `SEPA Instant` |
| 🇱🇹 Lithuania | LT | `SEPA` `SEPA Instant` |
| 🇱🇺 Luxembourg | LU | `SEPA` `SEPA Instant` |
| 🇲🇼 Malawi | MW | `Bank Transfer` |
| 🇲🇾 Malaysia | MY | `Bank Transfer` |
| 🇲🇱 Mali | ML | `Bank Transfer` |
| 🇲🇹 Malta | MT | `SEPA` `SEPA Instant` |
| 🇲🇽 Mexico | MX | `SPEI` |
| 🇳🇱 Netherlands | NL | `SEPA` `SEPA Instant` |
| 🇳🇬 Nigeria | NG | `Bank Transfer` |
| 🇳🇴 Norway | NO | `SEPA` `SEPA Instant` |
| 🇵🇭 Philippines | PH | `Bank Transfer` |
| 🇵🇱 Poland | PL | `SEPA` `SEPA Instant` |
| 🇵🇹 Portugal | PT | `SEPA` `SEPA Instant` |
| 🇷🇴 Romania | RO | `SEPA` `SEPA Instant` |
| 🇸🇳 Senegal | SN | `Bank Transfer` |
| 🇸🇬 Singapore | SG | `PayNow` `FAST` `Bank Transfer` |
| 🇸🇰 Slovakia | SK | `SEPA` `SEPA Instant` |
| 🇸🇮 Slovenia | SI | `SEPA` `SEPA Instant` |
| 🇿🇦 South Africa | ZA | `Bank Transfer` |
| 🇰🇷 South Korea | KR | `Bank Transfer` |
| 🇪🇸 Spain | ES | `SEPA` `SEPA Instant` |
| 🇱🇰 Sri Lanka | LK | `Bank Transfer` |
| 🇸🇪 Sweden | SE | `SEPA` `SEPA Instant` |
| 🇨🇭 Switzerland | CH | `SEPA` `SEPA Instant` |
| 🇹🇿 Tanzania | TZ | `Bank Transfer` |
| 🇹🇭 Thailand | TH | `Bank Transfer` |
| 🇹🇬 Togo | TG | `Bank Transfer` |
| 🇺🇬 Uganda | UG | `Bank Transfer` |
| 🇬🇧 United Kingdom | GB | `Faster Payments` `Bank Transfer` |
| 🇺🇸 United States | US | `ACH` `Wire Transfer` `RTP` `FedNow` |
| 🇻🇳 Vietnam | VN | `Bank Transfer` |
| 🇿🇲 Zambia | ZM | `Bank Transfer` |
Regional Summary
Primary: SEPA/SEPA Instant
Primary: Bank Transfer
Various instant payment systems
PIX, SPEI, ACH, FedNow
Grid automatically selects the optimal rail based on currency, country, and amount. You don't need to specify the rail in your API requests.
## Currency Conversion
Grid handles currency conversion automatically through the quote system:
### Exchange Rates
* **Real-time rates**: Grid provides live exchange rates based on market conditions
* **Rate locking**: Quotes lock rates for 1-15 minutes depending on payment type
* **Transparency**: Exact rates and fees shown before execution
* **No hidden fees**: What you see in the quote is what you pay
### Example Conversion Flow
```bash theme={null}
POST /quotes
{
"source": {"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"},
"destination": {"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123", "currency": "MXN"},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100000
}
```
**Response:**
```json theme={null}
{
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000020",
"status": "PROCESSING",
"createdAt": "2025-10-03T15:00:00Z",
"expiresAt": "2025-10-03T15:05:00Z",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "MXN"
},
"sendingCurrency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
},
"receivingCurrency": {
"code": "MXN",
"name": "Mexican Peso",
"symbol": "$",
"decimals": 8
},
"totalSendingAmount": 100,
"totalReceivingAmount": 17250,
"exchangeRate": 17.25,
"feesIncluded": 5,
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000025"
}
```
This shows: Send $1,000 → Receive $17,250 (at rate 17.25), \$250 fee.
# Entities & Relationships
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/core-concepts/entities
Understanding Grid's core data model
There are several key entities in the Grid API: **Platform**, **Customers**, **Internal Accounts**, **External Accounts**, **Quotes**, **Transactions**, and **UMA Addresses**.
## Businesses, People, and Accounts
### Platform
Your **platform** is you! It's the top-level entity that integrates with the Grid API. The platform:
* Has its own configuration (webhook endpoint, supported currencies, API tokens, etc.)
* A platform can have many customers both business and individual
* Manages multiple customers and their accounts
* Can hold platform-owned internal accounts for settlement and liquidity management
* Acts as the integration point between your application and the open Money Grid
### Customers
**Customers** are your end users who send and receive payments through your platform. Each customer:
* Can be an individual or business entity
* Has a KYC/KYB status that determines their ability to transact. If you are a regulated financial institution, this will typically be `APPROVED` since you do the KYC/KYB yourself.
* Is identified by both a system-generated ID and optionally your platform-specific customer ID
* May have associated internal accounts and external accounts
* May have a unique **UMA address** (e.g., `$john.doe@yourdomain.com`). If you don't assign an UMA address when creating a customer, they will be assigned a system-generated one.
### Internal Accounts
**Internal accounts** are Grid-managed accounts that hold balances in specific currencies. They can belong to either:
* **Platform internal accounts** - Owned by the platform for settlement, liquidity, and float management
* **Customer internal accounts** - Associated with specific customers for holding funds
Internal accounts:
* Have balances in a single currency (USD, EUR, MXN, etc.)
* Can be funded via bank transfers or crypto deposits using payment instructions
* Are used as sources or destinations for transactions instantly 24/7/365
* Track available balance for sending payments or receiving funds
### External Accounts
**External accounts** are traditional bank accounts, crypto wallets, or other payment instruments connected to customers
for on-ramping or off-ramping funds. Each external account:
* Are associated with a specific customer or the platform
* Represents a real-world bank account (with routing number, account number, IBAN, etc.), wallet, or payment instrument
* Has an associated beneficiary (individual or business) who receives payments from the customer or platform
* Has a status indicating screening status (ACTIVE, PENDING, INACTIVE, etc.)
* Can be used as a destination for quote-based transfers or same currency transfers like withdrawals
* For pullable sources like debit cards or ACH pulls, an external account can be used as a source for transfers-in to
fund internal accounts or to fund cross-border transfers via quotes.
## Entity Examples by Use Case
Understanding how entities map to your specific use case helps clarify your integration architecture. Here are common examples:
### B2B Payouts Platform (e.g., Bill.com, Routable)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------- | ------------------------------------------ |
| **Platform** | The payouts platform itself | Your company providing AP automation |
| **Customer** | Businesses sending payments to vendors | Acme Corp (your client company) |
| **External Account** | Vendors/suppliers receiving payments | Office supply vendor, freelance contractor |
**Flow**: Acme Corp (customer) uses your platform to pay their vendor invoices → funds move from Acme's internal account → to vendor's external bank account
### Direct Rewards Platform (Platform-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | ------------------------------------------- | ----------------------------------- |
| **Platform** | The app paying rewards directly to users | Your cashback app |
| **Customer** | (Not used in this model) | N/A |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Your platform sends micro-payouts directly from platform internal accounts → to users' external crypto wallets at scale. Common for cashback apps where the platform earns affiliate commissions and shares them with users.
### White-Label Rewards Platform (Customer-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------------- | ----------------------------------- |
| **Platform** | The rewards infrastructure provider | Your white-label rewards API |
| **Customer** | Brands or merchants running reward campaigns | Nike, Starbucks |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Nike (customer) funds their internal account → your platform sends rewards on their behalf → to users' external crypto wallets. Common for brand loyalty programs where merchants manage their own reward budgets.
### Remittance/P2P App (e.g., Wise, Remitly)
| Entity Type | Who They Are | Example |
| -------------------- | ----------------------------------- | ---------------------------------------------------------------- |
| **Platform** | The remittance service | Your money transfer app |
| **Customer** | Both sender and recipient of funds | Maria (sender in US), Juan (recipient in Mexico) |
| **External Account** | Bank accounts for funding/receiving | Maria's US bank (funding), Juan's Mexican bank (receiving funds) |
**Flow**: Maria (customer) funds transfer from her external account → to Juan (also a customer) → who receives funds in his external bank account. Alternatively, Maria could send to Juan's UMA address directly.
## Transactions and Addressing Entities
### Quotes
**Quotes** provide locked-in exchange rates and payment instructions for transfers. A quote:
* Specifies a source (internal account, customer ID, or the platform itself) and destination (internal/external account or UMA address)
* Locks an exchange rate for a short period (typically 1-5 minutes) or can be immediately executed with the `immediatelyExecute` flag
* Calculates total fees and amounts for currency conversion
* Provides payment instructions for funding the transfer if needed, or can be funded via an internal account balance.
* Must be executed before it expires
* Creates a transaction when executed
### Transactions
**Transactions** represent completed or in-progress payment transfers. Each transaction:
* Has a type (INCOMING or OUTGOING from the platform's perspective)
* Has a status (PENDING, COMPLETED, FAILED, etc.)
* References a customer (sender for outgoing, recipient for incoming) or a platform internal account
* Specifies source and destination (accounts or UMA addresses)
* Includes amounts, currencies, and settlement information
* May include counterparty information for compliance purposes if required by your platform configuration
Transactions are created when:
* A quote is executed (either incoming or outgoing)
* A same currency transfer is initiated (transfer-in or transfer-out)
### UMA Addresses (optional)
**UMA addresses** are human-readable payment identifiers that follow the format `$username@domain.com`. They:
* Uniquely identify entities on the Grid network
* Enable sending and receiving payments across different platforms without knowing the recipient's underlying account details or personal information
* Support currency negotiation and cross-border transfers
* Work similar to email addresses but for payments
* Are an optional UX improvement for some use cases. Use of UMA addresses is not required in order to use the Grid API.
## Platform Configuration
Your **platform configuration** defines global settings for your Grid integration:
* **Webhook endpoint**: URL where Grid sends event notifications
* **Supported currencies**: Which currencies your platform offers
* **UMA domain** (optional): Your domain for UMA addresses (e.g., `yourplatform.com`)
* **Required counterparty fields** (UMA only): Information required from senders for compliance
Platform configuration is managed via the dashboard or by contacting Lightspark support.
## ID Format
All Grid entities use a consistent ID format:
```
EntityType:UUID
```
Examples:
* `Customer:019542f5-b3e7-1d02-0000-000000000001`
* `InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965`
* `Transaction:019542f5-b3e7-1d02-0000-000000000030`
* `Quote:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123`
This format makes IDs self-documenting and easier to debug.
## Platform Customer ID
When creating customers, you can provide your own `platformCustomerId`:
```json theme={null}
{
"platformCustomerId": "user_12345_from_my_system",
"customerType": "INDIVIDUAL",
"fullName": "Alice Johnson"
}
```
This allows you to:
* Map Grid customers to your internal user IDs
* Query transactions by your own customer identifiers
* Maintain referential integrity between systems
Grid returns both IDs in all customer-related responses:
```json theme={null}
{
"id": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"platformCustomerId": "user_12345_from_my_system"
}
```
## Amount Representation
All monetary amounts in Grid are represented in the **smallest currency unit**:
**Cents, pence, centavos, etc.**
```json theme={null}
{
"amount": 50000,
"currency": {
"code": "USD",
"symbol": "$",
"decimals": 2
}
}
```
This represents **\$500.00** (50000 cents)
**Satoshis for Bitcoin**
```json theme={null}
{
"amount": 10000000,
"currency": {
"code": "BTC",
"symbol": "₿",
"decimals": 8
}
}
```
This represents **0.1 BTC** (10,000,000 satoshis)
The `decimals` field tells you how many decimal places to use when displaying the amount:
```javascript theme={null}
function formatAmount(amount, currency) {
const value = amount / Math.pow(10, currency.decimals);
return `${currency.symbol}${value.toFixed(currency.decimals)}`;
}
// formatAmount(50000, {code: "USD", symbol: "$", decimals: 2}) → "$500.00"
// formatAmount(10000000, {code: "BTC", symbol: "₿", decimals: 8}) → "₿0.10000000"
```
## Timestamps
All timestamps in Grid use **ISO 8601 format with UTC timezone**:
```json theme={null}
{
"createdAt": "2025-10-03T15:00:00Z",
"settledAt": "2025-10-03T15:30:00Z"
}
```
Key timestamp fields:
* **createdAt**: When the entity was created
* **updatedAt**: Last modification time
* **settledAt**: When a transaction completed (transactions only)
* **expiresAt**: When a quote expires (quotes only)
# Quote System
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/core-concepts/quote-system
How exchange rates, pricing, and payment execution work
Quotes are Grid's mechanism for providing locked-in exchange rates, transparent fee calculations, and payment instructions. Understanding quotes is essential for all cross-currency or crypto-involved transactions.
## What is a Quote?
A **quote** locks in:
* An exchange rate between two currencies
* Total fees for the transaction
* Exact amounts to be sent and received
* Payment instructions (if JIT funding is needed)
* An expiration time (typically 1-5 minutes, up to 15 minutes depending on the payment type)
Quotes ensure that your customers know exactly what they'll pay and what the recipient will receive before committing to a transaction.
## When Do You Need a Quote?
Use quotes for:
* **Cross-currency transfers** (USD → EUR, BRL → MXN)
* **Fiat-to-crypto conversion** (USD → BTC)
* **Crypto-to-fiat conversion** (BTC → USD)
* **UMA payments** (always require quotes)
* **JIT funded payments** (need payment instructions)
These scenarios involve currency conversion, exchange rate risk, or complex routing.
For same-currency transfers, use simpler endpoints:
* `POST /transfer-out` - Send from internal to external account (same currency)
* `POST /transfer-in` - Pull from external to internal account (same currency)
No quote needed because there's no currency conversion.
## Creating a Quote
### Basic Cross-Currency Quote
This is a basic quote for a cross-currency transfer from an internal account to an external account,
which were pre-created as described in the [Account Model](/platform-overview/core-concepts/account-model) section.
```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"
},
"destination": {
"currency": "BTC",
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123"
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100000
}'
```
**Response:**
```json theme={null}
{
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000020",
"status": "PROCESSING",
"createdAt": "2025-10-03T15:00:00Z",
"expiresAt": "2025-10-03T15:05:00Z",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "BTC"
},
"sendingCurrency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
},
"receivingCurrency": {
"code": "BTC",
"name": "Bitcoin",
"symbol": "₿",
"decimals": 8
},
"totalSendingAmount": 100000,
"totalReceivingAmount": 829167,
"exchangeRate": 0.00000833,
"feesIncluded": 500,
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000025"
}
```
This quote says: Send $1,000 USD, recipient receives 0.00829167 BTC, fees are $5.00, expires in 5 minutes.
### Locked Currency Side
You can lock either the **sending** or **receiving** amount:
**Use when:** Customer knows exactly how much they want to send
```json theme={null}
{
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100000
}
```
Grid calculates what the recipient will receive based on current exchange rates.
**Use when:** Recipient must receive an exact amount (e.g., invoice payment)
```json theme={null}
{
"lockedCurrencySide": "RECEIVING",
"lockedCurrencyAmount": 92000
}
```
Grid calculates how much the sender must send to ensure recipient gets exactly €920.
## Funding Models
Grid supports two funding models for quotes:
### Prefunded (From Internal Account)
Source is an existing internal account with available balance:
```json theme={null}
{
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"
}
}
```
* Funds are debited immediately when quote is executed
* No payment instructions needed
* Best for: Customers with pre-loaded balances
### Just-In-Time (JIT) Funding
Source is the customer ID or the platform itself — Grid provides payment instructions:
```json theme={null}
{
"source": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001"
}
}
```
**Quote response includes payment instructions:**
```json theme={null}
{
"quoteId": "Quote:...",
"paymentInstructions": [
{
"instructionsNotes": "Please ensure the reference code is included in the payment memo/description field",
"accountOrWalletInfo": {
"reference": "UMA-Q12345-REF",
"accountType": "US_ACCOUNT",
"accountNumber": "9876543210",
"routingNumber": "110000000",
"accountCategory": "CHECKING",
"bankName": "Chase Bank"
}
},
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
]
}
```
* Customer sends funds to provided account with reference
* Quote executes automatically when Grid receives payment
* Best for: On-demand payments without maintaining balances
## Executing a Quote
For a prefunded quote or one from a pullable external account source, once a quote is created, execute it before it expires:
```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes/Quote:abc123/execute \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json"
```
**Response:**
```json theme={null}
{
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000060",
"status": "PENDING",
"type": "OUTGOING",
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000050"
...
}
```
### Execution Timing
* Transaction created with status `PENDING`
* Funds debited from source account immediately
* Settlement begins right away
* Quote waits for payment receipt
* Once Grid receives payment with correct reference
* Quote executes automatically
* Transaction created and settlement begins
## Immediate Execution
For **market rate execution** without quote approval, use the `immediatelyExecute` flag:
```bash Immediate quote execution theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes \
-u "$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": 100000,
"immediatelyExecute": true
}'
```
```json theme={null}
{
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000020",
"status": "COMPLETED",
"createdAt": "2025-10-03T15:00:00Z",
"expiresAt": "2025-10-03T15:05:00Z",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"currency": "EUR"
},
"sendingCurrency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
},
"receivingCurrency": {
"code": "EUR",
"name": "Euro",
"symbol": "€",
"decimals": 2
},
"totalSendingAmount": 100000,
"totalReceivingAmount": 91540,
"exchangeRate": 0.92,
"feesIncluded": 500,
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000025"
}
```
* Quote is created and executed in one API call
* Best for: Rewards distribution, micro-payments, time-sensitive transfers
Customer doesn't see rate before execution. If you want to lock a quote and confirm fees and exchange rate details before executing the quote, set `immediatelyExecute` to `false` or omit the field.
## Creating External Accounts in Quotes
You can create an external account inline when creating a quote:
```json theme={null}
{
"source": {
"accountId": "InternalAccount:abc123"
},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "EUR",
"accountInfo": {
"accountType": "IBAN_ACCOUNT",
"iban": "DE89370400440532013000",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Alice Smith"
}
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100000
}
```
This is useful for one-time recipients or when you don't want to manage external accounts separately.
## Fees
All fees are transparently displayed in the quote response:
```json theme={null}
{
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000020",
"feesIncluded": 500,
... quote response ...
"rateDetails": {
"counterpartyMultiplier": 1.08,
"counterpartyFixedFee": 10,
"gridApiMultiplier": 0.925,
"gridApiFixedFee": 10,
"gridApiVariableFeeRate": 0.003,
"gridApiVariableFeeAmount": 30
}
}
```
**Rate Details Breakdown:**
* **`counterpartyMultiplier`**: Exchange rate from mSATs to receiving currency (1.08 = 1 mSAT = 1.08 cents EUR)
* **`counterpartyFixedFee`**: Fixed fee charged by counterparty (10 cents EUR)
* **`gridApiMultiplier`**: Exchange rate from sending currency to mSATs including variable fees (0.925 = \$1 USD = 0.925 mSATs)
* **`gridApiFixedFee`**: Fixed fee charged by Grid API (10 cents USD)
* **`gridApiVariableFeeRate`**: Variable fee rate as percentage (0.003 = 0.3%)
* **`gridApiVariableFeeAmount`**: Variable fee amount (30 cents USD for \$1,000 transaction)
Fees are deducted from the sending amount, so:
* **Customer sends**: \$1,000
* **Fees**: \$5.00
* **Amount converted**: \$995.00
* **Recipient receives**: €915.40 (at 0.92 rate)
## Best Practices
Let customers review the exchange rate, fees, and final amounts before committing:
```javascript theme={null}
// ✅ Good: Show quote details, await confirmation
const quote = await createQuote(params);
showQuoteToUser(quote);
if (await userConfirms()) {
await executeQuote(quote.id);
}
// ❌ Bad: Immediate execution without review (unless micro-payments/rewards)
await createQuote({...params, immediatelyExecute: true});
```
Store quote parameters so you can recreate expired quotes:
```javascript theme={null}
const quoteParams = {
source: {accountId: customerAccount},
destination: {accountId: recipientAccount},
lockedCurrencySide: 'SENDING',
lockedCurrencyAmount: amount
};
let quote = await createQuote(quoteParams);
// Later, if expired...
try {
await executeQuote(quote.id);
} catch (error) {
if (error.code === 'QUOTE_EXPIRED') {
quote = await createQuote(quoteParams); // Recreate with fresh rate
await executeQuote(quote.id);
}
}
```
Subscribe to quote-related webhooks:
```javascript theme={null}
app.post('/webhooks/grid', (req, res) => {
const {transaction, type} = req.body;
if (type === 'OUTGOING_PAYMENT' && transaction.quoteId) {
// Quote was executed, transaction created
updateCustomerUI(transaction);
}
res.status(200).send();
});
```
# Transaction Lifecycle
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/core-concepts/transaction-lifecycle
Follow a payment from creation to settlement
Understanding the transaction lifecycle helps you build robust payment flows, handle edge cases, and provide accurate status updates to your customers.
## Transaction States
Transactions progress through several states from creation to completion:
```
PENDING → PROCESSING → COMPLETED
↓
FAILED
```
### Status Descriptions
| Status | Description | Next State | Actions Available |
| -------------- | ------------------------------------------------ | ------------------ | ---------------------- |
| **PENDING** | Transaction created, awaiting processing | PROCESSING, FAILED | Monitor status |
| **PROCESSING** | Payment being routed and settled | COMPLETED, FAILED | Monitor status |
| **COMPLETED** | Successfully delivered to recipient | Terminal | None (final state) |
| **FAILED** | Transaction failed, funds refunded if applicable | Terminal | Create new transaction |
| **REJECTED** | Rejected by recipient or compliance | Terminal | None (final state) |
| **REFUNDED** | Completed transaction later refunded | Terminal | None (final state) |
| **EXPIRED** | Quote or payment expired before execution | Terminal | Create new quote |
**Terminal statuses**: `COMPLETED`, `REJECTED`, `FAILED`, `REFUNDED`, `EXPIRED`
Once a transaction reaches a terminal status, it will not change further.
## Outgoing Transaction Flow
**Your customer/platform sends funds to an external recipient.**
### Step-by-Step
Lock in exchange rate and fees:
```bash theme={null}
POST /quotes
{
"source": {"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"},s
"destination": {"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123", "currency": "EUR"},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100000
}
```
**Response:**
* Quote ID
* Locked exchange rate
* Expiration time (1-5 minutes)
Initiate the payment:
```bash theme={null}
POST /quotes/{quoteId}/execute
```
**Result:**
* Transaction created with status `PENDING`
* Source account debited immediately
* `OUTGOING_PAYMENT` webhook sent
Grid handles:
* Currency conversion (if applicable)
* Routing to appropriate payment rail
* Settlement with destination bank/wallet
**Status**: `PROCESSING`
**Webhook**: `OUTGOING_PAYMENT` with updated status
**Success Path:**
* Funds delivered to recipient
* Status: `COMPLETED`
* `settledAt` timestamp populated
* Final `OUTGOING_PAYMENT` webhook sent
**Failure Path:**
* Delivery failed (invalid account, etc.)
* Status: `FAILED`
* `failureReason` populated
* Funds automatically refunded to source account
* Final `OUTGOING_PAYMENT` webhook sent
Most transactions on Grid are completed in seconds.
### Webhook Payloads
**On Creation (PENDING):**
```json theme={null}
{
"type": "OUTGOING_PAYMENT",
"transaction": {
"id": "Transaction:...",
"status": "PENDING",
"type": "OUTGOING",
"sentAmount": {"amount": 100000, "currency": {"code": "USD"}},
"receivedAmount": {"amount": 92000, "currency": {"code": "EUR"}},
"createdAt": "2025-10-03T15:00:00Z"
}
}
```
**On Completion:**
```json theme={null}
{
"type": "OUTGOING_PAYMENT",
"transaction": {
"id": "Transaction:...",
"status": "COMPLETED",
"settledAt": "2025-10-03T15:05:00Z"
}
}
```
**On Failure:**
```json theme={null}
{
"type": "OUTGOING_PAYMENT",
"transaction": {
"id": "Transaction:...",
"status": "FAILED",
"failureReason": "INVALID_BANK_ACCOUNT"
}
}
```
## Same-Currency Transfers
For same-currency transfers without quotes:
### Transfer-Out (Internal → External)
```bash theme={null}
POST /transfer-out
{
"source": {"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"},
"destination": {"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123", "currency": "USD"},
"amount": 100000
}
```
**Response:**
```json theme={null}
{
"id": "Transaction:...",
"status": "PENDING",
"type": "OUTGOING"
}
```
Follows same lifecycle as quote-based outgoing transactions.
### Transfer-In (External → Internal)
```bash theme={null}
POST /transfer-in
{
"source": {"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123", "currency": "USD"},
"destination": {"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"},
"amount": 100000
}
```
Only works for "pullable" external accounts (Plaid-linked, debit cards, etc.).
## Monitoring Transactions
### Via Webhooks (Recommended)
Subscribe to transaction webhooks for real-time updates:
```javascript theme={null}
app.post('/webhooks/grid', async (req, res) => {
const {transaction, type} = req.body;
if (type === 'OUTGOING_PAYMENT') {
await updateTransactionStatus(transaction.id, transaction.status);
if (transaction.status === 'COMPLETED') {
await notifyCustomer(transaction.customerId, 'Payment delivered!');
} else if (transaction.status === 'FAILED') {
await notifyCustomer(transaction.customerId, `Payment failed: ${transaction.failureReason}`);
}
}
res.status(200).json({received: true});
});
```
### Via Polling (Backup)
Query transaction status periodically:
```bash theme={null}
GET /transactions/{transactionId}
```
**Response:**
```json theme={null}
{
"id": "Transaction:...",
"status": "PROCESSING",
"createdAt": "2025-10-03T15:00:00Z",
"updatedAt": "2025-10-03T15:02:00Z"
}
```
Poll every 5-10 seconds until terminal status reached.
### Listing Transactions
Query all transactions for a customer or date range:
```bash theme={null}
GET /transactions?customerId=Customer:abc123&startDate=2025-10-01T00:00:00Z&limit=50
```
**Response:**
```json theme={null}
{
"data": [
{
"id": "Transaction:...",
"status": "COMPLETED",
"type": "OUTGOING",
"sentAmount": {"amount": 100000, "currency": {"code": "USD"}},
"receivedAmount": {"amount": 92000, "currency": {"code": "EUR"}},
"settledAt": "2025-10-03T15:05:00Z"
}
],
"hasMore": false,
"nextCursor": null
}
```
Use for reconciliation and reporting.
## Failure Handling
### Common Failure Reasons
| Failure Reason | Description | Recovery |
| ------------------------ | ------------------------------ | ------------------------ |
| `QUOTE_EXPIRED` | Quote expired before execution | Create new quote |
| `QUOTE_EXECUTION_FAILED` | Error executing the quote | Create new quote |
| `INSUFFICIENT_BALANCE` | Source account lacks funds | Fund account, retry |
| `TIMEOUT` | Transaction timed out | Retry or contact support |
## Best Practices
Don't rely solely on polling:
```javascript theme={null}
// ✅ Good: Webhook-driven updates
app.post('/webhooks/grid', async (req, res) => {
await handleTransactionUpdate(req.body.transaction);
res.status(200).send();
});
// ❌ Bad: Constant polling
setInterval(() => getTransaction(txId), 5000);
```
Save transaction IDs to your database:
```javascript theme={null}
const transaction = await executeQuote(quoteId);
await db.transactions.insert({
gridTransactionId: transaction.id,
internalPaymentId: paymentId,
status: transaction.status,
createdAt: new Date()
});
```
Use idempotency keys for safe retries:
```javascript theme={null}
const idempotencyKey = `payment-${userId}-${Date.now()}`;
await createQuote({...params, idempotencyKey});
```
Translate technical statuses to user-friendly messages:
```javascript theme={null}
function getUserMessage(status, failureReason) {
if (status === 'PENDING') return 'Payment processing...';
if (status === 'PROCESSING') return 'Payment in progress...';
if (status === 'COMPLETED') return 'Payment delivered!';
if (status === 'FAILED') {
if (failureReason === 'INSUFFICIENT_BALANCE') {
return 'Insufficient funds. Please add money and try again.';
}
return 'Payment failed. Please try again or contact support.';
}
}
```
# FAQ
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/introduction/faq
## Getting Started
Grid is a low-level payment infrastructure API that enables businesses, financial institutions, and developers to send and receive payments globally across fiat currencies, stablecoins, and Bitcoin.
Grid is ideal for:
* Fintech companies building payment products
* Businesses sending international payouts
* Platforms enabling P2P payments
* Companies offering crypto on/off-ramps
* Businesses distributing rewards or incentives
Reach out to [sales@lightspark.com](mailto:sales@lightspark.com) to discuss your use case and get custom pricing.
You'll receive sandbox API credentials to start building and testing your integration.
Once testing is complete, you'll receive production credentials to launch with real transactions.
No. Grid handles all on-chain operations, wallet management, and crypto conversions behind the scenes. You interact with a simple REST API using familiar concepts like customers, accounts, and transactions. Bitcoin and stablecoins are just another payment rail that Grid manages for you automatically.
# What is Grid?
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/introduction/what-is-grid
Grid is a low-level payment infrastructure API that enables modern financial institutions, businesses, or any developer to send, receive, and settle value globally across fiat currencies, stablecoins, and Bitcoin. With a single, simple API, you can build applications that move money instantly across borders without the complexity of orchestrating multiple payment rails or currencies.
## Features
Move money anywhere. Fiat to fiat, crypto to fiat, or the other way around, all through a single API.
Send and receive across 65+ countries using local instant rails or global crypto rails.
Instant, 24/7/365. Powered by Bitcoin and local payment networks.
We handle all the on-chain logic, wallet ops, and conversions so you don't have to.
## What to build
Grid's APIs are intentionally low-level, giving developers, institutions, and businesses full control to build any payment flow. Fiat, crypto, or both.
Whether you're receiving Bitcoin or sending dollars as pesos to a bank account, the APIs stay completely unopinionated; you decide the flow. Each path has its own nuances, and our team can help you design the best one for your use case.
* **Remittances**: Move money across countries in local currencies using the best available rails and FX routes.
* **B2B Payments**: Pay vendors or partners abroad with real-time settlement and transparent fees.
* **Payroll**: Send global salaries or contractor payouts with locked exchange rates.
* **Marketplace Payouts**: Distribute earnings to sellers or service providers worldwide in seconds.
* **Stablecoin Payouts**: Send a stablecoin to users, wallets, or bank
accounts directly. - **Global USD Account**: Let users hold a stable USD
balance anywhere in the world. - **Orchestration**: Convert liquidity between
stablecoins and fiat automatically. - **Treasury**: Hold business reserves in
stablecoins with instant conversion to local currency when needed.
* **Buy & Sell Bitcoin**: Enable users to buy or sell Bitcoin instantly inside
your app. - **Rewards**: Offer instant Bitcoin cashback for actions or
purchases. - **Merchant Settlement**: Accept Bitcoin and auto-convert to fiat
on the fly.
## Next steps
Understand Grid's core data model and how entities relate to each other
Learn how exchange rates, pricing, and payment execution work
Explore internal and external accounts and how they work together
Follow a payment from creation through settlement
View supported currencies, countries, and payment methods
# Use Cases
Source: https://ramps-feat-building-with-ai.mintlify.app/platform-overview/use-cases
Discover what you can build with Grid
## Payouts & B2B
Pay creators and influencers instantly, anywhere they bank
Send salaries and contractor payments globally with locked FX rates
Distribute earnings to sellers and service providers worldwide automatically
Pay international vendors and suppliers in their local currency, settled in seconds
## Ramps
Let users purchase Bitcoin directly inside your app
Convert Bitcoin to fiat and settle to any bank account
On-ramp fiat to fund user wallets with stablecoins or BTC
Off-ramp digital assets to local bank rails in real time
## Rewards
Give users Bitcoin back on purchases or actions
Pay out BTC or fiat when users refer new customers
Build point-based or asset-based loyalty with real redemption value
## Global P2P
Move money across countries on the fastest, lowest-cost rails
Send and receive via UMA addresses across any participating app
# Depositing Funds
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/accounts/depositing-funds
Depositing funds into internal accounts
Grid provides two options to fund an account:
* Prefund
* Just-in-time funding
With prefunding, you'll deposit funds into internal accounts via Wire, PIX, or crypto transfers. You can then use the balances as the source of funds for quotes and transfers.
With just-in-time funding, you'll receive payment instructions as part of the quote. Once funds arrive, the payment to the receiver is automatically initiated.
Just-in-time funding supports instant payment rails only (for example: RTP,
PIX, SEPA Instant).
## Prerequisites
* You have created a customer (for customer-scoped internal accounts)
* You have `GRID_CLIENT_ID` and `GRID_CLIENT_SECRET`
Export your credentials for use with cURL:
```bash theme={null}
export GRID_CLIENT_ID="your_client_id"
export GRID_CLIENT_SECRET="your_client_secret"
```
## Prefunding an account via push payments (Wire, SEPA, PIX, etc.)
When customers need to deposit funds themselves, display the funding payment instructions in your application:
Fetch the customer's internal account for their desired currency using the API.
```bash cURL (Customer accounts) theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```bash cURL (Platform internal accounts) theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Parse the `fundingPaymentInstructions` array and select the appropriate instructions based on your customer's preferred payment method.
```javascript theme={null}
const instructions = account.fundingPaymentInstructions[0];
const bankInfo = instructions.accountOrWalletInfo;
```
Show the payment details prominently in your UI and enable copy / paste:
* Account holder name
* Bank name and routing information (account/routing number, CLABE, PIX key, etc.)
* Reference code (if provided)
* Any additional notes from `instructionsNotes`
Customer initiates a push payment from their bank or wallet to the account/address specified.
Set up webhook listeners to receive updates for the deposit transaction and account balance updates. The account balance will update automatically.
You'll receive `ACCOUNT_STATUS` webhook events when the internal account balance changes.
## Just-in-time funding (payment instructions from a quote)
With just-in-time funding, you request a quote and receive payment instructions (for example, a bank account or instant rail details). When your customer confirms the transaction, you trigger payment from your app.
More details of just-in-time funding can be found in the Sending Payments guides.
# External Accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/accounts/external-accounts
Configure external bank accounts and crypto wallets for ramp destinations
External accounts are bank accounts, cryptocurrency wallets, or payment destinations outside Grid where you can send funds. Grid supports two types:
* **Customer external accounts** - Scoped to individual customers, used for withdrawals and customer-specific payouts
* **Platform external accounts** - Scoped to your platform, used for platform-wide operations like receiving funds from external sources
Customer external accounts often require some basic beneficiary information for compliance.
Platform accounts are managed at the organization level.
## Create external accounts by region or wallet
**ACH, Wire, RTP**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}'
```
Category must be `CHECKING` or `SAVINGS`. Routing number must be 9 digits.
**CLABE/SPEI**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "MXN",
"platformAccountId": "mx_beneficiary_001",
"accountInfo": {
"accountType": "CLABE",
"clabeNumber": "123456789012345678",
"bankName": "BBVA Mexico",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "María García",
"birthDate": "1985-03-15",
"nationality": "MX",
"address": {
"line1": "Av. Reforma 123",
"city": "Ciudad de México",
"state": "CDMX",
"postalCode": "06600",
"country": "MX"
}
}
}
}'
```
**PIX**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "BRL",
"platformAccountId": "br_pix_001",
"accountInfo": {
"accountType": "PIX",
"pixKey": "user@email.com",
"pixKeyType": "EMAIL",
"bankName": "Nubank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "João Silva",
"birthDate": "1988-07-22",
"nationality": "BR",
"address": {
"line1": "Rua das Flores 456",
"city": "São Paulo",
"state": "SP",
"postalCode": "01234-567",
"country": "BR"
}
}
}
}'
```
Key types: `CPF`, `CNPJ`, `EMAIL`, `PHONE`, or `RANDOM`
**IBAN/SEPA**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "EUR",
"platformAccountId": "eu_iban_001",
"accountInfo": {
"accountType": "IBAN",
"iban": "DE89370400440532013000",
"swiftBic": "DEUTDEFF",
"bankName": "Deutsche Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Hans Schmidt",
"birthDate": "1982-11-08",
"nationality": "DE",
"address": {
"line1": "Hauptstraße 789",
"city": "Berlin",
"state": "Berlin",
"postalCode": "10115",
"country": "DE"
}
}
}
}'
```
**UPI**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "INR",
"platformAccountId": "in_upi_001",
"accountInfo": {
"accountType": "UPI",
"vpa": "user@okbank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Priya Sharma",
"birthDate": "1991-05-14",
"nationality": "IN",
"address": {
"line1": "123 MG Road",
"city": "Mumbai",
"state": "Maharashtra",
"postalCode": "400001",
"country": "IN"
}
}
}
}'
```
**Bitcoin Lightning (Spark Wallet)**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "BTC",
"platformAccountId": "btc_spark_001",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}'
```
Spark wallets don't require beneficiary information as they are self-custody wallets.
Use `platformAccountId` to tie your internal id with the external account.
**Sample Response:**
```json theme={null}
{
"id": "ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"status": "ACTIVE",
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}
```
### Business beneficiaries
For business accounts, include business information:
```json theme={null}
{
"currency": "USD",
"platformAccountId": "acme_corp_account",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "987654321",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "BUSINESS",
"businessInfo": {
"legalName": "Acme Corporation, Inc.",
"taxId": "EIN-987654321"
},
"address": {
"line1": "456 Business Ave",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}
}
}
```
## Account status
Beneficiary data may be reviewed for risk and compliance. Only `ACTIVE` accounts can receive payments. Updates to account data may trigger account re-review.
| Status | Description |
| -------------- | ----------------------------------- |
| `PENDING` | Created, awaiting verification |
| `ACTIVE` | Verified and ready for transactions |
| `UNDER_REVIEW` | Additional review required |
| `INACTIVE` | Disabled, cannot be used |
## Listing external accounts
### List customer accounts
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
### List platform accounts
For platform-wide operations, list all platform-level external accounts:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Platform external accounts are used for platform-wide operations like
depositing funds from external sources.
## Best practices
Validate account details before submission:
```javascript theme={null}
// US accounts: 9-digit routing, 4-17 digit account number
if (!/^\d{9}$/.test(routingNumber)) {
throw new Error("Invalid routing number");
}
// CLABE: exactly 18 digits
if (!/^\d{18}$/.test(clabeNumber)) {
throw new Error("Invalid CLABE number");
}
```
Verify status before sending payments:
```javascript theme={null}
if (account.status !== "ACTIVE") {
throw new Error(`Account is ${account.status}, cannot process payment`);
}
```
Never expose full account numbers. Display only masked info:
```javascript theme={null}
function displaySafely(account) {
return {
id: account.id,
bankName: account.accountInfo.bankName,
lastFour: account.accountInfo.accountNumber.slice(-4),
status: account.status,
};
}
```
## Using external accounts for ramps
External accounts serve as destinations for ramp conversions:
### For on-ramps (Fiat → Crypto)
External accounts represent crypto wallet destinations:
* **Spark wallets**: Lightning Network wallets for instant Bitcoin delivery
* **Self-custody**: User-controlled wallets for full ownership
* **No beneficiary required**: Crypto wallets don't need compliance information
Spark wallets are the recommended destination for on-ramps due to instant
settlement and minimal fees.
### For off-ramps (Crypto → Fiat)
External accounts represent bank account destinations:
* **Traditional bank accounts**: ACH, wire, SEPA, CLABE, PIX, UPI, etc.
* **Beneficiary required**: Full compliance information needed for fiat destinations
* **Multiple currencies**: Support for USD, EUR, MXN, BRL, INR, and more
Off-ramp destinations require complete beneficiary information for compliance.
Ensure all required fields are provided.
## Crypto wallet destinations
### Spark wallet addresses
The primary destination type for on-ramps:
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"platformAccountId": "user_wallet_001",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}'
```
**Response:**
```json theme={null}
{
"id": "ExternalAccount:wallet001",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"status": "ACTIVE",
"currency": "BTC",
"platformAccountId": "user_wallet_001",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
},
"createdAt": "2025-10-03T14:00:00Z"
}
```
Spark wallet external accounts are immediately `ACTIVE` and ready for on-ramp
conversions.
### Validate Spark addresses
Before creating external accounts, validate Spark wallet addresses:
```javascript theme={null}
function isValidSparkAddress(address) {
// Spark addresses start with 'spark1' and are 87 characters
return address.startsWith("spark1") && address.length === 87;
}
const walletAddress =
"spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu";
if (!isValidSparkAddress(walletAddress)) {
throw new Error("Invalid Spark wallet address format");
}
// Create external account
await createExternalAccount({
customerId,
currency: "BTC",
accountInfo: {
accountType: "SPARK_WALLET",
address: walletAddress,
},
});
```
Spark addresses are case-insensitive and follow the bech32 format starting
with `spark1`.
## Bank account destinations
For off-ramp flows, create external bank accounts with full beneficiary information:
### Example: US bank account for off-ramp
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "USD",
"platformAccountId": "user_bank_usd_001",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}'
```
## Creating accounts inline with quotes
For one-time conversions, create external accounts inline using `externalAccountDetails`:
```bash theme={null}
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": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "USD"
},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000
}'
```
Use `externalAccountDetails` for one-time destinations. The external account
will be automatically created and can be reused for future quotes using its
returned ID.
## Listing external accounts
### List customer external accounts
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
### Filter by currency
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001¤cy=BTC' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
### Filter by account type
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001&accountType=SPARK_WALLET' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
## Account status and verification
External accounts move through verification states:
| Status | Description | Can Use for Conversions |
| ---------- | ------------------------ | ----------------------- |
| `PENDING` | Verification in progress | ❌ |
| `ACTIVE` | Verified and ready | ✅ |
| `FAILED` | Verification failed | ❌ |
| `DISABLED` | Manually disabled | ❌ |
Spark wallet accounts are immediately `ACTIVE`. Bank accounts may require
verification (typically instant to a few hours).
## Best practices for ramps
Always validate wallet addresses and bank account details before creating external accounts:
```javascript theme={null}
// Validate Spark address format
function validateSparkAddress(address) {
if (!address.startsWith("spark1")) {
throw new Error("Spark address must start with spark1");
}
if (address.length !== 87) {
throw new Error("Spark address must be 87 characters");
}
// Additional validation logic
return true;
}
```
Map external accounts to your internal system using `platformAccountId`:
```javascript theme={null}
const externalAccount = await createExternalAccount({
customerId,
currency: "BTC",
platformAccountId: `${userId}_spark_primary`, // Your internal ID
accountInfo: {
accountType: "SPARK_WALLET",
address: sparkAddress,
},
});
// Store mapping in your database
await db.userWallets.create({
userId,
gridAccountId: externalAccount.id,
internalId: `${userId}_spark_primary`,
});
```
Support multiple wallets or bank accounts for flexibility:
```javascript theme={null}
// Primary Spark wallet for on-ramps
await createExternalAccount({
customerId,
currency: "BTC",
platformAccountId: `${userId}_spark_primary`,
accountInfo: { accountType: "SPARK_WALLET", address: primaryWallet },
});
// Secondary wallet for larger amounts
await createExternalAccount({
customerId,
currency: "BTC",
platformAccountId: `${userId}_spark_savings`,
accountInfo: { accountType: "SPARK_WALLET", address: savingsWallet },
});
```
For crypto destinations, implement additional verification:
```javascript theme={null}
// Verify Spark wallet is reachable (optional)
async function verifySparkWallet(address) {
try {
// Use Lightning Network tools to verify wallet exists
const probe = await lightningClient.probeWallet(address);
return probe.reachable;
} catch (error) {
console.error("Wallet verification failed:", error);
return false;
}
}
// Only create external account after verification
if (await verifySparkWallet(sparkAddress)) {
await createExternalAccount({
/* ... */
});
}
```
## Ramp-specific considerations
### On-ramp destinations
* **Instant delivery**: Spark wallets receive Bitcoin within seconds
* **No KYC required**: Self-custody wallets don't need beneficiary info
* **Reusable addresses**: Store and reuse Spark addresses for multiple conversions
* **No minimum**: Send any amount supported by Lightning Network
### Off-ramp destinations
* **Full compliance**: Bank accounts require complete beneficiary information
* **Verification delays**: Bank account verification may take a few hours
* **Settlement times**: Vary by destination (instant for RTP/PIX, 1-3 days for ACH)
* **Amount limits**: Check minimum and maximum amounts per destination currency
Always verify account details before initiating large off-ramp conversions.
Test with small amounts first.
## Next steps
* [Plaid Integration](/ramps/accounts/plaid) - Connect bank accounts via Plaid
* [Fiat-to-Crypto Conversion](/ramps/conversion-flows/fiat-crypto-conversion) - Build conversion flows
* [Self-Custody Wallets](/ramps/conversion-flows/self-custody-wallets) - Advanced wallet integration
* [Webhooks](/ramps/platform-tools/webhooks) - Handle account status updates
## Related resources
* [Platform Configuration](/ramps/onboarding/platform-configuration) - Configure supported account types
* [Sandbox Testing](/ramps/platform-tools/sandbox-testing) - Test with mock accounts
* [API Reference](/api-reference) - Complete API documentation
# Internal Accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/accounts/internal-accounts
Manage internal accounts for holding fiat and crypto balances for ramp operations
Internal accounts are Lightspark managed accounts that hold funds within the Grid platform. They allow you to receive deposits and send payments to external bank accounts or other payment destinations.
They are useful for holding funds on behalf or the platform or customers which will be used for instant, 24/7 quotes and transfers out of the system.
Internal accounts are created for both:
* **Platform-level accounts**: Hold pooled funds for your platform operations (rewards distribution, reconciliation, etc.)
* **Customer accounts**: Hold individual customer funds for their transactions
Internal accounts are automatically created when you onboard a customer, based
on your platform's currency configuration. Platform-level internal accounts
are created when you configure your platform with supported currencies.
## How internal accounts work
Internal accounts act as an intermediary holding account in the payment flow:
1. **Deposit funds**: You or your customers deposit money into internal accounts using bank transfers (ACH, wire, PIX, etc.) or crypto transfers
2. **Hold balance**: Funds are held securely in the internal account until needed
3. **Send payments**: You initiate transfers from internal accounts to external destinations
Each internal account:
* Is denominated in a single currency (USD, EUR, etc.)
* Has a unique balance that you can query at any time
* Includes unique payment instructions for depositing funds
* Supports multiple funding methods depending on the currency
## Retrieving internal accounts
### List customer internal accounts
To retrieve all internal accounts for a specific customer, use the customer ID to filter the results:
```bash Request internal accounts for a customer theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json theme={null}
{
"data": [
{
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"fundingPaymentInstructions": [
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"reference": "FUND-ABC123",
"accountType": "US_ACCOUNT",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
},
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
],
"createdAt": "2025-10-03T12:00:00Z",
"updatedAt": "2025-10-03T14:30:00Z"
}
],
"hasMore": false,
"totalCount": 1
}
```
### Filter by currency
You can filter internal accounts by currency to find accounts for specific denominations:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001¤cy=USD' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
### List platform internal accounts
To retrieve platform-level internal accounts (not tied to individual customers), use the platform internal accounts endpoint:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Platform internal accounts are useful for managing pooled funds, distributing
rewards, or handling platform-level operations.
## Understanding funding payment instructions
Each internal account includes `fundingPaymentInstructions` that tell your customers how to deposit funds. The structure varies by payment rail and currency:
For USD accounts, instructions include routing and account numbers:
```json theme={null}
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"accountType": "US_ACCOUNT",
"reference": "FUND-ABC123",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
}
```
Each internal account has unique banking details in the `accountOrWalletInfo`
field, which ensures deposits are automatically credited to the correct
account.
For EUR accounts, instructions use SEPA IBAN numbers:
```json theme={null}
{
"instructionsNotes": "Include reference in SEPA transfer description",
"accountOrWalletInfo": {
"accountType": "IBAN",
"reference": "FUND-EUR789",
"iban": "DE89370400440532013000",
"swiftBic": "DEUTDEFF",
"accountHolderName": "Lightspark Payments FBO Maria Garcia",
"bankName": "Banco de México"
}
}
```
For stablecoin accounts, using a Spark wallet as the funding source:
```json theme={null}
{
"invoice": "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs",
"instructionsNotes": "Use the invoice when making Spark payment",
"accountOrWalletInfo": {
"accountType": "SPARK_WALLET",
"assetType": "USDB",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
```
For Solana wallet accounts, using a Solana wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
```
For Tron wallet accounts, using a Tron wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "TRON_WALLET",
"assetType": "USDT",
"address": "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL"
}
}
```
For Polygon wallet accounts, using a Polygon wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "POLYGON_WALLET",
"assetType": "USDC",
"address": "0xAbCDEF1234567890aBCdEf1234567890ABcDef12"
}
}
```
For Base wallet accounts, using a Base wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "BASE_WALLET",
"assetType": "USDC",
"address": "0xAbCDEF1234567890aBCdEf1234567890ABcDef12"
}
}
```
## Checking account balances
The internal account balance reflects all deposits and withdrawals. The balance includes:
* **amount**: The balance amount in the smallest currency unit (cents for USD, centavos for MXN/BRL, etc.)
* **currency**: Full currency details including code, name, symbol, and decimal places
### Example balance check
```bash Fetch the balance of an internal account theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts/InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json theme={null}
{
"data": {
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
}
}
}
```
Always check the `decimals` field in the currency object to correctly convert
between display amounts and API amounts. For example, USD has 2 decimals, so
an amount of 50000 represents \$500.00.
## Displaying funding instructions to customers
When customers need to deposit funds themselves, display the funding payment instructions in your application:
Fetch the customer's internal account for their desired currency using the API.
Parse the `fundingPaymentInstructions` array and select the appropriate instructions based on your customer's preferred payment method.
```javascript theme={null}
const instructions = account.fundingPaymentInstructions[0];
const bankInfo = instructions.accountOrWalletInfo;
```
Show the payment details prominently in your UI:
* Account holder name
* Bank name and routing information (account/routing number, CLABE, PIX key, etc.)
* Reference code (if provided)
* Any additional notes from `instructionsNotes`
The unique banking details in each internal account automatically route
deposits to the correct destination.
Set up webhook listeners to receive notifications when deposits are credited to the internal account. The account balance will update automatically.
You'll receive `ACCOUNT_STATUS` webhook events when the internal account balance changes.
## Best practices
Ensure your customers have all the information needed to make deposits. Consider implementing:
* Clear display of all banking details from `fundingPaymentInstructions`
* Copy-to-clipboard functionality for account numbers and reference codes
* Email/SMS confirmations with complete deposit instructions
Set up monitoring to alert customers when their balance is low:
```javascript theme={null}
if (account.balance.amount < minimumThreshold) {
await notifyCustomer({
type: 'LOW_BALANCE',
account: account.id,
instructions: account.fundingPaymentInstructions
});
}
```
If your platform supports multiple currencies, organize internal accounts by currency in your UI:
```javascript theme={null}
const accountsByCurrency = accounts.data.reduce((acc, account) => {
const code = account.balance.currency.code;
acc[code] = account;
return acc;
}, {});
// Quick lookup: accountsByCurrency['USD']
```
Internal account details (especially funding instructions) rarely change, so you can cache them safely. However, always fetch fresh balance data before initiating transfers.
## Using internal accounts for ramps
Internal accounts play a critical role in both on-ramp and off-ramp flows:
### For on-ramps (Fiat → Crypto)
Internal accounts are optional for on-ramp flows using just-in-time (JIT) funding:
* **JIT funding**: Quotes provide payment instructions directly; funds flow through without requiring an internal account
* **Pre-funded model**: Deposit fiat to internal account first, then create and execute conversion quotes
Most on-ramp implementations use JIT funding to avoid holding customer
balances and simplify compliance.
### For off-ramps (Crypto → Fiat)
Internal accounts are essential for off-ramp flows:
1. **Deposit crypto**: Transfer Bitcoin or stablecoins to the internal account
2. **Hold balance**: Crypto balance is held securely until conversion
3. **Execute conversion**: Create and execute quote to convert crypto to fiat and send to bank account
Off-ramps require pre-funded internal accounts with crypto balances. Grid
supports Fiat, BTC, and Stablecoin deposits.
## Crypto funding for internal accounts
For off-ramp operations, fund internal accounts with cryptocurrency:
### Bitcoin (Lightning Network)
Internal accounts can receive Bitcoin via Lightning Network:
```json theme={null}
{
"fundingPaymentInstructions": [
{
"accountType": "SPARK_WALLET",
"assetType": "BTC",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu",
"invoice": "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3s..."
}
]
}
```
**Deposit methods:**
* **Spark address**: Reusable address for multiple deposits
* **Lightning invoice**: Single-use invoice for specific amounts
Lightning Network transfers are typically instant and have minimal fees,
making them ideal for crypto deposits.
### Stablecoins
Internal accounts can also receive stablecoins via Lightning Network or Spark:
```json theme={null}
{
"fundingPaymentInstructions": [
{
"accountType": "SPARK_WALLET",
"assetType": "USDB",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
]
}
```
Stablecoins like USDB are 1:1 pegged to USD, offering price stability for
off-ramp balances while maintaining instant settlement via Lightning Network.
## Multi-currency balances
Internal accounts support both fiat and crypto balances:
### Example: Account with USD and BTC
```json theme={null}
{
"data": [
{
"id": "InternalAccount:usd123",
"customerId": "Customer:cust001",
"balance": {
"amount": 100000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
}
},
{
"id": "InternalAccount:btc456",
"customerId": "Customer:cust001",
"balance": {
"amount": 10000000,
"currency": {
"code": "BTC",
"name": "Bitcoin",
"symbol": "₿",
"decimals": 8
}
}
}
]
}
```
Always check the `decimals` field when working with balances. USD uses 2
decimals (cents), while BTC uses 8 decimals (satoshis).
## Monitoring account balance changes
Subscribe to `ACCOUNT_STATUS` webhooks to receive real-time balance updates:
```json theme={null}
{
"accountId": "InternalAccount:btc456",
"oldBalance": {
"amount": 5000000,
"currency": { "code": "BTC", "decimals": 8 }
},
"newBalance": {
"amount": 10000000,
"currency": { "code": "BTC", "decimals": 8 }
},
"timestamp": "2025-10-03T14:32:00Z",
"webhookId": "Webhook:webhook001",
"type": "ACCOUNT_STATUS"
}
```
Balance updates are triggered by: - Incoming deposits (fiat or crypto) - Quote
executions (funds debited) - Failed transaction reversals (funds credited
back)
## Ramp-specific use cases
### Pre-funding for off-ramps
Maintain crypto balances for instant fiat conversions:
```javascript theme={null}
// Check Bitcoin balance before off-ramp
const accounts = await getInternalAccounts(customerId);
const btcAccount = accounts.data.find(
(acc) => acc.balance.currency.code === "BTC"
);
if (btcAccount.balance.amount >= requiredSats) {
// Create quote to convert BTC to fiat
const quote = await createQuote({
source: { accountId: btcAccount.id },
destination: { accountId: bankAccountId, currency: "USD" },
lockedCurrencySide: "SENDING",
lockedCurrencyAmount: requiredSats,
});
// Execute conversion
await executeQuote(quote.id);
}
```
### Platform treasury management
Use platform-level internal accounts for pooled liquidity:
```bash theme={null}
# Get platform internal accounts
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts?currency=BTC' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Platform internal accounts enable centralized treasury management for
high-volume ramp operations.
## Best practices for ramps
* **On-ramps**: Use JIT funding to avoid holding customer fiat
* **Off-ramps**: Pre-fund with crypto for instant fiat conversions
* **High volume**: Consider platform-level accounts for pooled liquidity
```javascript theme={null}
// Track BTC value in USD
const btcBalance = account.balance.amount; // satoshis
const currentRate = await getBtcUsdRate();
const usdValue = (btcBalance / 100000000) * currentRate;
if (usdValue > maxExposure) {
// Convert excess BTC to stablecoin
await convertToStablecoin(btcBalance - targetBalance);
}
```
Set up proactive notifications for low balances:
```javascript theme={null}
// Alert when off-ramp liquidity is low
if (btcAccount.balance.amount < minimumSats) {
await alertTreasury({
type: 'LOW_CRYPTO_BALANCE',
account: btcAccount.id,
balance: btcAccount.balance.amount,
minimumRequired: minimumSats
});
}
```
## Next steps
* [External Accounts](/ramps/accounts/external-accounts) - Configure destination accounts for conversions
* [Plaid Integration](/ramps/accounts/plaid) - Connect bank accounts via Plaid
* [Fiat-to-Crypto Conversion](/ramps/conversion-flows/fiat-crypto-conversion) - Build conversion flows
* [Webhooks](/ramps/platform-tools/webhooks) - Handle real-time notifications
## Related resources
* [Platform Configuration](/ramps/onboarding/platform-configuration) - Configure supported currencies
* [Sandbox Testing](/ramps/platform-tools/sandbox-testing) - Test funding and conversions safely
* [API Reference](/api-reference) - Complete API documentation
# External Accounts with Plaid
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/accounts/plaid
Connect bank accounts securely via Plaid for seamless off-ramp destinations
Plaid integration allows your customers to securely connect their bank accounts without manually entering account numbers and routing information. Grid handles the complete Plaid Link flow, automatically creating external accounts when customers authenticate their banks.
Plaid integration requires Grid to manage your Plaid configuration. Contact
support to enable Plaid for your platform.
## Overview
The Plaid flow involves collaboration between your platform, Grid, Plaid, and the customer's bank:
1. **Request link token**: Your platform requests a Plaid Link token from Grid for a specific customer
2. **Initialize Plaid Link**: Display Plaid Link UI to your customer using the link token
3. **Customer authenticates**: Customer selects their bank and authenticates using Plaid Link
4. **Exchange tokens**: Plaid returns a public token; your platform sends it to Grid's callback URL
5. **Async processing**: Grid exchanges the public token with Plaid and retrieves account details
6. **External account created**: Grid creates the external account and sends a webhook notification. The external account is available for transfers and payments
## Request a Plaid Link token
To initiate the Plaid flow, request a link token from Grid:
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/plaid/link-tokens' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001"
}'
```
**Response:**
```json theme={null}
{
"linkToken": "link-sandbox-af1a0311-da53-4636-b754-dd15cc058176",
"expiration": "2025-10-05T18:30:00Z",
"callbackUrl": "https://api.lightspark.com/grid/2025-10-13/plaid/callback/link-sandbox-af1a0311-da53-4636-b754-dd15cc058176",
"requestId": "req_abc123def456"
}
```
Store the `callbackUrl` when you request the link token so you can retrieve it later when exchanging the public token.
### Key response fields:
* **`linkToken`**: Use this to initialize Plaid Link in your frontend
* **`callbackUrl`**: Where to POST the public token after Plaid authentication completes. The URL follows the pattern `https://api.lightspark.com/grid/{version}/plaid/callback/{linkToken}`. While you can construct this manually, we recommend using the provided URL for forward compatibility.
* **`expiration`**: Link tokens typically expire after 4 hours
* **`requestId`**: Unique identifier for debugging purposes
Link tokens are single-use and will expire. If the customer doesn't complete
the flow, you'll need to request a new link token.
## Initialize Plaid Link
Display the Plaid Link UI to your customer using the link token. The implementation varies by platform:
Install the appropriate Plaid SDK for your platform:
* React: `npm install react-plaid-link`
* React Native: `npm install react-native-plaid-link-sdk`
* Vanilla JS: Include the Plaid script tag as shown above
```javascript theme={null}
import { usePlaidLink } from 'react-plaid-link';
function BankAccountConnector({ linkToken, onSuccess }) {
const { open, ready } = usePlaidLink({
token: linkToken,
onSuccess: async (publicToken, metadata) => {
console.log('Plaid authentication successful');
// Send public token to YOUR backend endpoint
await fetch('/api/plaid/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: metadata.account_id, // Optional
}),
});
onSuccess();
},
onExit: (error, metadata) => {
if (error) {
console.error('Plaid Link error:', error);
}
console.log('User exited Plaid Link');
},
});
return (
); }
```
```javascript theme={null}
import { PlaidLink } from 'react-native-plaid-link-sdk';
function BankAccountConnector({ linkToken, onSuccess }) {
return (
{
console.log('Plaid authentication successful');
// Send public token to YOUR backend endpoint
await fetch('https://yourapi.com/api/plaid/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: metadata.account_id,
}),
});
onSuccess();
}}
onExit={(error, metadata) => {
if (error) {
console.error('Plaid Link error:', error);
}
}}
>
Connect your bank account
);
}
```
```html theme={null}
```
## Exchange the public token on your backend
Create a backend endpoint that receives the public token from your frontend and forwards it to Grid's callback URL:
```javascript Express theme={null}
// Backend endpoint: POST /api/plaid/exchange-token
app.post('/api/plaid/exchange-token', async (req, res) => {
const { publicToken, accountId } = req.body;
const customerId = req.user.gridCustomerId; // From your auth
try {
// Get the callback URL (you stored this when requesting the link token)
const callbackUrl = await getStoredCallbackUrl(customerId);
// Forward to Grid's callback URL with proper authentication
const response = await fetch(callbackUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: accountId,
}),
});
if (!response.ok) {
throw new Error(`Grid API error: ${response.status}`);
}
const result = await response.json();
res.json({ success: true, message: result.message });
} catch (error) {
console.error('Error exchanging token:', error);
res.status(500).json({ error: 'Failed to process bank account' });
}
});
```
**Response from Grid (HTTP 202 Accepted):**
```json theme={null}
{
"message": "External account creation initiated. You will receive a webhook notification when complete.",
"requestId": "req_def456ghi789"
}
```
A `202 Accepted` response indicates Grid has received the token and is
processing it asynchronously. The external account will be created in the
background.
## Handle webhook notification
After Grid creates the external account, you'll receive an `ACCOUNT_STATUS` webhook.
```json theme={null}
{
"type": "ACCOUNT_STATUS",
"timestamp": "2025-01-15T14:32:10Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-0000000000ac",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"account": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"status": "ACTIVE",
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}
}
```
## Error handling
Handle common error scenarios:
### User exits Plaid Link
```javascript theme={null}
const { open } = usePlaidLink({
token: linkToken,
onExit: (error, metadata) => {
if (error) {
console.error("Plaid error:", error);
// Show user-friendly error message
setError("Unable to connect to your bank. Please try again.");
} else {
// User closed the modal without completing
console.log("User exited without connecting");
}
},
});
```
## Using Plaid for ramps
Plaid integration is particularly valuable for off-ramp flows where users convert crypto to fiat and need a bank account destination:
### Off-ramp benefits
* **Seamless UX**: Users authenticate with their bank directly—no manual account entry
* **Instant verification**: Bank accounts are verified in real-time
* **Reduced errors**: Eliminates typos in account numbers and routing information
* **Higher conversion**: Simplified flow increases completion rates
Plaid is recommended for consumer off-ramp flows where users need to quickly
connect a bank account to receive fiat currency.
### When to use Plaid
Use Plaid integration when:
* **Consumer off-ramps**: Individual users converting crypto to USD/fiat
* **First-time users**: Simplifying the initial bank account setup
* **Mobile apps**: Plaid's mobile SDKs provide native integration
* **Compliance**: Plaid's bank authentication provides additional verification
### When to use manual entry
Manual bank account entry may be preferred for:
* **Business accounts**: Corporate bank accounts often require additional documentation
* **International accounts**: Plaid primarily supports US banks (though expanding)
* **Wire transfers**: Large amounts that require wire-specific account details
* **Non-supported banks**: Some smaller banks or credit unions may not be available via Plaid
## Ramp-specific implementation
### Off-ramp flow with Plaid
User requests to convert Bitcoin to USD and wants to receive funds in their bank account.
```javascript theme={null}
// User clicks "Cash out Bitcoin"
const initiateOffRamp = async (amountSats) => {
// Check if user has connected bank account
const hasAccount = await checkExternalBankAccount(userId);
if (!hasAccount) {
// Trigger Plaid flow
await connectBankViaPlaid();
} else {
// Proceed with quote
await createOffRampQuote(amountSats);
}
};
```
Your backend requests a link token for the customer from Grid.
```javascript theme={null}
const response = await fetch(
'https://api.lightspark.com/grid/2025-10-13/plaid/link-tokens',
{
method: 'POST',
headers: {
'Authorization': `Basic ${gridCredentials}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customerId: gridCustomerId,
}),
}
);
const { linkToken, callbackUrl } = await response.json();
```
Show Plaid Link UI to the user for bank authentication.
```javascript theme={null}
const { open } = usePlaidLink({
token: linkToken,
onSuccess: async (publicToken, metadata) => {
// Forward to your backend
await fetch('/api/plaid/connect-bank', {
method: 'POST',
body: JSON.stringify({
publicToken,
accountId: metadata.account_id,
}),
});
// Show success message
setMessage('Bank connected! Processing your withdrawal...');
},
});
```
Grid creates the external account and sends a webhook notification.
```javascript theme={null}
// Your webhook handler
if (webhookPayload.type === 'ACCOUNT_STATUS' && webhookPayload.account) {
const { account, customerId } = webhookPayload;
// Bank account is ready for off-ramp
await db.users.update({
where: { gridCustomerId: customerId },
data: { bankAccountId: account.accountId },
});
// Automatically proceed with off-ramp if amount is pending
const pendingOffRamp = await db.offRamps.findPending(customerId);
if (pendingOffRamp) {
await createOffRampQuote({
customerId,
sourceAccountId: pendingOffRamp.btcAccountId,
destinationAccountId: account.accountId,
amountSats: pendingOffRamp.amountSats,
});
}
}
```
### Combining Plaid with quote creation
For a seamless user experience, combine Plaid bank connection with immediate quote execution:
```javascript theme={null}
// Complete off-ramp flow
async function executeOffRamp({ userId, amountSats }) {
const customer = await getCustomer(userId);
// Check for existing bank account
let bankAccountId = customer.bankAccountId;
if (!bankAccountId) {
// Initiate Plaid flow
const plaidToken = await requestPlaidLinkToken(customer.gridCustomerId);
// Wait for user to complete Plaid (via frontend)
await waitForPlaidCompletion(userId);
// Refresh customer to get bank account ID
const updated = await getCustomer(userId);
bankAccountId = updated.bankAccountId;
}
// Create off-ramp quote
const quote = await createQuote({
source: {
accountId: customer.btcInternalAccountId, // User's BTC balance
},
destination: {
accountId: bankAccountId, // Plaid-connected bank account
currency: "USD",
},
lockedCurrencySide: "SENDING",
lockedCurrencyAmount: amountSats,
});
// Execute quote
await executeQuote(quote.id);
return quote;
}
```
## Advanced patterns
### Pre-fetching link tokens
Improve UX by prefetching link tokens before users request off-ramps:
```javascript theme={null}
// On app load or settings page
useEffect(() => {
async function prefetchPlaidToken() {
if (!user.hasBankAccount && !sessionStorage.getItem("plaidLinkToken")) {
const response = await fetch("/api/plaid/link-token");
const { linkToken } = await response.json();
sessionStorage.setItem("plaidLinkToken", linkToken);
}
}
prefetchPlaidToken();
}, [user]);
// When user clicks "Cash out", token is already available
const startOffRamp = () => {
const linkToken = sessionStorage.getItem("plaidLinkToken");
if (linkToken) {
plaidLinkHandler.open();
}
};
```
Link tokens expire after 4 hours, so prefetch close to when you expect users
to need them.
### Handling multiple bank accounts
Allow users to connect multiple bank accounts for different off-ramp scenarios:
```javascript theme={null}
// List all Plaid-connected accounts
async function listBankAccounts(userId) {
const customer = await getCustomer(userId);
const accounts = await fetch(
`https://api.lightspark.com/grid/2025-10-13/customers/external-accounts?customerId=${customer.gridCustomerId}&accountType=US_ACCOUNT`,
{ headers: { Authorization: `Basic ${gridCredentials}` } }
);
return accounts.json();
}
// Let user choose destination for off-ramp
const selectBankForOffRamp = async (amountSats) => {
const banks = await listBankAccounts(userId);
if (banks.data.length === 0) {
// No banks connected, trigger Plaid
await connectNewBank();
} else {
// Show bank selection UI
showBankSelector(banks.data, (selectedBank) => {
createOffRampQuote({
destinationAccountId: selectedBank.id,
amountSats,
});
});
}
};
```
### Error recovery
Gracefully handle Plaid errors and offer alternatives:
```javascript theme={null}
const { open } = usePlaidLink({
token: linkToken,
onSuccess: handleSuccess,
onExit: (error, metadata) => {
if (error) {
console.error("Plaid error:", error);
// Show error-specific messaging
if (error.error_code === "ITEM_LOGIN_REQUIRED") {
setError(
"Your bank requires you to log in again. Please try reconnecting."
);
} else if (error.error_code === "INSTITUTION_NOT_RESPONDING") {
setError(
"Your bank is temporarily unavailable. Please try again later or connect manually."
);
// Offer manual entry option
setShowManualEntry(true);
} else {
setError(
"Unable to connect to your bank. Would you like to enter your account details manually?"
);
setShowManualEntry(true);
}
} else {
// User exited without completing
console.log("User closed Plaid without connecting");
}
},
});
{
showManualEntry && ;
}
```
## Best practices for ramps
Plaid's mobile SDKs provide the best UX for app-based off-ramps:
```javascript theme={null}
// React Native example
import { PlaidLink } from "react-native-plaid-link-sdk";
{
await submitToBackend(publicToken);
// Navigate to off-ramp confirmation
navigation.navigate("OffRampConfirm");
}}
>
Connect Bank Account;
```
Store Plaid-connected accounts to avoid re-connection:
```javascript theme={null}
// After successful Plaid connection
const handlePlaidSuccess = async (account) => {
// Store reference in your DB
await db.users.update({
where: { id: userId },
data: {
primaryBankAccountId: account.id,
bankAccountLastFour: account.accountInfo.accountNumber.slice(-4),
bankName: account.accountInfo.bankName,
},
});
// Show in UI without re-fetching
setBankAccount({
id: account.id,
lastFour: account.accountInfo.accountNumber.slice(-4),
bankName: account.accountInfo.bankName,
});
};
```
Keep users informed during async bank account creation:
```javascript theme={null}
// Show loading state while waiting for webhook
const [accountStatus, setAccountStatus] = useState("connecting");
useEffect(() => {
// After Plaid success
if (plaidConnected) {
setAccountStatus("verifying");
// Poll for account or wait for WebSocket
const pollInterval = setInterval(async () => {
const account = await checkBankAccountStatus(userId);
if (account?.status === "ACTIVE") {
setAccountStatus("ready");
clearInterval(pollInterval);
}
}, 2000);
return () => clearInterval(pollInterval);
}
}, [plaidConnected]);
// Show appropriate UI
{
accountStatus === "verifying" && (
Verifying your bank account...
);
}
```
## Next steps
* [Fiat-to-Crypto Conversion](/ramps/conversion-flows/fiat-crypto-conversion) - Build conversion flows
* [Internal Accounts](/ramps/accounts/internal-accounts) - Manage crypto balances for off-ramps
* [Webhooks](/ramps/platform-tools/webhooks) - Handle account creation notifications
* [Sandbox Testing](/ramps/platform-tools/sandbox-testing) - Test Plaid integration safely
## Related resources
* [External Accounts](/ramps/accounts/external-accounts) - Manual bank account setup
* [Platform Configuration](/ramps/onboarding/platform-configuration) - Enable Plaid for your platform
* [API Reference](/api-reference) - Complete Plaid API documentation
# Fiat-to-Crypto and Crypto-to-Fiat
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/conversion-flows/fiat-crypto-conversion
Build on-ramp and off-ramp flows to convert between fiat currencies and cryptocurrencies
## Overview
Grid enables seamless conversion between fiat currencies and cryptocurrencies via the Lightning Network. Use quotes to lock exchange rates and get payment instructions for completing transfers.
**On-ramp (Fiat → Crypto):** User sends fiat → Grid detects payment → Crypto sent to wallet
**Off-ramp (Crypto → Fiat):** Execute quote → Grid processes crypto → Fiat sent to bank
## Prerequisites
* Customer created in Grid
* **On-ramps:** Destination crypto wallet (Spark address) + webhook endpoint
* **Off-ramps:** Internal account with crypto + external bank account registered
## On-ramp: Fiat to crypto
### Create a quote
```bash cURL theme={null}
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": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "USD"
},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"beneficiary": {
"counterPartyType": "INDIVIDUAL",
"fullName": "John Doe",
"email": "john@example.com"
},
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
},
"lockedCurrencySide": "RECEIVING",
"lockedCurrencyAmount": 100000,
"description": "Buy 0.001 BTC"
}'
```
```javascript Node.js theme={null}
const quote = await fetch("https://api.lightspark.com/grid/2025-10-13/quotes", {
method: "POST",
headers: {
Authorization: `Basic ${credentials}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
source: {
customerId: "Customer:019542f5-b3e7-1d02-0000-000000000001",
currency: "USD",
},
destination: {
externalAccountDetails: {
customerId: "Customer:019542f5-b3e7-1d02-0000-000000000001",
currency: "BTC",
beneficiary: {
counterPartyType: "INDIVIDUAL",
fullName: "John Doe",
email: "john@example.com",
},
accountInfo: {
accountType: "SPARK_WALLET",
address:
"spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu",
},
},
},
lockedCurrencySide: "RECEIVING",
lockedCurrencyAmount: 100000, // 0.001 BTC in satoshis
description: "Buy 0.001 BTC",
}),
}).then((r) => r.json());
```
**Response includes payment instructions:**
```json theme={null}
{
"id": "Quote:019542f5-b3e7-1d02-0000-000000000006",
"totalSendingAmount": 6500,
"receivingAmount": 100000,
"expiresAt": "2025-10-03T12:05:00Z",
"paymentInstructions": [
{
"accountOrWalletInfo": {
"accountType": "US_ACCOUNT",
"reference": "UMA-Q12345-REF",
"accountNumber": "1234567890",
"routingNumber": "021000021",
"bankName": "Grid Settlement Bank"
}
},
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
]
}
```
### Display payment instructions
```javascript theme={null}
function displayPaymentInstructions(quote) {
const instructions = quote.paymentInstructions[0];
return {
amount: `$${(quote.totalSendingAmount / 100).toFixed(2)}`,
bankName: instructions.accountOrWalletInfo.bankName,
accountNumber: instructions.accountOrWalletInfo.accountNumber,
routingNumber: instructions.accountOrWalletInfo.routingNumber,
referenceCode: instructions.reference, // User must include this
expiresAt: quote.expiresAt,
willReceive: `${quote.receivingAmount / 100000000} BTC`,
};
}
```
### Monitor completion
Grid sends a webhook when the transfer completes:
```javascript theme={null}
app.post("/webhooks/grid", async (req, res) => {
const { type, transaction } = req.body;
if (type === "OUTGOING_PAYMENT" && transaction.status === "COMPLETED") {
await notifyUser(transaction.customerId, {
message: "Your Bitcoin purchase is complete!",
amount: `${transaction.receivedAmount.amount / 100000000} BTC`,
});
}
res.status(200).json({ received: true });
});
```
## Off-ramp: Crypto to fiat
### Create and execute a quote
```javascript theme={null}
// 1. Create quote
const quote = await fetch("https://api.lightspark.com/grid/2025-10-13/quotes", {
method: "POST",
body: JSON.stringify({
source: {
accountId: "InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
},
destination: {
accountId: "ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
currency: "USD",
},
lockedCurrencySide: "SENDING",
lockedCurrencyAmount: 100000, // 0.001 BTC
description: "Sell 0.001 BTC",
}),
}).then((r) => r.json());
// 2. Execute quote
const result = await fetch(
`https://api.lightspark.com/grid/2025-10-13/quotes/${quote.id}/execute`,
{
method: "POST",
headers: { Authorization: `Basic ${credentials}` },
}
).then((r) => r.json());
```
### Track completion
```javascript theme={null}
app.post("/webhooks/grid", async (req, res) => {
const { type, transaction } = req.body;
if (type === "OUTGOING_PAYMENT" && transaction.status === "COMPLETED") {
await notifyUser(transaction.customerId, {
message: "Your USD withdrawal is complete!",
amount: `$${transaction.receivedAmount.amount / 100}`,
});
}
res.status(200).json({ received: true });
});
```
## Immediate execution
For instant on-ramps (e.g., reward payouts), use `immediatelyExecute: true`:
```javascript theme={null}
const quote = await fetch("https://api.lightspark.com/grid/2025-10-13/quotes", {
method: "POST",
body: JSON.stringify({
source: { customerId: "Customer:...", currency: "USD" },
destination: {
externalAccountDetails: {
/* wallet details */
},
},
lockedCurrencySide: "RECEIVING",
lockedCurrencyAmount: 100000,
immediatelyExecute: true,
}),
}).then((r) => r.json());
```
## Best practices
```javascript theme={null}
async function refreshQuoteIfNeeded(quote) {
const expiresAt = new Date(quote.expiresAt);
const now = new Date();
if (expiresAt - now < 60000) {
// Less than 1 minute left
return await createNewQuote(quote.originalParams);
}
return quote;
}
```
```javascript theme={null}
const settlementTimes = {
US_ACCOUNT: "1-3 business days (ACH)",
WIRE: "Same day",
SPARK_WALLET: "Instant",
PIX: "Instant",
SEPA: "1-2 business days",
};
```
```javascript theme={null}
if (transaction.status === "FAILED") {
await notifyUser(transaction.customerId, {
message: "Transaction failed",
reason: transaction.failureReason,
action: "retry",
});
if (transaction.failureReason === "QUOTE_EXPIRED") {
await createNewQuote(transaction.originalParams);
}
}
```
## Next steps
Send crypto to user-controlled wallets
Monitor transaction status
Complete API documentation
# Self-Custody Wallet Integration
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/conversion-flows/self-custody-wallets
Send and receive cryptocurrency to and from user-controlled wallets
## Overview
Grid supports sending Bitcoin via Lightning Network to self-custody wallets using Spark wallet addresses. This enables users to maintain full control of their crypto while benefiting from Grid's fiat-to-crypto conversion and payment rails.
Spark wallets use the Lightning Network for instant, low-cost Bitcoin
transactions. Users can receive payments directly to their self-custody
wallets without Grid holding their funds.
## How it works
1. **User provides wallet address** - Get Spark wallet address from user
2. **Create quote** - Generate quote for fiat-to-crypto or crypto-to-crypto transfer
3. **Execute transfer** - Send crypto directly to user's wallet
4. **Instant settlement** - Lightning Network provides near-instant confirmation
## Prerequisites
* Customer created in Grid
* Valid Spark wallet address from user
* Webhook endpoint for payment notifications (optional, for status updates)
## Sending crypto to self-custody wallets
### Step 1: Collect wallet address
Request the user's Spark wallet address. Spark addresses start with `spark1`:
```javascript theme={null}
function validateSparkAddress(address) {
// Spark addresses start with spark1 and are typically 90+ characters
if (!address.startsWith("spark1")) {
throw new Error("Invalid Spark wallet address");
}
if (address.length < 90) {
throw new Error("Spark address appears incomplete");
}
return address;
}
// Example valid address
const sparkAddress =
"spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu";
```
Always validate wallet addresses before creating quotes. Invalid addresses
will cause transaction failures and potential fund loss.
### Step 2: Create external account for the wallet
Register the Spark wallet as an external account:
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"beneficiary": {
"counterPartyType": "INDIVIDUAL",
"fullName": "John Doe",
"email": "john@example.com"
},
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}'
```
```javascript Node.js theme={null}
const externalAccount = await fetch(
"https://api.lightspark.com/grid/2025-10-13/customers/external-accounts",
{
method: "POST",
headers: {
Authorization: `Basic ${credentials}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
customerId: "Customer:019542f5-b3e7-1d02-0000-000000000001",
currency: "BTC",
beneficiary: {
counterPartyType: "INDIVIDUAL",
fullName: "John Doe",
email: "john@example.com",
},
accountInfo: {
accountType: "SPARK_WALLET",
address: sparkAddress,
},
}),
}
).then((r) => r.json());
console.log("External account ID:", externalAccount.id);
```
```python Python theme={null}
import requests
import base64
credentials = base64.b64encode(
f"{api_token_id}:{api_secret}".encode()
).decode()
response = requests.post(
'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts',
headers={
'Authorization': f'Basic {credentials}',
'Content-Type': 'application/json'
},
json={
'customerId': 'Customer:019542f5-b3e7-1d02-0000-000000000001',
'currency': 'BTC',
'beneficiary': {
'counterPartyType': 'INDIVIDUAL',
'fullName': 'John Doe',
'email': 'john@example.com'
},
'accountInfo': {
'accountType': 'SPARK_WALLET',
'address': spark_address
}
}
)
external_account = response.json()
print(f"External account ID: {external_account['id']}")
```
Store the external account ID for future transfers. Users can reuse the same
wallet address for multiple transactions.
### Step 3: Create and execute a quote
#### Option A: From fiat to self-custody wallet
Convert fiat directly to crypto in user's wallet:
```javascript theme={null}
// Create quote for fiat-to-crypto
const quote = await fetch("https://api.lightspark.com/grid/2025-10-13/quotes", {
method: "POST",
body: JSON.stringify({
source: {
customerId: "Customer:019542f5-b3e7-1d02-0000-000000000001",
currency: "USD",
},
destination: {
accountId: externalAccount.id,
currency: "BTC",
},
lockedCurrencySide: "SENDING",
lockedCurrencyAmount: 10000, // $100.00
description: "Buy Bitcoin to self-custody wallet",
}),
}).then((r) => r.json());
// Display payment instructions to user
console.log("Send fiat to:", quote.paymentInstructions);
console.log("Will receive:", `${quote.receivingAmount / 100000000} BTC`);
```
#### Option B: From internal account to self-custody wallet
Transfer crypto from internal account to user's wallet:
```javascript theme={null}
// Same-currency transfer (no quote needed)
const transaction = await fetch(
"https://api.lightspark.com/grid/2025-10-13/transfer-out",
{
method: "POST",
body: JSON.stringify({
source: {
accountId: "InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
},
destination: {
accountId: externalAccount.id,
},
amount: 100000, // 0.001 BTC in satoshis
}),
}
).then((r) => r.json());
console.log("Transfer initiated:", transaction.id);
```
### Step 4: Monitor transfer completion
Track the transfer status via webhooks:
```javascript theme={null}
app.post("/webhooks/grid", async (req, res) => {
const { type, transaction } = req.body;
if (type === "OUTGOING_PAYMENT") {
if (transaction.status === "COMPLETED") {
// Notify user of successful transfer
await notifyUser(transaction.customerId, {
message: "Bitcoin sent to your wallet!",
amount: `${transaction.receivedAmount.amount / 100000000} BTC`,
walletAddress: transaction.destination.accountInfo.address,
});
} else if (transaction.status === "FAILED") {
// Handle failure
await notifyUser(transaction.customerId, {
message: "Transfer failed",
reason: transaction.failureReason,
action: "Please verify your wallet address",
});
}
}
res.status(200).json({ received: true });
});
```
## Best practices
Always validate Spark addresses before processing:
```javascript theme={null}
function validateSparkAddress(address) {
// Check format
if (!address.startsWith("spark1")) {
return { valid: false, error: "Must start with spark1" };
}
// Check length (typical Spark addresses are 90+ chars)
if (address.length < 90) {
return { valid: false, error: "Address too short" };
}
// Check for common typos
if (address.includes(" ") || address.includes("\n")) {
return { valid: false, error: "Address contains whitespace" };
}
return { valid: true };
}
```
Lightning Network has unique characteristics:
```javascript theme={null}
const lightningLimits = {
minAmount: 1000, // 0.00001 BTC (1000 satoshis)
maxAmount: 10000000, // 0.1 BTC (10M satoshis)
settlementTime: "Instant (typically < 10 seconds)",
fees: "Very low (typically < 1%)",
};
function validateLightningAmount(satoshis) {
if (satoshis < lightningLimits.minAmount) {
throw new Error(`Minimum amount is ${lightningLimits.minAmount} sats`);
}
if (satoshis > lightningLimits.maxAmount) {
throw new Error(`Maximum amount is ${lightningLimits.maxAmount} sats`);
}
return true;
}
```
Help users understand the process:
```javascript theme={null}
function getWalletInstructions(walletType) {
return {
SPARK_WALLET: {
title: "Lightning Network Wallet",
steps: [
"Open your Lightning wallet app",
'Select "Send" or "Pay"',
"Scan the QR code or paste the address",
"Confirm the amount and send",
"Funds arrive instantly",
],
compatibleWallets: [
"Spark Wallet",
"Phoenix",
"Breez",
"Muun",
"Blue Wallet (Lightning)",
],
},
};
}
```
Implement retry logic for common failures:
```javascript theme={null}
async function handleTransferFailure(transaction) {
const { failureReason } = transaction;
const retryableReasons = [
"TEMPORARY_NETWORK_ERROR",
"INSUFFICIENT_LIQUIDITY",
"ROUTE_NOT_FOUND",
];
if (retryableReasons.includes(failureReason)) {
// Retry after delay
await delay(5000);
return await retryTransfer(transaction.quoteId);
}
// Non-retryable errors
const errorMessages = {
INVALID_ADDRESS:
"The wallet address is invalid. Please verify and try again.",
AMOUNT_TOO_SMALL:
"Amount is below minimum. Lightning Network requires at least 1000 satoshis.",
AMOUNT_TOO_LARGE:
"Amount exceeds Lightning Network limits. Consider splitting into multiple transfers.",
};
return {
canRetry: false,
message:
errorMessages[failureReason] ||
"Transfer failed. Please contact support.",
};
}
```
## Testing in sandbox
Test self-custody wallet flows in sandbox mode:
```javascript theme={null}
// Sandbox: Use test Spark addresses
const testSparkAddress =
"spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu";
// Create test transfer
const testTransfer = await fetch(
"https://api.lightspark.com/grid/2025-10-13/quotes",
{
method: "POST",
body: JSON.stringify({
source: { accountId: "InternalAccount:..." },
destination: {
externalAccountDetails: {
customerId: "Customer:...",
currency: "BTC",
beneficiary: {
/* test data */
},
accountInfo: {
accountType: "SPARK_WALLET",
address: testSparkAddress,
},
},
},
lockedCurrencySide: "SENDING",
lockedCurrencyAmount: 10000,
immediatelyExecute: true,
}),
}
).then((r) => r.json());
// Simulate completion (sandbox auto-completes)
console.log("Test transfer status:", testTransfer.status);
```
In sandbox mode, transfers to Spark wallets complete instantly without
requiring actual Lightning Network transactions.
## Next steps
Learn about on-ramp and off-ramp flows
Manage external accounts including Spark wallets
Set up webhooks to monitor transaction status
Test wallet integrations in sandbox mode
# Ramps
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/index
With Grid, you can seamlessly convert between fiat currencies and cryptocurrencies through a single, simple API. The automatically handles currency conversion, compliance, and instant settlement via the Lightning Network.
Convert fiat to crypto (on-ramp) or crypto to fiat (off-ramp) in seconds using real-time exchange rates.
Grid handles the conversion and settlement process, reducing complexity in your crypto integration.
Leverages the Lightning Network for instant Bitcoin transfers and local banking rails for fiat settlement worldwide.
***
## How Ramps Work
Get real-time exchange rates and payment instructions for your desired
conversion (fiat → crypto or crypto → fiat).
Fund the quote using the provided payment instructions. Grid executes the
conversion at the quoted rate.
Receive your converted funds via Lightning Network (for crypto) or local
banking rails (for fiat) within seconds.
***
## Features
Users interact with through two main interfaces:
Programmatic access to create quotes, fund conversions, execute transfers,
and reconcile all activity with real-time webhooks.
Your development and operations team can use the dashboard to monitor
conversions, manage API keys and environments, and troubleshoot with
detailed logs.
### On-Ramp: Fiat to Crypto
Convert fiat currency to cryptocurrency and deliver it to self-custody wallets.
* **Create quotes** to lock exchange rates for fiat-to-crypto conversions
* **Payment instructions** guide users on how to fund their conversion
* **Instant delivery** to Spark wallets or other supported crypto destinations
* **Webhook notifications** confirm successful conversion and delivery
### Off-Ramp: Crypto to Fiat
Convert cryptocurrency to fiat currency and deliver it to bank accounts.
* **Pre-funded accounts** hold crypto balances for conversion
* **Real-time rates** for crypto-to-fiat exchange
* **Bank account delivery** via local payment rails
* **Compliance** handled automatically through Grid's infrastructure
### Funding Options
supports multiple funding models for on-ramps and off-ramps:
* **Just-in-time (JIT)**: Create a quote and fund it in real-time using payment instructions (ideal for on-ramps)
* **Pre-funded accounts**: Maintain balances in crypto or fiat and convert from those balances (ideal for off-ramps)
You can mix funding models based on your use case. On-ramps typically use JIT
funding, while off-ramps use pre-funded accounts.
### Environments
supports two environments: **Sandbox** and **Production**.
The Sandbox mirrors production behavior, allowing you to test the full end-to-end flow—from creating quotes and simulating funding to executing conversions and receiving webhooks—without moving real funds.
The Production environment uses live credentials and base URLs for real transactions once you're ready to launch.
***
Ready to integrate ? Check out our quickstart guide.
# Configuring Customers
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/onboarding/configuring-customers
Create and manage customers for ramp conversions
Customers must complete identity verification before processing conversions. The required information varies based on your platform's regulatory status.
**Regulated platforms** have lighter KYC requirements since they handle compliance verification internally.
The KYC/KYB flow allows you to onboard customers through direct API calls.
Regulated financial institutions can:
* **Direct API Onboarding**: Create customers directly via API calls with minimal verification
* **Internal KYC/KYB**: Handle identity verification through your own compliance systems
* **Reduced Documentation**: Only provide essential customer information required by your payment counterparty or service provider.
* **Faster Onboarding**: Streamlined process for known, verified customers
#### Creating Customers via Direct API
For regulated platforms, you can create customers directly through the API without requiring external KYC verification:
To register a new customer in the system, use the `POST /customers` endpoint:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/customers" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"platformCustomerId": "customer_12345",
"customerType": "INDIVIDUAL",
"fullName": "Jane Doe",
"birthDate": "1992-03-25",
"nationality": "US",
"address": {
"line1": "123 Pine Street",
"city": "Seattle",
"state": "WA",
"postalCode": "98101",
"country": "US"
}
}'
```
The examples below show a more comprehensive set of data. Not all fields are strictly required by the API for customer creation itself, but become necessary based on currency and UMA provider requirements if using UMA.
```json theme={null}
{
"platformCustomerId": "9f84e0c2a72c4fa",
"customerType": "INDIVIDUAL",
"fullName": "John Sender",
"birthDate": "1985-06-15",
"address": {
"line1": "Paseo de la Reforma 222",
"line2": "Piso 15",
"city": "Ciudad de México",
"state": "Ciudad de México",
"postalCode": "06600",
"country": "MX"
}
}
```
```json theme={null}
{
"platformCustomerId": "b87d2e4a9c13f5b",
"customerType": "BUSINESS",
"businessInfo": {
"legalName": "Acme Corporation",
"registrationNumber": "789012345",
"taxId": "123-45-6789"
},
"address": {
"line1": "456 Oak Avenue",
"line2": "Floor 12",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}
```
**Unregulated platforms** require full KYC/KYB verification of customers through hosted flows.
Unregulated platforms must:
* **Hosted KYC Flow**: Use the hosted KYC link for complete identity verification
* **Extended Review**: Customers may require manual review and approval in some cases
### Hosted KYC Link Flow
The hosted KYC flow provides a secure, hosted interface where customers can complete their identity verification and onboarding process.
#### Generate KYC Link
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/customers/kyc-link?redirectUri=https://yourapp.com/onboarding-complete&platformCustomerId=019542f5-b3e7-1d02-0000-000000000001" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
**Response:**
```json theme={null}
{
"kycUrl": "https://kyc.lightspark.com/onboard/abc123def456",
"platformCustomerId": "019542f5-b3e7-1d02-0000-000000000001"
}
```
#### Complete KYC Process
Call the `/customers/kyc-link` endpoint with your `redirectUri` parameter to generate a hosted KYC URL for your customer.
The `redirectUri` parameter is embedded in the generated KYC URL and will be used to automatically redirect the customer back to your application after they complete verification.
Redirect your customer to the returned `kycUrl` where they can complete their identity verification in the hosted interface.
The KYC link is single-use and expires after a limited time period for security.
The customer completes the identity verification process in the hosted KYC interface, providing required documents and information.
The hosted interface handles document collection, verification checks, and compliance requirements automatically.
After verification processing, you'll receive a KYC status webhook notification indicating the final verification result.
Upon successful KYC completion, the customer is automatically redirected to your specified `redirectUri` URL.
The customer account will be automatically created by the system upon successful KYC completion. You can identify the new customer using your `platformCustomerId` or other identifiers.
On your redirect page, handle the completed KYC flow and integrate the new customer into your application.
## Monitor verification status
After a customer completes the KYC/KYB verification process, you'll receive webhook notifications about their KYC status. These notifications are sent to your configured webhook endpoint.
For regulated platforms, customers are created with `APPROVED` KYC status by default.
**Webhook Payload (sent to your endpoint):**
```json theme={null}
{
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
"type": "KYC_STATUS",
"timestamp": "2023-07-21T17:32:28Z",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"kycStatus": "APPROVED",
"platformCustomerId": "1234567"
}
```
**Webhook Headers:**
* `Content-Type: application/json`
* `X-Webhook-Signature: sha256=abc123...`
System-generated unique identifier of the customer whose KYC status has changed.
Final KYC verification status. Webhooks are only sent for final states:
* `APPROVED`: Customer verification completed successfully
* `REJECTED`: Customer verification was rejected
* `EXPIRED`: KYC verification has expired and needs renewal
* `CANCELED`: Verification process was canceled
* `MANUALLY_APPROVED`: Customer was manually approved by platform
* `MANUALLY_REJECTED`: Customer was manually rejected by platform
Intermediate states like `PENDING_REVIEW` do not trigger webhook notifications. Only final resolution states will send webhook notifications.
```javascript theme={null}
// Example webhook handler for KYC status updates
// Note: Only final states trigger webhook notifications
app.post('/webhooks/kyc-status', (req, res) => {
const { customerId, kycStatus } = req.body;
switch (kycStatus) {
case 'APPROVED':
// Activate customer account
await activateCustomer(customerId);
await sendWelcomeEmail(customerId);
break;
case 'REJECTED':
// Notify support and customer
await notifySupport(customerId, 'KYC_REJECTED');
await sendRejectionEmail(customerId);
break;
case 'MANUALLY_APPROVED':
// Handle manual approval
await activateCustomer(customerId);
await sendWelcomeEmail(customerId);
break;
case 'MANUALLY_REJECTED':
// Handle manual rejection
await notifySupport(customerId, 'KYC_MANUALLY_REJECTED');
await sendRejectionEmail(customerId);
break;
case 'EXPIRED':
// Handle expired KYC
await notifyCustomerForReKyc(customerId);
break;
case 'CANCELED':
// Handle canceled verification
await logKycCancelation(customerId);
break;
default:
// Log unexpected statuses
console.log(`Unexpected KYC status ${kycStatus} for customer ${customerId}`);
}
res.status(200).send('OK');
});
```
Only customers with `APPROVED` status can create quotes and process
conversions.
***
## Customer types
For personal conversions and consumer wallets.
**Required fields:** `fullName`, `email`, `birthDate`, `address`
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers' \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"platformCustomerId": "user_12345",
"customerType": "INDIVIDUAL",
"fullName": "Alice Johnson",
"email": "alice@example.com",
"birthDate": "1990-01-15",
"address": {...}
}'
```
For corporate conversions and business accounts.
**Required fields:** `businessName`, `email`, `taxId`, `address`
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers' \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"platformCustomerId": "biz_67890",
"customerType": "BUSINESS",
"businessName": "Acme Corporation",
"email": "finance@acme.com",
"taxId": "12-3456789",
"address": {...}
}'
```
***
## Next steps
Fund crypto for off-ramp conversions
Set up wallet destinations
Build conversion flows
Complete customer API docs
# Implementation Overview
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/onboarding/implementation-overview
This page gives you a 10,000‑ft view of an end‑to‑end Grid implementation for on and off-ramps.
It is intentionally generalized to cover both on-ramp (fiat → crypto) and off-ramp (crypto → fiat) flows.
The detailed guides that follow provide concrete fields, edge cases, and step‑by‑step instructions.
This overview highlights the main building blocks: platform setup, customer
onboarding, account management, conversion flows, reconciliation, sandbox
testing, and go‑live enablement.
## Platform configuration
Configure your platform once before building user flows.
* Provide webhook endpoints for transaction and conversion status notifications
* Generate API credentials for Sandbox (and later Production)
* Configure supported currencies for conversions (fiat and crypto)
* Review settlement methods and supported crypto destinations
## Onboarding customers
Onboard customers who will use ramp services. There are two patterns:
* Regulated platforms can directly create customers by providing minimal KYC data via API
* Unregulated entities should request a KYC link and embed the hosted KYC flow; once completed, the customer can transact
You'll need to persist the Grid customer IDs for use in conversion flows.
## Account management
Set up accounts for funding and receiving conversions.
### Internal Accounts
* Platform and customer internal accounts hold fiat or crypto balances
* Used for pre-funded off-ramp scenarios (crypto → fiat)
* Fund via ACH, wire, or crypto deposits
### External Accounts
* Register crypto wallets (Spark, Bitcoin addresses) for on-ramp destinations
* Register bank accounts for off-ramp destinations
* Capture required beneficiary and account information
On-ramps typically deliver to external crypto wallets, while off-ramps
typically deliver to external bank accounts.
## On-ramp flow (Fiat → Crypto)
Enable users to convert fiat currency to cryptocurrency.
### Quote Creation
* Create a quote specifying source (fiat) and destination (crypto wallet)
* Lock exchange rate and receive payment instructions
* Quote includes reference code for payment matching
### Just-in-Time Funding
* User sends fiat payment to provided bank account details
* Include reference code in payment memo for matching
* Grid detects payment and automatically executes conversion
### Crypto Delivery
* Grid converts fiat to crypto at locked rate
* Crypto delivered to specified wallet (Spark, Bitcoin, etc.)
* Webhook notification confirms delivery
For JIT-funded quotes, do NOT call the execute endpoint. Grid automatically
processes the conversion when it receives your payment.
## Off-ramp flow (Crypto → Fiat)
Enable users to convert cryptocurrency to fiat currency.
### Pre-funding
* Customer or platform holds crypto in internal accounts
* Balances can be funded via crypto deposits (Lightning, on-chain)
### Quote and Execution
* Create a quote specifying source (crypto account) and destination (bank account)
* Lock exchange rate and fees
* Execute the quote to initiate conversion
* Grid converts crypto to fiat and delivers to bank account
### Fiat Delivery
* Fiat delivered via local banking rails
* Settlement times vary by destination (instant to 1-3 business days)
* Webhook notification confirms delivery
## Self-custody wallet integration
Support for sending crypto to user-controlled wallets.
* **Spark wallets**: Lightning-compatible wallets for instant, low-fee Bitcoin transfers
* **Validation**: Validate wallet addresses before creating external accounts
* **Limits**: Verify minimum and maximum amounts for crypto transfers
* **Monitoring**: Track transfer completion via webhooks
Spark wallets are recommended for crypto delivery as transfers complete within
seconds with minimal fees.
## Reconciling transactions
Implement operational processes to keep your ledger in sync.
* Process webhooks idempotently; map statuses (pending, processing, completed, failed)
* Tie transactions back to quotes and customers
* Track conversion rates and fees for accurate accounting
* Query for transactions by date range or customer as necessary
## Testing in Sandbox
Use Sandbox to build and validate end‑to‑end without moving real funds.
* Simulate fiat funding using `/sandbox/send` endpoint
* Simulate crypto deposits using `/sandbox/internal-accounts/{accountId}/fund` endpoint or `/transfer-in` endpoint
* Test quote creation, conversion, and webhook lifecycles
* Validate customer onboarding flows with test data
See the [Sandbox Testing](../platform-tools/sandbox-testing.mdx) page for more details.
## Enabling Production
When you're ready to go live:
* Ensure adequate funding mechanisms are in place (for off-ramps)
* Confirm webhook security, monitoring, and alerting are configured
* Review rate limits, error handling, and idempotency
* Test conversion flows thoroughly in Sandbox
* Run final UAT in Sandbox, then request Production access from our team
Contact our team to enable Production and begin processing real conversions.
## Support
If you need assistance with , please contact our support team at [support@lightspark.com](mailto:support@lightspark.com) or visit our support portal at [https://support.lightspark.com](https://support.lightspark.com).
# Platform Configuration
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/onboarding/platform-configuration
This guide explains how to configure your platform for ramp operations: get API credentials, configure webhooks, and set up supported currencies for conversions.
## API credentials and authentication
Create API credentials in the Grid dashboard. Credentials are scoped to an environment (Sandbox or Production) and cannot be used across environments.
* **Authentication**: Use HTTP Basic Auth with your API key and secret in the `Authorization` header
* **Environment isolation**: Sandbox keys only work against Sandbox; Production keys only work against Production
Never share or expose your API secret. Rotate credentials periodically and
restrict access to authorized systems only.
### Example: HTTP Basic Auth
```bash theme={null}
# Using cURL's Basic Auth shorthand (-u):
curl -sS -X GET 'https://api.lightspark.com/grid/2025-10-13/config' \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
## Base API path
The base API path is consistent across environments; your credentials determine which environment you're accessing.
**Base URL**: `https://api.lightspark.com/grid/2025-10-13`
The same base URL is used for both Sandbox and Production. Your API keys
determine which environment processes your requests.
## Supported currencies
During onboarding, configure the currencies your platform will support for conversions. Grid supports:
### Fiat Currencies
* **USD** (United States Dollar) - Primary fiat currency
* **EUR** (Euro)
* **GBP** (British Pound)
* **MXN** (Mexican Peso)
* And more regional currencies
### Cryptocurrencies
* **Bitcoin** (BTC) via Spark, L1, or Lightning Network
* **Stablecoins**
You can add or remove supported currencies anytime in the Grid dashboard. For pre-funded models, Grid automatically creates internal accounts for each supported currency.
Start with USD and BTC for the simplest on-ramp and off-ramp implementation,
then add additional currencies as needed.
## Webhooks and notifications
Configure your webhook endpoint to receive real-time notifications about conversion status, transaction completion, and account balance updates.
### Webhook setup
1. **Create a public HTTPS endpoint** to receive webhook notifications
2. **Configure the endpoint URL** in the Grid dashboard
3. **Verify webhook signatures** using the Grid public key (provided in dashboard)
4. **Respond with 2xx status codes** to acknowledge receipt
5. **Process events idempotently** to handle duplicate deliveries
For development, use reverse proxies like ngrok to expose local endpoints.
Never use them in production.
### Webhook signature verification
Webhooks use asymmetric (public/private key) signatures for security:
* Verify the `X-Grid-Signature` header against the exact request body
* Use the Grid public key from your dashboard
* Reject webhooks with invalid signatures
The public key for verification is shown in the dashboard. Rotate keys when
instructed by Lightspark support.
### Test your webhook endpoint
Use the webhook test endpoint to verify your endpoint configuration:
```bash theme={null}
curl -sS -X POST 'https://api.lightspark.com/grid/2025-10-13/webhooks/test' \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
**Example test webhook payload:**
```json theme={null}
{
"test": true,
"timestamp": "2025-10-03T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000001",
"type": "TEST"
}
```
If your endpoint receives the test webhook and responds with a 2xx status
code, your webhook configuration is working correctly.
## Conversion limits
Configure minimum and maximum amounts for conversions per currency:
* **Minimum amounts**: Prevent uneconomical micro-conversions
* **Maximum amounts**: Manage risk and liquidity requirements
* **Per-transaction limits**: Configurable in the dashboard
## Retrieve platform configuration
You can retrieve your current platform configuration programmatically:
```bash theme={null}
curl -sS -X GET 'https://api.lightspark.com/grid/2025-10-13/config' \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
**Response example:**
```json theme={null}
{
"id": "PlatformConfig:019542f5-b3e7-1d02-0000-000000000003",
"webhookEndpoint": "https://api.example.com/webhooks/grid",
"supportedCurrencies": [
{
"currencyCode": "USD",
"minAmount": 100,
"maxAmount": 1000000,
"enabledTransactionTypes": ["OUTGOING", "INCOMING"]
},
{
"currencyCode": "BTC",
"minAmount": 1000,
"maxAmount": 10000000,
"enabledTransactionTypes": ["OUTGOING", "INCOMING"]
}
],
"createdAt": "2025-09-01T12:30:45Z",
"updatedAt": "2025-10-01T10:00:00Z"
}
```
## Update platform configuration
Update your platform configuration using the PATCH endpoint:
```bash theme={null}
curl -sS -X PATCH 'https://api.lightspark.com/grid/2025-10-13/config' \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"webhookEndpoint": "https://api.mycompany.com/webhooks/grid",
"supportedCurrencies": [
{
"currencyCode": "USD",
"minAmount": 100,
"maxAmount": 1000000,
"enabledTransactionTypes": ["OUTGOING", "INCOMING"]
},
{
"currencyCode": "BTC",
"minAmount": 1000,
"maxAmount": 10000000,
"enabledTransactionTypes": ["OUTGOING", "INCOMING"]
}
]
}'
```
Configuration changes take effect immediately. Test changes in Sandbox before
applying to Production.
# Postman Collection
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/platform-tools/postman-collection
Use our hosted Postman collection to explore endpoints and send test requests quickly.
Launch the collection in Postman.
# Sandbox Testing
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/platform-tools/sandbox-testing
Test ramp flows safely without moving real funds
The Grid Sandbox environment provides a complete testing environment for ramp operations, allowing you to validate on-ramp and off-ramp flows without using real money or cryptocurrency.
## Sandbox overview
Sandbox mirrors production behavior while using simulated funds:
* **Same API endpoints**: Use identical API calls as production
* **Simulated funding**: Mock bank transfers and crypto deposits
* **Real webhooks**: Receive actual webhook notifications
* **No real money**: All transactions use test funds
* **Isolated environment**: Sandbox data never affects production
Sandbox is perfect for development, testing, and demonstrating ramp
functionality before going live.
## Getting started
### Create sandbox credentials
1. Log into the Grid dashboard
2. Navigate to **Settings** → **API Keys**
3. Click **Create API Key** and select **Sandbox** environment
4. Save your API key ID and secret securely
Sandbox credentials only work with the sandbox environment. They cannot access
production data or move real funds.
### Configure sandbox webhook
Set up a webhook endpoint for sandbox notifications:
```bash theme={null}
curl -X PATCH 'https://api.lightspark.com/grid/2025-10-13/config' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"webhookEndpoint": "https://api.yourapp.dev/webhooks/grid"
}'
```
Use tools like ngrok to expose local webhook endpoints during development:
`ngrok http 3000`
## Testing on-ramps (Fiat → Crypto)
Simulate the complete on-ramp flow in sandbox:
### Step 1: Create a test customer
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"platformCustomerId": "test_user_001",
"customerType": "INDIVIDUAL",
"fullName": "Alice Test",
"email": "alice@example.com",
"birthDate": "1990-01-15",
"address": {
"line1": "123 Test Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}'
```
In sandbox, customers are automatically approved for testing.
### Step 2: Create an on-ramp quote (just-in-time funding)
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"source": {
"customerId": "Customer:sandbox001",
"currency": "USD"
},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:sandbox001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000,
"description": "Test on-ramp conversion"
}'
```
The quote response includes payment instructions with a reference code.
### Step 3: Simulate funding
Use the sandbox endpoint to simulate receiving the fiat payment:
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/sandbox/send' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"reference": "RAMP-ABC123",
"currencyCode": "USD",
"currencyAmount": 10000
}'
```
The reference code must match the one provided in the quote's payment
instructions.
### Step 4: Verify completion
Within seconds, you'll receive a webhook notification confirming the on-ramp completed:
```json theme={null}
{
"transaction": {
"id": "Transaction:sandbox025",
"status": "COMPLETED",
"type": "OUTGOING",
"sentAmount": {
"amount": 10000,
"currency": { "code": "USD" }
},
"receivedAmount": {
"amount": 95000,
"currency": { "code": "BTC" }
},
"settledAt": "2025-10-03T15:02:30Z"
},
"type": "OUTGOING_PAYMENT"
}
```
## Testing off-ramps (Crypto → Fiat)
Simulate the complete off-ramp flow:
### Step 1: Fund internal account with crypto
Simulate a Bitcoin deposit to the customer's internal account using the sandbox funding endpoint:
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/sandbox/internal-accounts/InternalAccount:btc001/fund' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"amount": 10000000
}'
```
Replace `InternalAccount:btc001` with your actual BTC internal account ID.
You'll receive an `ACCOUNT_STATUS` webhook showing the updated balance.
### Step 2: Create external bank account
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"customerId": "Customer:sandbox001",
"currency": "USD",
"platformAccountId": "test_bank_001",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456001",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Test Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Alice Test",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Test Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}'
```
In sandbox, you can use special account number patterns to test different scenarios. The **last 3 digits** determine the behavior: **002** (insufficient funds), **003** (account closed), **004** (transfer rejected), **005** (timeout/delayed failure). Any other ending succeeds normally. See "Testing transfer failures" below for details.
### Step 3: Create and execute off-ramp quote
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"source": {
"accountId": "InternalAccount:sandbox_btc001"
},
"destination": {
"accountId": "ExternalAccount:sandbox_bank001",
"currency": "USD"
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 5000000,
"description": "Test off-ramp conversion"
}'
```
Then execute the quote:
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/execute' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET"
```
In sandbox, off-ramp conversions complete instantly. In production, bank
settlement may take 1-3 business days.
## Testing transfer failures
### External account test patterns
When creating external bank accounts in sandbox, use special account number patterns to simulate different transfer failure scenarios. The **last 3 digits** of the account number determine the test behavior:
| Last Digits | Behavior | Use Case |
| ------------- | ----------------------- | ---------------------------------------------------------------- |
| **002** | Insufficient funds | Simulates bank account with insufficient balance |
| **003** | Account closed/invalid | Simulates closed or non-existent account |
| **004** | Transfer rejected | Simulates bank rejecting the transfer (compliance, limits, etc.) |
| **005** | Timeout/delayed failure | Transaction stays pending \~30s, then fails |
| **Any other** | Success | All transfers complete normally |
**Example - Testing Insufficient Funds:**
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"customerId": "Customer:sandbox001",
"currency": "USD",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "000000002", // Will trigger insufficient funds
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Test User"
}
}
}'
```
When you create an off-ramp quote to this account and execute it, the transaction will fail immediately with an insufficient funds error.
These patterns work for all account types: US account numbers, IBANs, CLABEs, etc. Just ensure the identifier ends with the appropriate
test digits. For scenarios like PIX and UPI, where there's a domain part involved, append the test digits to the user name part.
For example, if testing email addresses as a PIX key, the full identifier would be "[testuser.002@pix.com.br](mailto:testuser.002@pix.com.br)" to trigger the
insufficient funds scenario.
## Test scenarios
### Successful conversions
The complete on-ramp and off-ramp flows described in the sections above demonstrate successful conversion scenarios. For quick reference:
**On-ramp test (USD → BTC):**
1. Create customer and quote with payment instructions
2. Use `/sandbox/send` to simulate funding
3. Verify completion via webhook
**Off-ramp test (BTC → USD):**
1. Fund BTC internal account with `/sandbox/internal-accounts/{accountId}/fund`
2. Create external bank account (use default account number for success)
3. Create and execute quote
4. Verify completion via webhook
### Failed conversions
Test error scenarios systematically using the magic account patterns:
**1. Test external account insufficient funds (002):**
```bash theme={null}
# Create account with insufficient funds pattern
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"customerId": "Customer:sandbox001",
"currency": "USD",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "000000002",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Test User"
}
}
}'
# Attempt off-ramp to this account - will fail immediately
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/execute' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET"
# Response: 400 Bad Request with insufficient funds error
```
**2. Test account closed (003):**
```bash theme={null}
# Create account with closed pattern
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-d '{"accountNumber": "000000003", ...}'
# Attempt to use - will fail with account closed error
```
**3. Test insufficient balance in internal account:**
```bash theme={null}
# Create quote from empty internal account
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"source": {
"accountId": "InternalAccount:empty_btc"
},
"destination": {
"accountId": "ExternalAccount:bank001",
"currency": "USD"
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000000
}'
# Execute will fail with insufficient balance error
```
**4. Test invalid wallet address:**
```bash theme={null}
# Attempt quote with invalid Spark address
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
-u "$SANDBOX_API_KEY:$SANDBOX_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"destination": {
"externalAccountDetails": {
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "invalid_address"
}
}
}
}'
# Response: 400 Bad Request with validation error
```
## Moving to Production
When you're ready to move to production:
1. Generate production API tokens in the dashboard
2. Swap those credentials for the sandbox credentials in your environment variables
3. Remove any sandbox-specific test patterns from your code
4. Configure production webhook endpoints
5. Test with small amounts first
## Next steps
* [Webhooks](/ramps/platform-tools/webhooks) - Handle real-time notifications
* [Fiat-to-Crypto Conversion](/ramps/conversion-flows/fiat-crypto-conversion) - Implement production flows
* [Self-Custody Wallets](/ramps/conversion-flows/self-custody-wallets) - Advanced wallet integration
* [Platform Configuration](/ramps/onboarding/platform-configuration) - Configure production settings
* [API Reference](/api-reference) - Complete API documentation
# Webhooks
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/platform-tools/webhooks
Receive real-time notifications for ramp conversions, account updates, and transaction status
Webhooks provide real-time notifications about ramp operations, allowing you to respond immediately to conversion completions, account balance changes, and transaction status updates.
## Webhook events for ramps
Grid sends webhooks for key events in the ramp lifecycle:
### Conversion events
Sent when a conversion (on-ramp or off-ramp) completes, fails, or changes status.
```json theme={null}
{
"transaction": {
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000025",
"status": "COMPLETED",
"type": "OUTGOING",
"sentAmount": {
"amount": 10000,
"currency": { "code": "USD", "decimals": 2 }
},
"receivedAmount": {
"amount": 95000,
"currency": { "code": "BTC", "decimals": 8 }
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"settledAt": "2025-10-03T15:02:30Z",
"exchangeRate": 9.5,
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006"
},
"timestamp": "2025-10-03T15:03:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000030",
"type": "OUTGOING_PAYMENT"
}
```
Use this webhook to update your UI, credit customer accounts, and trigger post-conversion workflows.
Sent when internal account balances change (deposits, conversions, withdrawals).
```json theme={null}
{
"accountId": "InternalAccount:btc456",
"oldBalance": {
"amount": 10000000,
"currency": { "code": "BTC", "decimals": 8 }
},
"newBalance": {
"amount": 5000000,
"currency": { "code": "BTC", "decimals": 8 }
},
"timestamp": "2025-10-03T15:03:00Z",
"webhookId": "Webhook:webhook001",
"type": "ACCOUNT_STATUS"
}
```
Critical for tracking crypto deposits (for off-ramps) and fiat balance changes.
Sent when customer KYC verification completes (required before conversions).
```json theme={null}
{
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
"type": "KYC_STATUS",
"timestamp": "2025-10-03T14:32:00Z",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"kycStatus": "APPROVED",
"platformCustomerId": "user_12345"
}
```
Enable ramp access immediately when KYC status changes to `APPROVED`.
## Webhook configuration
Configure your webhook endpoint in the Grid dashboard or via API:
```bash theme={null}
curl -X PATCH 'https://api.lightspark.com/grid/2025-10-13/config' \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
-H 'Content-Type: application/json' \
-d '{
"webhookEndpoint": "https://api.yourapp.com/webhooks/grid"
}'
```
Your webhook endpoint must be publicly accessible over HTTPS and respond
within 30 seconds.
## Webhook verification
Always verify webhook signatures to ensure authenticity:
### Verification steps
Get the `X-Grid-Signature` header from the webhook request.
```javascript theme={null}
const signature = req.headers['x-grid-signature'];
```
Retrieve the Grid public key from your dashboard (provided during onboarding).
`javascript const publicKey = process.env.GRID_PUBLIC_KEY; `
Use the public key to verify the signature against the raw request body.
```javascript theme={null}
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, publicKey) {
const verify = crypto.createVerify('SHA256');
verify.update(payload);
verify.end();
return verify.verify(publicKey, signature, 'base64');
}
// In your webhook handler
const rawBody = JSON.stringify(req.body);
const isValid = verifyWebhookSignature(rawBody, signature, publicKey);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
```
The signature is created using secp256r1 (P-256) asymmetric cryptography with
SHA-256 hashing.
## Handling ramp webhooks
### On-ramp completion
Handle successful fiat-to-crypto conversions:
```javascript theme={null}
app.post("/webhooks/grid", async (req, res) => {
// Verify signature
if (!verifySignature(req.body, req.headers["x-grid-signature"])) {
return res.status(401).end();
}
const { type, transaction } = req.body;
if (type === "OUTGOING_PAYMENT" && transaction.status === "COMPLETED") {
// On-ramp completed (USD → BTC)
if (
transaction.sentAmount.currency.code === "USD" &&
transaction.receivedAmount.currency.code === "BTC"
) {
// Update user's transaction history
await db.transactions.create({
userId: transaction.customerId,
type: "ON_RAMP",
amountUsd: transaction.sentAmount.amount,
amountBtc: transaction.receivedAmount.amount,
rate: transaction.exchangeRate,
status: "COMPLETED",
completedAt: new Date(transaction.settledAt),
});
// Notify user
await sendNotification(transaction.customerId, {
title: "Bitcoin purchased!",
message: `You received ${formatBtc(
transaction.receivedAmount.amount
)} BTC`,
});
}
}
res.status(200).json({ received: true });
});
```
### Off-ramp completion
Handle successful crypto-to-fiat conversions:
```javascript theme={null}
if (type === "OUTGOING_PAYMENT" && transaction.status === "COMPLETED") {
// Off-ramp completed (BTC → USD)
if (
transaction.sentAmount.currency.code === "BTC" &&
transaction.receivedAmount.currency.code === "USD"
) {
// Update user's balance and transaction history
await db.transactions.create({
userId: transaction.customerId,
type: "OFF_RAMP",
amountBtc: transaction.sentAmount.amount,
amountUsd: transaction.receivedAmount.amount,
rate: transaction.exchangeRate,
status: "COMPLETED",
bankAccountId: transaction.destination.accountId,
completedAt: new Date(transaction.settledAt),
});
// Notify user
await sendNotification(transaction.customerId, {
title: "Cash out completed!",
message: `$${formatUsd(
transaction.receivedAmount.amount
)} sent to your bank account`,
});
}
}
```
### Balance updates
Track crypto deposits for off-ramp liquidity:
```javascript theme={null}
if (type === "ACCOUNT_STATUS") {
const { accountId, newBalance, oldBalance } = req.body;
// Crypto deposit detected
if (
newBalance.currency.code === "BTC" &&
newBalance.amount > oldBalance.amount
) {
const depositAmount = newBalance.amount - oldBalance.amount;
// Record deposit
await db.deposits.create({
accountId,
currency: "BTC",
amount: depositAmount,
newBalance: newBalance.amount,
});
// Check if user has pending off-ramp
const pendingOffRamp = await db.offRamps.findPending(accountId);
if (pendingOffRamp && newBalance.amount >= pendingOffRamp.requiredAmount) {
// Auto-execute pending off-ramp
await executeOffRamp(pendingOffRamp.id);
}
}
}
```
## Best practices
Handle duplicate webhooks gracefully using webhook IDs:
```javascript theme={null}
const { webhookId } = req.body;
// Check if already processed
const existing = await db.webhooks.findUnique({ where: { webhookId } });
if (existing) {
return res.status(200).json({ received: true });
}
// Process webhook
await processRampWebhook(req.body);
// Record webhook ID
await db.webhooks.create({
data: { webhookId, processedAt: new Date() }
});
```
Transaction IDs are unique identifiers for conversions:
```javascript theme={null}
const { transaction } = req.body;
// Upsert transaction (handles duplicates)
await db.transactions.upsert({
where: { gridTransactionId: transaction.id },
update: { status: transaction.status },
create: {
gridTransactionId: transaction.id,
customerId: transaction.customerId,
status: transaction.status,
// ... other fields
},
});
```
Grid retries failed webhooks with exponential backoff. Ensure your endpoint can handle retries:
```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);
// Return 5xx for retryable errors
if (error.retryable) {
res.status(503).json({ error: 'Temporary failure' });
} else {
// Return 200 for non-retryable to prevent retries
res.status(200).json({ error: error.message });
}
}
});
```
Track webhook delivery and processing:
```javascript theme={null}
// Log webhook metrics
await metrics.increment('webhooks.received', {
type: req.body.type,
status: req.body.transaction?.status,
});
// Track processing time
const start = Date.now();
await processWebhook(req.body);
const duration = Date.now() - start;
await metrics.histogram('webhooks.processing_time', duration, {
type: req.body.type,
});
```
## Testing webhooks
Test webhook handling using the test endpoint:
```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/webhooks/test' \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
This sends a test webhook to your configured endpoint:
```json theme={null}
{
"test": true,
"timestamp": "2025-10-03T14:32:00Z",
"webhookId": "Webhook:test001",
"type": "TEST"
}
```
Verify your endpoint receives the test webhook and responds with a 200 status
code.
## Next steps
* [Sandbox Testing](/ramps/platform-tools/sandbox-testing) - Test ramp flows end-to-end
* [Platform Configuration](/ramps/onboarding/platform-configuration) - Configure webhook endpoint
* [Fiat-to-Crypto Conversion](/ramps/conversion-flows/fiat-crypto-conversion) - Implement conversion flows
* [API Reference](/api-reference) - Complete webhook documentation
# Quickstart
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/quickstart
Complete guide for converting fiat to crypto (on-ramp) and delivering Bitcoin to a Spark wallet
This guide walks you through the complete process of converting USD to Bitcoin and sending it to a Spark wallet using just-in-time (JIT) funding.
## Prerequisites
Before starting this guide, ensure you have:
* A Grid API account with valid authentication credentials
* Access to the Grid API endpoints (production or sandbox)
* A webhook endpoint configured to receive notifications
* A Spark wallet address where the Bitcoin will be delivered
## Overview
The on-ramp process consists of the following steps:
1. **Create a customer** via the API
2. **Create a quote** for the USD-to-BTC conversion with current exchange rate
3. **Fund the quote** using the provided payment instructions (JIT funding)
4. **Receive webhook notification** confirming Bitcoin delivery to the Spark wallet
***
## Step 1: Customer Onboarding
If your platform is a regulated financial institution that already has a KYC/KYB process in place,
you can create a customer directly via the API. However, if your platform is not regulated, you must use the hosted KYC/KYB
link flow to onboard your customers.
**Regulated platforms** have lighter KYC requirements since they handle compliance verification internally.
The KYC/KYB flow allows you to onboard customers through direct API calls.
Regulated financial institutions can:
* **Direct API Onboarding**: Create customers directly via API calls with minimal verification
* **Internal KYC/KYB**: Handle identity verification through your own compliance systems
* **Reduced Documentation**: Only provide essential customer information required by your payment counterparty or service provider.
* **Faster Onboarding**: Streamlined process for known, verified customers
#### Creating Customers via Direct API
For regulated platforms, you can create customers directly through the API without requiring external KYC verification:
To register a new customer in the system, use the `POST /customers` endpoint:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/customers" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"platformCustomerId": "customer_12345",
"customerType": "INDIVIDUAL",
"fullName": "Jane Doe",
"birthDate": "1992-03-25",
"nationality": "US",
"address": {
"line1": "123 Pine Street",
"city": "Seattle",
"state": "WA",
"postalCode": "98101",
"country": "US"
}
}'
```
The examples below show a more comprehensive set of data. Not all fields are strictly required by the API for customer creation itself, but become necessary based on currency and UMA provider requirements if using UMA.
```json theme={null}
{
"platformCustomerId": "9f84e0c2a72c4fa",
"customerType": "INDIVIDUAL",
"fullName": "John Sender",
"birthDate": "1985-06-15",
"address": {
"line1": "Paseo de la Reforma 222",
"line2": "Piso 15",
"city": "Ciudad de México",
"state": "Ciudad de México",
"postalCode": "06600",
"country": "MX"
}
}
```
```json theme={null}
{
"platformCustomerId": "b87d2e4a9c13f5b",
"customerType": "BUSINESS",
"businessInfo": {
"legalName": "Acme Corporation",
"registrationNumber": "789012345",
"taxId": "123-45-6789"
},
"address": {
"line1": "456 Oak Avenue",
"line2": "Floor 12",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}
```
**Unregulated platforms** require full KYC/KYB verification of customers through hosted flows.
Unregulated platforms must:
* **Hosted KYC Flow**: Use the hosted KYC link for complete identity verification
* **Extended Review**: Customers may require manual review and approval in some cases
### Hosted KYC Link Flow
The hosted KYC flow provides a secure, hosted interface where customers can complete their identity verification and onboarding process.
#### Generate KYC Link
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/customers/kyc-link?redirectUri=https://yourapp.com/onboarding-complete&platformCustomerId=019542f5-b3e7-1d02-0000-000000000001" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
**Response:**
```json theme={null}
{
"kycUrl": "https://kyc.lightspark.com/onboard/abc123def456",
"platformCustomerId": "019542f5-b3e7-1d02-0000-000000000001"
}
```
#### Complete KYC Process
Call the `/customers/kyc-link` endpoint with your `redirectUri` parameter to generate a hosted KYC URL for your customer.
The `redirectUri` parameter is embedded in the generated KYC URL and will be used to automatically redirect the customer back to your application after they complete verification.
Redirect your customer to the returned `kycUrl` where they can complete their identity verification in the hosted interface.
The KYC link is single-use and expires after a limited time period for security.
The customer completes the identity verification process in the hosted KYC interface, providing required documents and information.
The hosted interface handles document collection, verification checks, and compliance requirements automatically.
After verification processing, you'll receive a KYC status webhook notification indicating the final verification result.
Upon successful KYC completion, the customer is automatically redirected to your specified `redirectUri` URL.
The customer account will be automatically created by the system upon successful KYC completion. You can identify the new customer using your `platformCustomerId` or other identifiers.
On your redirect page, handle the completed KYC flow and integrate the new customer into your application.
## Step 2: Create a Quote for Fiat-to-Crypto Conversion
Create a quote to convert USD to Bitcoin and deliver it to a Spark wallet. The quote will provide the current exchange rate and payment instructions for funding.
### Request
```bash theme={null}
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": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "USD"
},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000,
"description": "On-ramp: Buy $100 of Bitcoin"
}'
```
**Combined External Account Creation**: The `externalAccountDetails` option
automatically creates the external account (Spark wallet) and uses it as the
destination for the Bitcoin transfer in a single API call. If you want to reuse
the same external accounts for many quotes, you can add them using the
`/external-accounts` endpoint, and then use the `accountId` in the quote creation request.
### Response
```json theme={null}
{
"id": "Quote:019542f5-b3e7-1d02-0000-000000000006",
"status": "PENDING",
"createdAt": "2025-10-03T15:00:00Z",
"expiresAt": "2025-10-03T15:05:00Z",
"source": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456",
"currency": "BTC"
},
"sendingCurrency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
},
"receivingCurrency": {
"code": "BTC",
"name": "Bitcoin",
"symbol": "₿",
"decimals": 8
},
"totalSendingAmount": 10000,
"totalReceivingAmount": 83333,
"exchangeRate": 8.3333,
"feesIncluded": 250,
"paymentInstructions": [
{
"instructionsNotes": "Include reference code in transfer memo",
"accountOrWalletInfo": {
"reference": "RAMP-ABC123",
"accountType": "US_ACCOUNT",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO Customer",
"bankName": "JP Morgan Chase"
}
},
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
]
}
```
The quote shows:
* **Sending**: \$100.00 USD (including \$2.50 fee)
* **Receiving**: 0.00083333 BTC (83,333 satoshis)
* **Exchange rate**: 8.3333 sats per USD cent (\~\$120,000 per BTC)
* **Quote expires**: In 5 minutes
* **Payment instructions**: Bank account details and reference code for funding
For JIT-funded quotes, do NOT call the `/quotes/{quoteId}/execute` endpoint.
Simply fund using the payment instructions, and Grid will automatically
execute the conversion upon receiving your payment. The execute endpoint is
used for quotes with an internal account or pullable external account as the source.
***
## Step 3: Fund the Quote (Just-in-Time)
In production, you would initiate a real-time push payment (ACH, RTP, wire, etc.) to the bank account provided in `paymentInstructions`, making sure to include the exact reference code `RAMP-ABC123` in the transfer memo.
In Sandbox, you can simulate funding using the `/sandbox/send` endpoint:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/sandbox/send" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"reference": "RAMP-ABC123",
"currencyCode": "USD",
"currencyAmount": 10000
}'
```
**Response:**
```json theme={null}
{
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000025",
"status": "PROCESSING",
"type": "OUTGOING",
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006"
}
```
In production, your customer or platform would initiate a bank transfer to the provided account details, include the reference code `RAMP-ABC123` in the transfer memo, and wait for Grid to detect the incoming payment (typically 1-3 business days for ACH, instant for RTP/wire). You'll receive a webhook notification when Bitcoin is delivered to the Spark wallet.
The reference code is critical for matching your payment to the quote. Always
include it exactly as provided in the payment instructions.
***
## Step 4: Receive Completion Webhook
Once Grid receives your payment and completes the USD-to-BTC conversion and delivery, you'll receive a webhook notification:
```json theme={null}
{
"transaction": {
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000025",
"status": "COMPLETED",
"type": "OUTGOING",
"sentAmount": {
"amount": 10000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"receivedAmount": {
"amount": 83333,
"currency": {
"code": "BTC",
"name": "Bitcoin",
"symbol": "₿",
"decimals": 8
}
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"settledAt": "2025-10-03T15:02:30Z",
"createdAt": "2025-10-03T15:00:00Z",
"description": "On-ramp: Buy $100 of Bitcoin",
"exchangeRate": 8.3333,
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006",
"paymentInstructions": [
{
"instructionsNotes": "Include reference code in transfer memo",
"accountOrWalletInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "1234567890",
"routingNumber": "021000021",
"bankName": "Chase Bank",
"referenceCode": "REF123456"
}
}
]
},
"timestamp": "2025-10-03T15:03:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000030",
"type": "OUTGOING_PAYMENT"
}
```
The customer now has 83,333 satoshis (0.00083333 BTC) in their Spark wallet!
***
## Summary
You've successfully completed a fiat-to-crypto on-ramp! Here's what happened:
1. ✅ Created a customer via API
2. ✅ Created a quote with exchange rate and payment instructions
3. ✅ Funded the quote using JIT payment (simulated in sandbox)
4. ✅ Bitcoin automatically converted and delivered to Spark wallet
## Next Steps
* **Off-ramps**: Learn how to convert crypto to fiat in the [Crypto-to-Fiat guide](/ramps/conversion-flows/fiat-crypto-conversion)
* **Self-custody wallets**: Explore advanced wallet integration in the [Self-Custody Wallets guide](/ramps/conversion-flows/self-custody-wallets)
* **Webhook verification**: Implement signature verification for security (see [Webhooks guide](/ramps/platform-tools/webhooks))
* **Sandbox testing**: Learn more about testing in the [Sandbox Testing guide](/ramps/platform-tools/sandbox-testing)
## Related Resources
* [API Reference](/api-reference) - Complete API documentation
* [Implementation Overview](/ramps/onboarding/implementation-overview) - High-level architecture and flow
* [Platform Configuration](/ramps/onboarding/platform-configuration) - Configure your platform settings
# Core Concepts
Source: https://ramps-feat-building-with-ai.mintlify.app/ramps/terminology
Core concepts and terminology for the Grid API
There are several key entities in the Grid API: **Platform**, **Customers**, **Internal Accounts**, **External Accounts**, **Quotes**, **Transactions**, and **UMA Addresses**.
## Businesses, People, and Accounts
### Platform
Your **platform** is you! It's the top-level entity that integrates with the Grid API. The platform:
* Has its own configuration (webhook endpoint, supported currencies, API tokens, etc.)
* A platform can have many customers both business and individual
* Manages multiple customers and their accounts
* Can hold platform-owned internal accounts for settlement and liquidity management
* Acts as the integration point between your application and the open Money Grid
### Customers
**Customers** are your end users who send and receive payments through your platform. Each customer:
* Can be an individual or business entity
* Has a KYC/KYB status that determines their ability to transact. If you are a regulated financial institution, this will typically be `APPROVED` since you do the KYC/KYB yourself.
* Is identified by both a system-generated ID and optionally your platform-specific customer ID
* May have associated internal accounts and external accounts
* May have a unique **UMA address** (e.g., `$john.doe@yourdomain.com`). If you don't assign an UMA address when creating a customer, they will be assigned a system-generated one.
### Internal Accounts
**Internal accounts** are Grid-managed accounts that hold balances in specific currencies. They can belong to either:
* **Platform internal accounts** - Owned by the platform for settlement, liquidity, and float management
* **Customer internal accounts** - Associated with specific customers for holding funds
Internal accounts:
* Have balances in a single currency (USD, EUR, MXN, etc.)
* Can be funded via bank transfers or crypto deposits using payment instructions
* Are used as sources or destinations for transactions instantly 24/7/365
* Track available balance for sending payments or receiving funds
### External Accounts
**External accounts** are traditional bank accounts, crypto wallets, or other payment instruments connected to customers
for on-ramping or off-ramping funds. Each external account:
* Are associated with a specific customer or the platform
* Represents a real-world bank account (with routing number, account number, IBAN, etc.), wallet, or payment instrument
* Has an associated beneficiary (individual or business) who receives payments from the customer or platform
* Has a status indicating screening status (ACTIVE, PENDING, INACTIVE, etc.)
* Can be used as a destination for quote-based transfers or same currency transfers like withdrawals
* For pullable sources like debit cards or ACH pulls, an external account can be used as a source for transfers-in to
fund internal accounts or to fund cross-border transfers via quotes.
## Entity Examples by Use Case
Understanding how entities map to your specific use case helps clarify your integration architecture. Here are common examples:
### B2B Payouts Platform (e.g., Bill.com, Routable)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------- | ------------------------------------------ |
| **Platform** | The payouts platform itself | Your company providing AP automation |
| **Customer** | Businesses sending payments to vendors | Acme Corp (your client company) |
| **External Account** | Vendors/suppliers receiving payments | Office supply vendor, freelance contractor |
**Flow**: Acme Corp (customer) uses your platform to pay their vendor invoices → funds move from Acme's internal account → to vendor's external bank account
### Direct Rewards Platform (Platform-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | ------------------------------------------- | ----------------------------------- |
| **Platform** | The app paying rewards directly to users | Your cashback app |
| **Customer** | (Not used in this model) | N/A |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Your platform sends micro-payouts directly from platform internal accounts → to users' external crypto wallets at scale. Common for cashback apps where the platform earns affiliate commissions and shares them with users.
### White-Label Rewards Platform (Customer-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------------- | ----------------------------------- |
| **Platform** | The rewards infrastructure provider | Your white-label rewards API |
| **Customer** | Brands or merchants running reward campaigns | Nike, Starbucks |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Nike (customer) funds their internal account → your platform sends rewards on their behalf → to users' external crypto wallets. Common for brand loyalty programs where merchants manage their own reward budgets.
### Remittance/P2P App (e.g., Wise, Remitly)
| Entity Type | Who They Are | Example |
| -------------------- | ----------------------------------- | ---------------------------------------------------------------- |
| **Platform** | The remittance service | Your money transfer app |
| **Customer** | Both sender and recipient of funds | Maria (sender in US), Juan (recipient in Mexico) |
| **External Account** | Bank accounts for funding/receiving | Maria's US bank (funding), Juan's Mexican bank (receiving funds) |
**Flow**: Maria (customer) funds transfer from her external account → to Juan (also a customer) → who receives funds in his external bank account. Alternatively, Maria could send to Juan's UMA address directly.
## Transactions and Addressing Entities
### Quotes
**Quotes** provide locked-in exchange rates and payment instructions for transfers. A quote:
* Specifies a source (internal account, customer ID, or the platform itself) and destination (internal/external account or UMA address)
* Locks an exchange rate for a short period (typically 1-5 minutes) or can be immediately executed with the `immediatelyExecute` flag
* Calculates total fees and amounts for currency conversion
* Provides payment instructions for funding the transfer if needed, or can be funded via an internal account balance.
* Must be executed before it expires
* Creates a transaction when executed
### Transactions
**Transactions** represent completed or in-progress payment transfers. Each transaction:
* Has a type (INCOMING or OUTGOING from the platform's perspective)
* Has a status (PENDING, COMPLETED, FAILED, etc.)
* References a customer (sender for outgoing, recipient for incoming) or a platform internal account
* Specifies source and destination (accounts or UMA addresses)
* Includes amounts, currencies, and settlement information
* May include counterparty information for compliance purposes if required by your platform configuration
Transactions are created when:
* A quote is executed (either incoming or outgoing)
* A same currency transfer is initiated (transfer-in or transfer-out)
### UMA Addresses (optional)
**UMA addresses** are human-readable payment identifiers that follow the format `$username@domain.com`. They:
* Uniquely identify entities on the Grid network
* Enable sending and receiving payments across different platforms without knowing the recipient's underlying account details or personal information
* Support currency negotiation and cross-border transfers
* Work similar to email addresses but for payments
* Are an optional UX improvement for some use cases. Use of UMA addresses is not required in order to use the Grid API.
# Paying out Bitcoin rewards
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/developer-guides/distributing-rewards
Send Bitcoin rewards to customers using the Grid API
This guide covers how to distribute Bitcoin rewards to your customers, including quote creation, execution options, and tracking delivery.
## Overview
Distributing Bitcoin rewards involves creating a quote that converts your fiat balance (typically USD) to Bitcoin and sends it to the customer's wallet. You have flexibility in how you lock amounts, register destinations, and execute transfers.
## Basic Flow
1. **Create a quote** - Specify source account, destination, and amount
2. **Execute the quote** - Either immediately or after review
3. **Monitor completion** - Track via webhooks or polling
## Finding your platform's internal account
In this guide, we'll use the platform's USD internal account as the funding source for the rewards.
You can find your platform's internal account by listing all internal accounts for your platform.
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Response:
```json theme={null}
{
"data": [
{
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"balance": {
"amount": 10000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"fundingPaymentInstructions": [...],
"createdAt": "2025-10-03T12:00:00Z",
"updatedAt": "2025-10-03T12:00:00Z"
}
]
}
```
## Creating a Quote
The core request specifies your platform's internal account as the source and the customer's wallet as the destination.
```bash theme={null}
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": {
"externalAccountDetails": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100,
"immediatelyExecute": true,
"description": "Weekly reward payout"
}'
```
Response:
```json theme={null}
{
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000020",
"status": "PROCESSING",
"createdAt": "2025-10-03T15:00:00Z",
"expiresAt": "2025-10-03T15:05:00Z",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456",
"currency": "BTC"
},
"sendingCurrency": {
"code": "USD",
"decimals": 2
},
"receivingCurrency": {
"code": "BTC",
"decimals": 8
},
"totalSendingAmount": 100,
"totalReceivingAmount": 810,
"exchangeRate": 8.1,
"feesIncluded": 5,
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000025"
}
```
## Locking Amount: Sending vs. Receiving
When creating a quote, you can choose to either lock the amount you're sending (fiat) or the amount the customer receives (Bitcoin).
### Lock Sending Amount
Use this when you want to send a fixed dollar amount (e.g., \$1.00 reward).
```json theme={null}
{
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100 // $1.00 in cents
}
```
The customer receives whatever Bitcoin this amount buys at the current rate.
### Lock Receiving Amount
Use this when you want the receiver to receive a specific Bitcoin amount (e.g., 1000 sats).
```json theme={null}
{
"lockedCurrencySide": "RECEIVING",
"lockedCurrencyAmount": 1000 // 1000 satoshis
}
```
Your platform account is debited whatever fiat amount is needed to send that Bitcoin amount.
For consistent dollar-value rewards, use `SENDING`. For consistent
Bitcoin-value rewards, use `RECEIVING`.
## Execution Options
### Immediate Execution (Market Order)
Set `immediatelyExecute: true` to create and execute the quote in one step. This is ideal for automated reward distribution where you accept the current market rate.
```json theme={null}
{
"immediatelyExecute": true
}
```
The quote is created and executed immediately. You receive a `transactionId` in the response.
### Two-Step Execution (Review Before Sending)
Omit `immediatelyExecute` or set it to `false` to review the quote before executing.
```json theme={null}
{
"immediatelyExecute": false // or omit this field
}
```
The response will be the same as the immediate execution response, but the status will be `PENDING` and you'll have until the quote's `expiresAt` timestamp to execute the quote.
After reviewing the quote's exchange rate and fees, execute it:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/execute" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Quotes expire after a short time (typically 5 minutes). You must execute
before expiration.
## Destination Options
### Inline External Account Creation
Use `externalAccountDetails` to create the destination wallet on the fly. This is perfect for one-time or infrequent payouts.
```json theme={null}
{
"destination": {
"externalAccountDetails": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
}
}
```
The external account is created automatically and returned in the quote response.
### Pre-Registered External Account
If you've already registered the external account, reference it by ID. This is more efficient for recurring rewards to the same wallets.
First, create the external account:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/customers/external-accounts" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}'
```
Then reference it in `/quotes`:
```json theme={null}
{
"destination": {
"accountId": "ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456"
}
}
```
Pre-register external accounts for customers who receive regular rewards. This
avoids duplicate account creation and improves performance.
## Tracking Delivery
### Webhook Notification
When the Bitcoin transfer completes, you'll receive a webhook:
```json theme={null}
{
"transaction": {
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000025",
"status": "COMPLETED",
"type": "OUTGOING",
"sentAmount": {
"amount": 100,
"currency": { "code": "USD" }
},
"receivedAmount": {
"amount": 810,
"currency": { "code": "BTC" }
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"settledAt": "2025-10-03T15:01:45Z"
},
"type": "OUTGOING_PAYMENT"
}
```
### Polling Status
Alternatively, poll the quote or transaction endpoint:
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Or:
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/transactions/{transactionId}" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
## Common Patterns
### Fixed Dollar Rewards (e.g., \$1.00 per action)
```json theme={null}
{
"source": { "accountId": "InternalAccount:..." },
"destination": { "externalAccountDetails": { ... } },
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100,
"immediatelyExecute": true
}
```
### Fixed Satoshi Rewards (e.g., 1000 sats per action)
```json theme={null}
{
"source": { "accountId": "InternalAccount:..." },
"destination": { "accountId": "ExternalAccount:..." },
"lockedCurrencySide": "RECEIVING",
"lockedCurrencyAmount": 1000,
"immediatelyExecute": true
}
```
### Review Before Sending
```json theme={null}
{
"source": { "accountId": "InternalAccount:..." },
"destination": { "accountId": "ExternalAccount:..." },
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 5000,
"immediatelyExecute": false
}
```
Then review the exchange rate and fees, and execute:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/execute" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
## Best Practices
Use `immediatelyExecute: true` for automated, small-dollar rewards where you
accept market rates.
Pre-register external accounts for customers receiving recurring rewards to
improve performance.
Set up webhook handlers to track completion status and update your reward
records.
Ensure your platform's internal account has sufficient balance before
distributing rewards. Monitor balances via the `/platform/internal-accounts`
endpoint or account status webhooks.
## Related Resources
* [Quick Start Guide](/rewards/quickstart) - End-to-end Bitcoin rewards walkthrough
* [Configuring Customers](/rewards/developer-guides/configuring-customers) - Customer creation and management
* [API Reference: Quotes](/api-reference/quotes/create-a-transfer-quote) - Complete quote API documentation
* [Handling Webhooks](/rewards/platform-tools/webhooks) - Webhook security and implementation
# External Accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/developer-guides/external-accounts
Add and manage external funding sources and wallets as payment destinations for rewards
External accounts are bank accounts, cryptocurrency wallets, or payment destinations outside Grid where you can send funds. Grid supports two types:
* **Customer external accounts** - Scoped to individual customers, used for withdrawals and customer-specific payouts
* **Platform external accounts** - Scoped to your platform, used for platform-wide operations like receiving funds from external sources
Customer external accounts often require some basic beneficiary information for compliance.
Platform accounts are managed at the organization level.
## Create external accounts by region or wallet
**ACH, Wire, RTP**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}'
```
Category must be `CHECKING` or `SAVINGS`. Routing number must be 9 digits.
**CLABE/SPEI**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "MXN",
"platformAccountId": "mx_beneficiary_001",
"accountInfo": {
"accountType": "CLABE",
"clabeNumber": "123456789012345678",
"bankName": "BBVA Mexico",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "María García",
"birthDate": "1985-03-15",
"nationality": "MX",
"address": {
"line1": "Av. Reforma 123",
"city": "Ciudad de México",
"state": "CDMX",
"postalCode": "06600",
"country": "MX"
}
}
}
}'
```
**PIX**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "BRL",
"platformAccountId": "br_pix_001",
"accountInfo": {
"accountType": "PIX",
"pixKey": "user@email.com",
"pixKeyType": "EMAIL",
"bankName": "Nubank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "João Silva",
"birthDate": "1988-07-22",
"nationality": "BR",
"address": {
"line1": "Rua das Flores 456",
"city": "São Paulo",
"state": "SP",
"postalCode": "01234-567",
"country": "BR"
}
}
}
}'
```
Key types: `CPF`, `CNPJ`, `EMAIL`, `PHONE`, or `RANDOM`
**IBAN/SEPA**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "EUR",
"platformAccountId": "eu_iban_001",
"accountInfo": {
"accountType": "IBAN",
"iban": "DE89370400440532013000",
"swiftBic": "DEUTDEFF",
"bankName": "Deutsche Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Hans Schmidt",
"birthDate": "1982-11-08",
"nationality": "DE",
"address": {
"line1": "Hauptstraße 789",
"city": "Berlin",
"state": "Berlin",
"postalCode": "10115",
"country": "DE"
}
}
}
}'
```
**UPI**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "INR",
"platformAccountId": "in_upi_001",
"accountInfo": {
"accountType": "UPI",
"vpa": "user@okbank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "Priya Sharma",
"birthDate": "1991-05-14",
"nationality": "IN",
"address": {
"line1": "123 MG Road",
"city": "Mumbai",
"state": "Maharashtra",
"postalCode": "400001",
"country": "IN"
}
}
}
}'
```
**Bitcoin Lightning (Spark Wallet)**
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"currency": "BTC",
"platformAccountId": "btc_spark_001",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}'
```
Spark wallets don't require beneficiary information as they are self-custody wallets.
Use `platformAccountId` to tie your internal id with the external account.
**Sample Response:**
```json theme={null}
{
"id": "ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"status": "ACTIVE",
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}
```
### Business beneficiaries
For business accounts, include business information:
```json theme={null}
{
"currency": "USD",
"platformAccountId": "acme_corp_account",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "987654321",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "BUSINESS",
"businessInfo": {
"legalName": "Acme Corporation, Inc.",
"taxId": "EIN-987654321"
},
"address": {
"line1": "456 Business Ave",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}
}
}
```
## Account status
Beneficiary data may be reviewed for risk and compliance. Only `ACTIVE` accounts can receive payments. Updates to account data may trigger account re-review.
| Status | Description |
| -------------- | ----------------------------------- |
| `PENDING` | Created, awaiting verification |
| `ACTIVE` | Verified and ready for transactions |
| `UNDER_REVIEW` | Additional review required |
| `INACTIVE` | Disabled, cannot be used |
## Listing external accounts
### List customer accounts
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
### List platform accounts
For platform-wide operations, list all platform-level external accounts:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/external-accounts' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Platform external accounts are used for platform-wide operations like
depositing funds from external sources.
## Best practices
Validate account details before submission:
```javascript theme={null}
// US accounts: 9-digit routing, 4-17 digit account number
if (!/^\d{9}$/.test(routingNumber)) {
throw new Error("Invalid routing number");
}
// CLABE: exactly 18 digits
if (!/^\d{18}$/.test(clabeNumber)) {
throw new Error("Invalid CLABE number");
}
```
Verify status before sending payments:
```javascript theme={null}
if (account.status !== "ACTIVE") {
throw new Error(`Account is ${account.status}, cannot process payment`);
}
```
Never expose full account numbers. Display only masked info:
```javascript theme={null}
function displaySafely(account) {
return {
id: account.id,
bankName: account.accountInfo.bankName,
lastFour: account.accountInfo.accountNumber.slice(-4),
status: account.status,
};
}
```
## Next steps
Simplify external account setup with Plaid Link for instant bank verification
Learn how to pay out Bitcoin rewards using external accounts
View complete API documentation for external accounts
# Implementation Overview
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/developer-guides/implementation-overview
This page gives you a 10,000‑ft view of an end‑to‑end Bitcoin rewards implementation. It is intentionally generalized to cover the main building blocks. The detailed guides that follow provide concrete fields, edge cases, and step‑by‑step instructions.
This overview highlights the main building blocks: platform setup, funding, customer onboarding, external account creation, and Bitcoin reward distribution.
## Platform configuration
Configure your platform once before building user flows.
* Provide webhook endpoints for transaction and account status notifications
* Generate API credentials for Sandbox (and later Production)
* Configure supported currencies (typically a Fiat currency and BTC or stables for rewards)
## Platform account funding
Fund your platform's internal account to enable instant Bitcoin rewards distribution.
* **Platform internal accounts**: Automatically created for each supported currency when your platform is set up
* **Funding options**: ACH transfer, wire transfer, or crypto deposits (BTC, Stablecoins)
* **Balance management**: Monitor balances via API or webhook notifications to ensure sufficient funds for rewards distribution
For Bitcoin rewards, maintaining a prefunded USD balance allows you to instantly purchase and distribute Bitcoin to customers without delays.
## Onboarding customers
For rewards, the only entity who needs to be KYB'd is the entity paying for the reward. This can be you, the platform, or
your business customers that want to pay out rewards to their end users.
All you need in order to pay out a reward is the wallet address.
No need to collect extra personal information or go through the full hosted KYC flow for end users!
To generate a spark wallet, you can use a tool like [Privy](https://privy.io) or the Spark SDK directly.
## External account creation
Register external accounts where customers will receive their Bitcoin rewards.
* **Spark wallets**: Lightning-compatible Spark wallets for instant, low-fee transfers of Bitcoin and Stablecoins
* **Other cryptocurrency wallets**: Support for various Bitcoin destination types
* Capture wallet addresses and validate formats where applicable
Spark wallets are recommended for Bitcoin rewards as transfers complete within seconds with minimal fees.
## Distributing rewards
Send Bitcoin rewards to customers using a streamlined quote-and-execute flow.
* **Create and execute quote**: Specify source (your platform's USD account), destination (customer's Spark wallet), and amount
* **Currency conversion**: Platform handles USD to BTC conversion at current market rates
* **Immediate execution**: Use `immediatelyExecute: true` for one-step market-order style distribution
* **Monitor status**: Track completion via webhooks or polling
For recurring small-dollar rewards, use `immediatelyExecute: true` to skip the quote confirmation step and send at the current market rate.
## Reconciling transactions
Implement operational processes to keep your ledger in sync.
* Process webhooks idempotently; map statuses (pending, processing, completed, failed)
* Tie transactions back to customers and reward events
* Monitor platform account balances to ensure adequate funding
* Query for transactions by date range or customer as necessary
## Testing in Sandbox
Use Sandbox to build and validate end‑to‑end without moving real funds.
* Simulate account funding using `/sandbox/internal-accounts/{accountId}/fund` endpoint
* Test quote creation, execution, and webhook lifecycles
* Validate customer onboarding flows with test data
## Enabling Production
When you're ready to go live:
* Ensure adequate funding in your production platform account
* Confirm webhook security, monitoring, and alerting are in place
* Review rate limits, error handling, and idempotency
* Run final UAT in Sandbox, then request Production access from our team
Contact our team to enable Production and begin distributing Bitcoin rewards.
# Internal Accounts
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/developer-guides/internal-accounts
Learn how to manage and fund internal accounts for holding platform and customer funds
Internal accounts are Lightspark managed accounts that hold funds within the Grid platform. They allow you to receive deposits and send payments to external bank accounts or other payment destinations.
They are useful for holding funds on behalf or the platform or customers which will be used for instant, 24/7 quotes and transfers out of the system.
Internal accounts are created for both:
* **Platform-level accounts**: Hold pooled funds for your platform operations (rewards distribution, reconciliation, etc.)
* **Customer accounts**: Hold individual customer funds for their transactions
Internal accounts are automatically created when you onboard a customer, based
on your platform's currency configuration. Platform-level internal accounts
are created when you configure your platform with supported currencies.
## How internal accounts work
Internal accounts act as an intermediary holding account in the payment flow:
1. **Deposit funds**: You or your customers deposit money into internal accounts using bank transfers (ACH, wire, PIX, etc.) or crypto transfers
2. **Hold balance**: Funds are held securely in the internal account until needed
3. **Send payments**: You initiate transfers from internal accounts to external destinations
Each internal account:
* Is denominated in a single currency (USD, EUR, etc.)
* Has a unique balance that you can query at any time
* Includes unique payment instructions for depositing funds
* Supports multiple funding methods depending on the currency
## Retrieving internal accounts
### List customer internal accounts
To retrieve all internal accounts for a specific customer, use the customer ID to filter the results:
```bash Request internal accounts for a customer theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json theme={null}
{
"data": [
{
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"fundingPaymentInstructions": [
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"reference": "FUND-ABC123",
"accountType": "US_ACCOUNT",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
},
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
],
"createdAt": "2025-10-03T12:00:00Z",
"updatedAt": "2025-10-03T14:30:00Z"
}
],
"hasMore": false,
"totalCount": 1
}
```
### Filter by currency
You can filter internal accounts by currency to find accounts for specific denominations:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001¤cy=USD' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
### List platform internal accounts
To retrieve platform-level internal accounts (not tied to individual customers), use the platform internal accounts endpoint:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
Platform internal accounts are useful for managing pooled funds, distributing
rewards, or handling platform-level operations.
## Understanding funding payment instructions
Each internal account includes `fundingPaymentInstructions` that tell your customers how to deposit funds. The structure varies by payment rail and currency:
For USD accounts, instructions include routing and account numbers:
```json theme={null}
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"accountType": "US_ACCOUNT",
"reference": "FUND-ABC123",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
}
```
Each internal account has unique banking details in the `accountOrWalletInfo`
field, which ensures deposits are automatically credited to the correct
account.
For EUR accounts, instructions use SEPA IBAN numbers:
```json theme={null}
{
"instructionsNotes": "Include reference in SEPA transfer description",
"accountOrWalletInfo": {
"accountType": "IBAN",
"reference": "FUND-EUR789",
"iban": "DE89370400440532013000",
"swiftBic": "DEUTDEFF",
"accountHolderName": "Lightspark Payments FBO Maria Garcia",
"bankName": "Banco de México"
}
}
```
For stablecoin accounts, using a Spark wallet as the funding source:
```json theme={null}
{
"invoice": "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs",
"instructionsNotes": "Use the invoice when making Spark payment",
"accountOrWalletInfo": {
"accountType": "SPARK_WALLET",
"assetType": "USDB",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
```
For Solana wallet accounts, using a Solana wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
```
For Tron wallet accounts, using a Tron wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "TRON_WALLET",
"assetType": "USDT",
"address": "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL"
}
}
```
For Polygon wallet accounts, using a Polygon wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "POLYGON_WALLET",
"assetType": "USDC",
"address": "0xAbCDEF1234567890aBCdEf1234567890ABcDef12"
}
}
```
For Base wallet accounts, using a Base wallet as the funding source:
```json theme={null}
{
"accountOrWalletInfo": {
"accountType": "BASE_WALLET",
"assetType": "USDC",
"address": "0xAbCDEF1234567890aBCdEf1234567890ABcDef12"
}
}
```
## Checking account balances
The internal account balance reflects all deposits and withdrawals. The balance includes:
* **amount**: The balance amount in the smallest currency unit (cents for USD, centavos for MXN/BRL, etc.)
* **currency**: Full currency details including code, name, symbol, and decimal places
### Example balance check
```bash Fetch the balance of an internal account theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/internal-accounts/InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965' \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
```json theme={null}
{
"data": {
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"balance": {
"amount": 50000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
}
}
}
```
Always check the `decimals` field in the currency object to correctly convert
between display amounts and API amounts. For example, USD has 2 decimals, so
an amount of 50000 represents \$500.00.
## Displaying funding instructions to customers
When customers need to deposit funds themselves, display the funding payment instructions in your application:
Fetch the customer's internal account for their desired currency using the API.
Parse the `fundingPaymentInstructions` array and select the appropriate instructions based on your customer's preferred payment method.
```javascript theme={null}
const instructions = account.fundingPaymentInstructions[0];
const bankInfo = instructions.accountOrWalletInfo;
```
Show the payment details prominently in your UI:
* Account holder name
* Bank name and routing information (account/routing number, CLABE, PIX key, etc.)
* Reference code (if provided)
* Any additional notes from `instructionsNotes`
The unique banking details in each internal account automatically route
deposits to the correct destination.
Set up webhook listeners to receive notifications when deposits are credited to the internal account. The account balance will update automatically.
You'll receive `ACCOUNT_STATUS` webhook events when the internal account balance changes.
## Best practices
Ensure your customers have all the information needed to make deposits. Consider implementing:
* Clear display of all banking details from `fundingPaymentInstructions`
* Copy-to-clipboard functionality for account numbers and reference codes
* Email/SMS confirmations with complete deposit instructions
Set up monitoring to alert customers when their balance is low:
```javascript theme={null}
if (account.balance.amount < minimumThreshold) {
await notifyCustomer({
type: 'LOW_BALANCE',
account: account.id,
instructions: account.fundingPaymentInstructions
});
}
```
If your platform supports multiple currencies, organize internal accounts by currency in your UI:
```javascript theme={null}
const accountsByCurrency = accounts.data.reduce((acc, account) => {
const code = account.balance.currency.code;
acc[code] = account;
return acc;
}, {});
// Quick lookup: accountsByCurrency['USD']
```
Internal account details (especially funding instructions) rarely change, so you can cache them safely. However, always fetch fresh balance data before initiating transfers.
## Next steps
Learn how to add customer wallets as reward destinations
Simplify bank account verification with Plaid Link
Use internal account balances to send Bitcoin rewards
View complete API documentation for internal accounts
# Listing Transactions
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/developer-guides/listing-transactions
Query and track Bitcoin reward payment history with filtering and pagination
Retrieve transaction history for Bitcoin rewards distributed through your platform. Transactions are returned in descending order (most recent first) and are paginated.
## Basic request
```bash cURL theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
```json response theme={null}
{
"data": [
{
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000025",
"status": "COMPLETED",
"type": "OUTGOING",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456",
"currency": "BTC"
},
"sentAmount": {
"amount": 100,
"currency": {
"code": "USD",
"symbol": "$",
"decimals": 2
}
},
"receivedAmount": {
"amount": 810,
"currency": {
"code": "BTC",
"symbol": "₿",
"decimals": 8
}
},
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"platformCustomerId": "user_789",
"description": "Weekly reward payout",
"exchangeRate": 8.1,
"settledAt": "2025-10-03T15:01:45Z",
"createdAt": "2025-10-03T15:00:00Z"
}
],
"hasMore": true,
"nextCursor": "eyJpZCI6IlRyYW5zYWN0aW9uOjAxOTU0MmY1LWIzZTctMWQwMi0wMDAwLTAwMDAwMDAwMDAyNSJ9",
"totalCount": 142
}
```
## Common filtering patterns
### Rewards for a specific customer
Get all Bitcoin rewards sent to a customer:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Or use your platform's customer ID:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?platformCustomerId=user_789' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
### Rewards by date range
Get all rewards distributed in a specific period:
```bash theme={null}
# October 2025 rewards
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?startDate=2025-10-01T00:00:00Z&endDate=2025-10-31T23:59:59Z' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
Dates must be in ISO 8601 format (e.g., `2025-10-03T15:00:00Z`).
### Failed rewards
Track rewards that failed to complete:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?status=FAILED&type=OUTGOING' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
**Transaction statuses:**
* `PENDING` - Reward initiated, awaiting processing
* `PROCESSING` - Bitcoin purchase and transfer in progress
* `COMPLETED` - Reward successfully delivered
* `FAILED` - Reward failed (invalid wallet address, insufficient balance, etc.)
### Rewards from platform account
Get all rewards paid from your platform's USD internal account:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?senderAccountIdentifier=InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965&type=OUTGOING' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
## Combining filters
Narrow down results by combining multiple filters:
```bash theme={null}
# All completed rewards for a customer in October
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions?platformCustomerId=user_789&status=COMPLETED&startDate=2025-10-01T00:00:00Z&endDate=2025-10-31T23:59:59Z' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
## Pagination
Handle large result sets with cursor-based pagination:
```javascript theme={null}
async function getAllRewardsForCustomer(platformCustomerId) {
const allRewards = [];
let cursor = null;
let hasMore = true;
while (hasMore) {
const url = cursor
? `https://api.lightspark.com/grid/2025-10-13/transactions?platformCustomerId=${platformCustomerId}&limit=100&cursor=${cursor}`
: `https://api.lightspark.com/grid/2025-10-13/transactions?platformCustomerId=${platformCustomerId}&limit=100`;
const response = await fetch(url, {
headers: { Authorization: `Basic ${credentials}` },
});
const { data, hasMore: more, nextCursor } = await response.json();
allRewards.push(...data);
hasMore = more;
cursor = nextCursor;
}
return allRewards;
}
```
The maximum `limit` is 100 transactions per request. Default is 20.
## Get a single transaction
Retrieve details for a specific reward transaction:
```bash theme={null}
curl -X GET 'https://api.lightspark.com/grid/2025-10-13/transactions/Transaction:019542f5-b3e7-1d02-0000-000000000025' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```
## Common use cases
### Calculate total rewards distributed
```javascript theme={null}
async function calculateMonthlyRewards(month) {
const startDate = new Date(month);
const endDate = new Date(startDate);
endDate.setMonth(endDate.getMonth() + 1);
let totalUSD = 0;
let totalBTC = 0;
let cursor = null;
do {
const url = `/transactions?status=COMPLETED&startDate=${startDate.toISOString()}&endDate=${endDate.toISOString()}&cursor=${cursor || ''}`;
const { data, nextCursor } = await fetch(url).then(r => r.json());
data.forEach(tx => {
totalUSD += tx.sentAmount.amount;
totalBTC += tx.receivedAmount.amount;
});
cursor = nextCursor;
} while (cursor);
return {
totalUSD: totalUSD / 100, // Convert cents to dollars
totalBTC: totalBTC / 100000000, // Convert sats to BTC
};
}
```
### Track customer reward history
```javascript theme={null}
async function getCustomerRewardsSummary(platformCustomerId) {
const response = await fetch(
`/transactions?platformCustomerId=${platformCustomerId}&status=COMPLETED`,
{ headers: { Authorization: `Basic ${credentials}` } }
);
const { data, totalCount } = await response.json();
const totalSatsReceived = data.reduce(
(sum, tx) => sum + tx.receivedAmount.amount,
0
);
return {
rewardCount: totalCount,
totalSatsReceived,
lastRewardAt: data[0]?.settledAt,
};
}
```
### Monitor failed rewards
```javascript theme={null}
async function getFailedRewards(startDate) {
const response = await fetch(
`/transactions?status=FAILED&type=OUTGOING&startDate=${startDate}`,
{ headers: { Authorization: `Basic ${credentials}` } }
);
const { data } = await response.json();
return data.map(tx => ({
transactionId: tx.id,
customerId: tx.platformCustomerId,
amount: tx.sentAmount.amount / 100,
failedAt: tx.createdAt,
description: tx.description,
}));
}
```
## Best practices
Use pagination when fetching transaction history to avoid timeouts and memory issues.
Filter by `platformCustomerId` for easier reconciliation with your internal user IDs.
Cache completed transaction data since it won't change after settlement.
Always filter by `type=OUTGOING` when tracking rewards to exclude incoming transactions.
## Next steps
Learn how to create and execute Bitcoin reward payouts
Set up webhook handling for real-time transaction notifications
View complete transaction API documentation
# External Accounts with Plaid
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/developer-guides/plaid
Simplify bank account verification with Plaid Link for external account setup
Plaid integration allows your customers to securely connect their bank accounts without manually entering account numbers and routing information. Grid handles the complete Plaid Link flow, automatically creating external accounts when customers authenticate their banks.
Plaid integration requires Grid to manage your Plaid configuration. Contact
support to enable Plaid for your platform.
## Overview
The Plaid flow involves collaboration between your platform, Grid, Plaid, and the customer's bank:
1. **Request link token**: Your platform requests a Plaid Link token from Grid for a specific customer
2. **Initialize Plaid Link**: Display Plaid Link UI to your customer using the link token
3. **Customer authenticates**: Customer selects their bank and authenticates using Plaid Link
4. **Exchange tokens**: Plaid returns a public token; your platform sends it to Grid's callback URL
5. **Async processing**: Grid exchanges the public token with Plaid and retrieves account details
6. **External account created**: Grid creates the external account and sends a webhook notification. The external account is available for transfers and payments
## Request a Plaid Link token
To initiate the Plaid flow, request a link token from Grid:
```bash cURL theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/plaid/link-tokens' \
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
-H 'Content-Type: application/json' \
-d '{
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001"
}'
```
**Response:**
```json theme={null}
{
"linkToken": "link-sandbox-af1a0311-da53-4636-b754-dd15cc058176",
"expiration": "2025-10-05T18:30:00Z",
"callbackUrl": "https://api.lightspark.com/grid/2025-10-13/plaid/callback/link-sandbox-af1a0311-da53-4636-b754-dd15cc058176",
"requestId": "req_abc123def456"
}
```
Store the `callbackUrl` when you request the link token so you can retrieve it later when exchanging the public token.
### Key response fields:
* **`linkToken`**: Use this to initialize Plaid Link in your frontend
* **`callbackUrl`**: Where to POST the public token after Plaid authentication completes. The URL follows the pattern `https://api.lightspark.com/grid/{version}/plaid/callback/{linkToken}`. While you can construct this manually, we recommend using the provided URL for forward compatibility.
* **`expiration`**: Link tokens typically expire after 4 hours
* **`requestId`**: Unique identifier for debugging purposes
Link tokens are single-use and will expire. If the customer doesn't complete
the flow, you'll need to request a new link token.
## Initialize Plaid Link
Display the Plaid Link UI to your customer using the link token. The implementation varies by platform:
Install the appropriate Plaid SDK for your platform:
* React: `npm install react-plaid-link`
* React Native: `npm install react-native-plaid-link-sdk`
* Vanilla JS: Include the Plaid script tag as shown above
```javascript theme={null}
import { usePlaidLink } from 'react-plaid-link';
function BankAccountConnector({ linkToken, onSuccess }) {
const { open, ready } = usePlaidLink({
token: linkToken,
onSuccess: async (publicToken, metadata) => {
console.log('Plaid authentication successful');
// Send public token to YOUR backend endpoint
await fetch('/api/plaid/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: metadata.account_id, // Optional
}),
});
onSuccess();
},
onExit: (error, metadata) => {
if (error) {
console.error('Plaid Link error:', error);
}
console.log('User exited Plaid Link');
},
});
return (
); }
```
```javascript theme={null}
import { PlaidLink } from 'react-native-plaid-link-sdk';
function BankAccountConnector({ linkToken, onSuccess }) {
return (
{
console.log('Plaid authentication successful');
// Send public token to YOUR backend endpoint
await fetch('https://yourapi.com/api/plaid/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: metadata.account_id,
}),
});
onSuccess();
}}
onExit={(error, metadata) => {
if (error) {
console.error('Plaid Link error:', error);
}
}}
>
Connect your bank account
);
}
```
```html theme={null}
```
## Exchange the public token on your backend
Create a backend endpoint that receives the public token from your frontend and forwards it to Grid's callback URL:
```javascript Express theme={null}
// Backend endpoint: POST /api/plaid/exchange-token
app.post('/api/plaid/exchange-token', async (req, res) => {
const { publicToken, accountId } = req.body;
const customerId = req.user.gridCustomerId; // From your auth
try {
// Get the callback URL (you stored this when requesting the link token)
const callbackUrl = await getStoredCallbackUrl(customerId);
// Forward to Grid's callback URL with proper authentication
const response = await fetch(callbackUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
publicToken: publicToken,
accountId: accountId,
}),
});
if (!response.ok) {
throw new Error(`Grid API error: ${response.status}`);
}
const result = await response.json();
res.json({ success: true, message: result.message });
} catch (error) {
console.error('Error exchanging token:', error);
res.status(500).json({ error: 'Failed to process bank account' });
}
});
```
**Response from Grid (HTTP 202 Accepted):**
```json theme={null}
{
"message": "External account creation initiated. You will receive a webhook notification when complete.",
"requestId": "req_def456ghi789"
}
```
A `202 Accepted` response indicates Grid has received the token and is
processing it asynchronously. The external account will be created in the
background.
## Handle webhook notification
After Grid creates the external account, you'll receive an `ACCOUNT_STATUS` webhook.
```json theme={null}
{
"type": "ACCOUNT_STATUS",
"timestamp": "2025-01-15T14:32:10Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-0000000000ac",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"account": {
"accountId": "ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
"status": "ACTIVE",
"currency": "USD",
"platformAccountId": "user_123_primary_bank",
"accountInfo": {
"accountType": "US_ACCOUNT",
"accountNumber": "123456789",
"routingNumber": "021000021",
"accountCategory": "CHECKING",
"bankName": "Chase Bank",
"beneficiary": {
"beneficiaryType": "INDIVIDUAL",
"fullName": "John Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}
}
}
```
## Error handling
Handle common error scenarios:
### User exits Plaid Link
```javascript theme={null}
const { open } = usePlaidLink({
token: linkToken,
onExit: (error, metadata) => {
if (error) {
console.error("Plaid error:", error);
// Show user-friendly error message
setError("Unable to connect to your bank. Please try again.");
} else {
// User closed the modal without completing
console.log("User exited without connecting");
}
},
});
```
## Next steps
Learn more about managing external accounts
Set up webhook handling for account notifications
View complete Plaid API documentation
# Platform Configuration
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/developer-guides/platform-configuration
Configuring platform settings for rewards
## Supported currencies
During onboarding, choose the currencies your platform will support. For prefunded models, Grid automatically creates per‑currency accounts for each new customer. You can add or remove supported currencies anytime in the Grid dashboard.
## API credentials and authentication
Create API credentials in the Grid dashboard. Credentials are scoped to an environment (Sandbox or Production) and cannot be used across environments.
* Authentication: Use HTTP Basic Auth with your API key and secret in the `Authorization` header.
* Keys: Sandbox keys only work against Sandbox; Production keys only work against Production.
Never share or expose your API secret. Rotate credentials periodically and restrict access.
### Example: HTTP Basic Auth in cURL
```bash theme={null}
# Using cURL's Basic Auth shorthand (-u):
curl -sS -X GET "https://api.lightspark.com/grid/2025-10-13/config" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
## Base API path
The base API path is consistent across environments; your credentials determine the environment.
Base URL: `https://api.lightspark.com/grid/2025-10-13` (same for Sandbox and Production; your keys select the environment).
## Webhooks and signature verification
Configure your webhook endpoint to receive payment lifecycle events. Webhooks use asymmetric (public/private key) signatures; verify each webhook using the Grid public key available in your dashboard.
* Expose a public HTTPS endpoint (for development, reverse proxies like ngrok can help). You'll also need to set your webhook endpoint in the Grid dashboard.
* When receiving webhooks, verify the `X-Grid-Signature` header against the exact request body using the dashboard-provided public key
* Process events idempotently and respond with 2xx on success
You can trigger a test delivery from the API to validate your endpoint setup. The public key for verification is shown in the dashboard; rotate and update it when instructed by Lightspark.
### Test your webhook endpoint
Use the webhook test endpoint to send a synthetic event to your configured endpoint.
```bash theme={null}
curl -sS -X POST "https://api.lightspark.com/grid/2025-10-13/webhooks/test" \
-u "$GRID_CLIENT_ID:$GRID_API_SECRET"
```
Example test webhook payload:
```json theme={null}
{
"test": true,
"timestamp": "2023-08-15T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000001",
"type": "TEST"
}
```
For more details about webhooks like retry policy and examples, take a look at our Webhooks documentation.
# Rewards
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/index
With , you can send instant Bitcoin rewards to your **users' self-custody wallets** worldwide through a single, simple API. automatically handles the entire process, managing the fiat-to-crypto conversion, compliance checks, and instant delivery for you.
The Grid API combines USD-to-BTC conversion and payout into a single, atomic operation, simplifying the process of distributing rewards.
Grid converts your platform's fiat balance into Bitcoin on demand, allowing you to offer crypto rewards without managing digital asset custody or complex exchange integrations.
Delivers Bitcoin to your users' wallets in seconds (like a Spark wallet) giving them immediate ownership and control.
***
## Rewards Payout Flow
Your platform's internal account is pre-funded with fiat currency (e.g., USD) via standard payment rails like ACH push.
For rewards, the only entity who needs to be KYB'd is the entity paying for the reward. This can be you, the platform, or
your business customers that want to pay out rewards to their end users.
You execute a single API call to create a quote, instantly convert a specific USD amount to BTC at the current market rate, and transfer the Bitcoin to the user's wallet address.
***
## Features
Users interact with through two main interfaces:
Programmatic access to onboard customers, fund your platform account, get quotes for Bitcoin purchases, and execute reward payouts. Reconcile all activity with real-time webhooks.
Your development and operations team can use the dashboard to monitor balances and transactions, manage API keys and environments, and troubleshoot with detailed logs.
### Onboarding Customers
For rewards with Grid, the only entity who needs to be KYB'd is the entity paying for the reward. This can be you, the platform, or
your business customers that want to pay out rewards to their end users.
All you need in order to pay out a reward is the wallet address.
No need to collect extra personal information or go through the full hosted KYC flow for end users!
To generate a spark wallet, you can use a tool like [Privy](https://privy.io) or the Spark SDK directly.
If you do have business customers that want to pay out rewards to their end users, you can onboard that business customer
via the hosted KYB link flow.
### Funding Your Platform Account
operates on a pre-funded model. You can fund your internal platform account using several payment rails such as ACH, wire transfers, Lightning, and more. This stored balance is then used to instantly purchase and send Bitcoin rewards to your customers.
### Sending Rewards
To send a reward with , you create and execute a quote. The API call specifies your funded internal account as the source and the customer's Bitcoin wallet address as the destination. Grid handles the USD-to-BTC conversion and instant delivery to the receiving wallet, notifying you of the completed transfer via webhook.
### Environments
supports two environments: **Sandbox** and **Production**.
The Sandbox mirrors production behavior, allowing you to test the full end-to-end flow—from funding a test account and onboarding a mock customer to sending a simulated Bitcoin reward—without moving real funds.
The Production environment uses live credentials and base URLs for real transactions once you're ready to launch.
***
Ready to integrate ? Check out our quickstart guide.
# Postman Collection
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/platform-tools/postman-collection
Use our hosted Postman collection to explore endpoints and send test requests quickly.
Launch the collection in Postman.
# Sandbox Testing
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/platform-tools/sandbox-testing
Test your rewards integration in the Grid sandbox environment
## Overview
The Grid sandbox environment allows you to test your rewards integration without moving real money or cryptocurrency. All API endpoints work the same way in sandbox as they do in production, but transactions are simulated and you can control test scenarios using special test values.
## Getting Started with Sandbox
### Sandbox Credentials
To use the sandbox environment:
1. Contact Lightspark to get your inital sandbox credentials configured. Email [support@lightspark.com](mailto:support@lightspark.com) to get started.
2. Add your sandbox API token and secret to your environment variables.
3. Use the normal production base URL: `https://api.lightspark.com/grid/2025-10-13`
4. Authenticate using your sandbox token with HTTP Basic Auth
## Simulating Money Movements
### Funding Platform Internal Accounts
In production, your platform's internal account is funded by following the payment instructions (bank transfer, wire, etc.). In sandbox, you can instantly add funds to your platform's internal account using the following endpoint:
```bash theme={null}
POST /sandbox/internal-accounts/{accountId}/fund
{
"amount": 200000 # $2,000 in cents
}
```
**Example:**
```bash theme={null}
curl -X POST https://api.lightspark.com/grid/2025-10-13/sandbox/internal-accounts/InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965/fund \
-u "sandbox_token_id:sandbox_token_secret" \
-H "Content-Type: application/json" \
-d '{
"amount": 200000
}'
```
This endpoint returns the updated `InternalAccount` object with the new balance. You'll also receive an `ACCOUNT_STATUS` webhook showing the balance change.
In production, ACH transfers typically take 1-3 business days to settle. In sandbox, funding is instant.
## Testing Reward Distributions
### Testing Successful Bitcoin Rewards
The standard reward flow works seamlessly in sandbox. Create and execute a quote to instantly convert USD to BTC and send to a Spark wallet:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
-u "sandbox_token_id:sandbox_token_secret" \
-H "Content-Type: application/json" \
-d '{
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"
},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100,
"immediatelyExecute": true,
"description": "Bitcoin reward payout!"
}'
```
In sandbox:
* The USD is instantly debited from your platform's internal account
* Bitcoin is "purchased" at a simulated exchange rate
* The Bitcoin is delivered to the Spark wallet address. In sandbox, BTC funds are regtest funds so that they're compatible with real regtest spark wallets.
* You receive an `OUTGOING_PAYMENT` webhook notification
In sandbox, Bitcoin transfers complete instantly on regtest. In production, Spark wallet transfers typically complete within seconds.
### Testing Wallet Address Failures
Use special Spark wallet address patterns to test different failure scenarios. The **last 3 digits** of the wallet address determine the test behavior:
| Last Digits | Behavior | Use Case |
| ------------- | ----------------------- | ------------------------------------------- |
| **003** | Wallet unavailable | Recipient wallet is offline or unreachable |
| **005** | Timeout/delayed failure | Transaction stays pending \~30s, then fails |
| **Any other** | Success | All transfers complete normally |
**Example - Testing Wallet Unavailable:**
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
-u "sandbox_token_id:sandbox_token_secret" \
-H "Content-Type: application/json" \
-d '{
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"
},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6m003"
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100,
"immediatelyExecute": true
}'
```
The quote execution will fail immediately with a wallet unavailable error.
Note that these failure test patterns work for any external account type. If you want to test other cases of funding from a broken fiat account,
you can create an external account with the appropriate test pattern and use that for the quote source for funding. There are also two other
failure test patterns relevant for bank accounts:
* **002**: Insufficient funds (transfer-in will fail)
* **004**: Transfer rejected (bank rejects the transfer)
## Testing Customer Onboarding
### Sandbox KYB Flow
In sandbox, the KYB onboarding process is simplified to always use the `/customers` endpoint instead of the KYB link flow.
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/customers" \
-u "sandbox_token_id:sandbox_token_secret" \
-H "Content-Type: application/json" \
-d '{
"platformCustomerId": "user_12345",
"customerType": "INDIVIDUAL",
"fullName": "Jane Doe",
"birthDate": "1992-03-25",
"nationality": "US"
}'
```
In sandbox, customers are automatically approved. In production, KYB verification may take several minutes.
## Testing Insufficient Balance
To test insufficient balance scenarios, simply attempt to send more than your platform's internal account balance:
```bash theme={null}
# Assuming account balance is $2,000 (200000 cents)
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
-u "sandbox_token_id:sandbox_token_secret" \
-H "Content-Type: application/json" \
-d '{
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965"
},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 300000,
"immediatelyExecute": true
}'
```
The quote execution will fail with an insufficient balance error.
## Testing Webhooks
All webhook events fire normally in sandbox. To test your webhook endpoint:
1. Configure your webhook URL in the dashboard
2. Perform actions that trigger webhooks (funding accounts, executing quotes, etc.)
3. Receive webhook events at your endpoint
4. Verify signature using the sandbox public key
You can also manually trigger a test webhook:
```bash theme={null}
curl -X POST "https://api.lightspark.com/grid/2025-10-13/webhooks/test" \
-u "sandbox_token_id:sandbox_token_secret" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks"
}'
```
## Common Testing Workflows
### Complete Reward Distribution Test
Here's a complete test workflow for distributing a \$1.00 Bitcoin reward:
1. **Fund your platform's internal account:**
```bash theme={null}
POST /sandbox/internal-accounts/InternalAccount:platform-usd/fund
{ "amount": 100000 } # $1,000
```
2. **Create a test customer:**
```bash theme={null}
POST /customers
```
3. **Execute a reward quote:**
```bash theme={null}
POST /quotes
# Platform USD account → Customer's Spark wallet
# With immediatelyExecute: true
```
4. **Verify completion via webhook** (`OUTGOING_PAYMENT` event)
5. **Check transaction history:**
```bash theme={null}
GET /transactions?customerId=Customer:019542f5-b3e7-1d02-0000-000000000001
```
### Testing Error Scenarios
Test each failure mode systematically:
```bash theme={null}
# 1. Test wallet unavailable (003)
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
-u "sandbox_token_id:sandbox_token_secret" \
-H "Content-Type: application/json" \
-d '{
"source": {"accountId": "InternalAccount:platform-usd"},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:test001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6m003" # Wallet unavailable *003
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100,
"immediatelyExecute": true
}'
# Response: Transaction fails with wallet unavailable error
# 2. Test insufficient balance
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
-u "sandbox_token_id:sandbox_token_secret" \
-H "Content-Type: application/json" \
-d '{
"source": {"accountId": "InternalAccount:platform-usd"},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:test001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 10000000,
"immediatelyExecute": true
}'
# Response: 400 Bad Request with insufficient balance error
# 3. Test timeout scenario (005)
curl -X POST "https://api.lightspark.com/grid/2025-10-13/quotes" \
-u "sandbox_token_id:sandbox_token_secret" \
-H "Content-Type: application/json" \
-d '{
"source": {"accountId": "InternalAccount:platform-usd"},
"destination": {
"externalAccountDetails": {
"customerId": "Customer:test001",
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6m005" # Timeout/delayed failure *005
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100,
"immediatelyExecute": true
}'
# Check status immediately - will show PENDING
# Wait 30s, check again - will show FAILED
```
## Sandbox Limitations
While sandbox closely mimics production, there are some differences:
* **Instant settlement**: All Bitcoin transfers complete instantly (success cases) or fail immediately (error cases), except timeout scenarios (005)
* **Uses Regtest funds**: Spark bitcoin funds are regtest funds so that they're compatible with real regtest spark wallets.
* **Simplified KYB**: KYB processes are simulated and complete instantly with automatic approval
* **Fixed exchange rates**: Currency conversion rates may not reflect real-time market rates
Do not try sending money to any sandbox wallet addresses or bank accounts. These are not real addresses and will not receive funds.
## Moving to Production
When you're ready to move to production:
1. Generate production API tokens in the dashboard
2. Swap those credentials for the sandbox credentials in your environment variables
3. Remove any sandbox-specific test patterns from your code (magic number wallet addresses)
4. Configure production webhook endpoints
5. Test with small reward amounts first ($0.01-$1.00)
6. Gradually increase volume as you gain confidence
## Next Steps
* Review [Webhooks](/rewards/platform-tools/webhooks) for event handling
* Check out the [Postman Collection](/rewards/platform-tools/postman-collection) for API examples
* See [Platform Configuration](/rewards/developer-guides/platform-configuration) for production settings
# Webhooks
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/platform-tools/webhooks
All webhooks sent by the Grid API include a signature in the `X-Grid-Signature` header, which allows you to verify the authenticity of the webhook. This is critical for security, as it ensures that only legitimate webhooks from Grid are processed by your system.
## Signature Verification Process
1. **Obtain your Grid public key**
* This is provided to you during the integration process. Reach out to us at [support@lightspark.com](mailto:support@lightspark.com) or over Slack to get the public key.
* The key is in PEM format and can be used with standard cryptographic libraries
2. **Verify incoming webhooks**
* Extract the signature from the `X-Grid-Signature` header
* Decode the base64 signature
* Create a SHA-256 hash of the entire request body
* Verify the signature using the Grid webhook public key and the hash
* Only process the webhook if the signature verification succeeds
## Verification Examples
### Node.js Example
```javascript theme={null}
const crypto = require('crypto');
const express = require('express');
const app = express();
// Your Grid public key provided during integration
const GRID_WEBHOOK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----`;
app.post('/webhooks/uma', (req, res) => {
const signatureHeader = req.header('X-Grid-Signature');
if (!signatureHeader) {
return res.status(401).json({ error: 'Signature missing' });
}
try {
let signature: Buffer;
try {
// Parse the signature as JSON. It's in the format {"v": "1", "s": "base64_signature"}
const signatureObj = JSON.parse(signatureHeader);
if (signatureObj.v && signatureObj.s) {
// The signature is in the 's' field
signature = Buffer.from(signatureObj.s, "base64");
} else {
throw new Error("Invalid JSON signature format");
}
} catch {
// If JSON parsing fails, treat as direct base64
signature = Buffer.from(signatureHeader, "base64");
}
// Create verifier with the public key and correct algorithm
const verifier = crypto.createVerify("SHA256");
const payload = await request.text();
verifier.update(payload);
verifier.end();
// Verify the signature using the webhook public key
const isValid = verifier.verify(
{
key: GRID_WEBHOOK_PUBLIC_KEY,
format: "pem",
type: "spki",
},
signature,
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Webhook is verified, process it based on type
const webhookData = req.body;
if (webhookData.type === 'INCOMING_PAYMENT') {
// Process incoming payment webhook
// ...
} else if (webhookData.type === 'OUTGOING_PAYMENT') {
// Process outgoing payment webhook
// ...
}
// Acknowledge receipt of the webhook
return res.status(200).json({ received: true });
} catch (error) {
console.error('Signature verification error:', error);
return res.status(401).json({ error: 'Signature verification failed' });
}
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
```
### Python Example
```python theme={null}
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
from flask import Flask, request, jsonify
import base64
app = Flask(__name__)
# Your Grid public key provided during integration
GRID_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----"""
# Load the public key
public_key = serialization.load_pem_public_key(
GRID_PUBLIC_KEY.encode('utf-8')
)
@app.route('/webhooks/uma', methods=['POST'])
def handle_webhook():
# Get signature from header
signature = request.headers.get('X-Grid-Signature')
if not signature:
return jsonify({'error': 'Signature missing'}), 401
try:
# Get the raw request body
request_body = request.get_data()
# Create a SHA-256 hash of the request body
hash_obj = hashes.Hash(hashes.SHA256())
hash_obj.update(request_body)
digest = hash_obj.finalize()
# Decode the base64 signature
signature_bytes = base64.b64decode(signature)
# Verify the signature
try:
public_key.verify(
signature_bytes,
request_body,
ec.ECDSA(hashes.SHA256())
)
except Exception as e:
return jsonify({'error': 'Invalid signature'}), 401
# Webhook is verified, process it based on type
webhook_data = request.json
if webhook_data['type'] == 'INCOMING_PAYMENT':
# Process incoming payment webhook
# ...
pass
elif webhook_data['type'] == 'OUTGOING_PAYMENT':
# Process outgoing payment webhook
# ...
pass
# Acknowledge receipt of the webhook
return jsonify({'received': True}), 200
except Exception as e:
print(f'Signature verification error: {e}')
return jsonify({'error': 'Signature verification failed'}), 401
if __name__ == '__main__':
app.run(port=3000)
```
## Testing
To test your webhook implementation, you can trigger a test webhook from the Grid dashboard. This will send a test webhook to the endpoint you provided during the integration process. The test webhook will also be sent automatically when you update your platform configuration with a new webhook URL.
An example of the test webhook payload is shown below:
```json theme={null}
{
"test": true,
"timestamp": "2023-08-15T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
"type": "TEST"
}
```
You should verify the signature of the webhook using the Grid public key and the process outlined in the [Signature Verification Process](#signature-verification-process) section and then reply with a 200 OK response to acknowledge receipt of the webhook.
## Security Considerations
* **Always verify signatures**: Never process webhooks without verifying their signatures.
* **Use HTTPS**: Ensure your webhook endpoint uses HTTPS to prevent man-in-the-middle attacks.
* **Implement idempotency**: Use the `webhookId` field to prevent processing duplicate webhooks.
* **Timeout handling**: Implement proper timeout handling and respond to webhooks promptly.
## Retry Policy
The Grid API will retry webhooks with the following policy based on the webhook type:
| Webhook Type | Retry Policy | Notes |
| ------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| TEST | No retries | Used for testing webhook configuration |
| OUTGOING\_PAYMENT | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| INCOMING\_PAYMENT | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on: 409 (duplicate webhook) or PENDING status since it is served as an approval mechanism in-flow |
| BULK\_UPLOAD | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| INVITATION\_CLAIMED | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| KYC\_STATUS | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| ACCOUNT\_STATUS | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
# Quickstart
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/quickstart
Complete walkthrough for buying Bitcoin and sending it as a reward to an external Spark wallet for self-custody
This guide walks you through the complete process of buying Bitcoin with USD and sending it to a self-custody Spark wallet, from customer onboarding through quote execution.
## Understanding Entity Mapping for Rewards
In this guide, the entities map as follows (platform-funded model):
| Entity Type | Who They Are | In This Example |
| -------------------- | ---------------------------------------- | -------------------------------- |
| **Platform** | Your rewards app paying rewards directly | Your cashback/rewards platform |
| **Customer** | (Not used in this model) | N/A |
| **External Account** | Users' crypto wallets receiving rewards | User's self-custody Spark wallet |
**Flow**: Your platform funds its internal account → sends Bitcoin micro-payouts directly → to users' external crypto wallets at scale. This is common for cashback apps where you earn affiliate commissions and share them with users.
For white-label reward programs where brands like Nike or Starbucks fund their own reward campaigns, those brands would be created as **Customers** who manage their own reward budgets.
## Prerequisites
Before starting this guide, ensure you have:
* A Grid API account with valid authentication credentials
* Access to the Grid API endpoints (production or sandbox)
* A webhook endpoint configured to receive notifications
* A Spark wallet address where the Bitcoin will be sent
## Overview
The process consists of the following steps:
1. **List platform internal accounts** to find your platform's USD funding instructions
2. **Fund your internal account** via ACH push and receive a webhook notification
3. **Generate a spark wallet** for your customer, or let them connect their own
4. **Execute a quote** to complete the Bitcoin purchase and transfer a reward to the user's own Spark wallet.
## Step 1: List your platform's internal accounts
When your platform is first created, it is automatically assigned an internal account with a balance in your configured fiat currency.
List your platform's internal accounts to see the available balances and funding instructions for USD.
### Request
```bash theme={null}
curl -X GET "https://api.lightspark.com/grid/2025-10-13/platform/internal-accounts?currency=USD" \
-H "Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```
### Response
```json theme={null}
{
"data": [
{
"id": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"balance": {
"amount": 0,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"fundingPaymentInstructions": [
{
"instructionsNotes": "Include the reference code in your ACH transfer memo",
"accountOrWalletInfo": {
"accountType": "US_ACCOUNT",
"reference": "FUND-BTC123",
"accountNumber": "9876543210",
"routingNumber": "021000021",
"accountHolderName": "Lightspark Payments FBO John Doe",
"bankName": "JP Morgan Chase"
}
},
{
"accountOrWalletInfo": {
"accountType": "SPARK_WALLET",
"assetType": "USDB",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu",
"invoice": "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs"
}
},
{
"accountOrWalletInfo": {
"accountType": "SOLANA_WALLET",
"assetType": "USDC",
"address": "4Nd1m6Qkq7RfKuE5vQ9qP9Tn6H94Ueqb4xXHzsAbd8Wg"
}
}
],
"createdAt": "2025-10-03T12:00:00Z",
"updatedAt": "2025-10-03T12:00:00Z"
}
]
}
```
The `fundingPaymentInstructions` provide the bank account details and reference code needed to fund this internal account via ACH pull from the customer's bank.
You can also see that there are Spark wallet funding instructions in this
example response which can be used to fund the internal account with USDB
instantly.
## Step 2: Fund your Internal Account
You can initiate an ACH transfer from your bank to the account details provided in the funding instructions, making sure to include the reference code `FUND-BTC123` in the transfer memo.
In sandbox mode, you can use the `/sandbox/internal-accounts/{accountId}/fund`
endpoint to simulate receiving funds. In production, actual ACH transfers
typically take 1-3 business days to settle.
### Webhook Notification
When the funds are received and the internal account balance is updated, you'll receive a webhook notification:
```json theme={null}
{
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"oldBalance": {
"amount": 0,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"newBalance": {
"amount": 200000,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"timestamp": "2025-10-03T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
"type": "ACCOUNT_STATUS"
}
```
The internal account now has a balance of \$2,000.00 (200000 cents). You can use this balance to instantly distribute Bitcoin rewards to your customers.
## Step 3: Customer Onboarding
This guide assumes you have a Spark wallet address for your customer who will receive the Bitcoin reward.
For rewards, the only entity who needs to be KYB'd is the entity paying for the reward - in this case, you, the platform!
All you need in order to pay out a reward is the wallet address. No need to go through the full hosted KYC flow
for this use case! To generate a spark wallet, you can use a tool like [Privy](https://privy.io) or the Spark SDK directly.
## Step 4: Create and Execute a Quote to the Customer's Spark Wallet
Create and execute a trade from USD to BTC, and initiate the final transfer in one step. This combines external account
creation and quote execution using the `externalAccountDetails` option.
### Request
```bash theme={null}
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": {
"externalAccountDetails": {
"currency": "BTC",
"accountInfo": {
"accountType": "SPARK_WALLET",
"address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
}
}
},
"lockedCurrencySide": "SENDING",
"lockedCurrencyAmount": 100,
"immediatelyExecute": true,
"description": "Bitcoin reward payout!"
}'
```
**Combined External Account Creation and Quote Execution**: The `externalAccountDetails` option allows you to
create the external account and execute the quote in a single API call, which is perfect for one-off payments
to new destinations. The external account will be automatically created and then used as the destination for
the Bitcoin transfer. Its ID in the response can be used directly in future quote creation requests.
**Immediate Quote Execution (Market Order)**: Note that `immediatelyExecute` is set to `true` in this example.
Because we always just want to send \$1.00 worth of BTC to users as a reward at the current market rate, we don't
need to lock a quote and view the rate details before executing. If you want to lock a quote and confirm fees
and exchange rate details before executing the quote, set `immediatelyExecute` to `false` or omit the field.
### Response
```json theme={null}
{
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000020",
"status": "PROCESSING",
"createdAt": "2025-10-03T15:00:00Z",
"expiresAt": "2025-10-03T15:05:00Z",
"source": {
"accountId": "InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965",
"currency": "USD"
},
"destination": {
"accountId": "ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456",
"currency": "BTC"
},
"sendingCurrency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
},
"receivingCurrency": {
"code": "BTC",
"name": "Bitcoin",
"symbol": "₿",
"decimals": 8
},
"totalSendingAmount": 100,
"totalReceivingAmount": 810,
"exchangeRate": 8.1,
"feesIncluded": 5,
"transactionId": "Transaction:019542f5-b3e7-1d02-0000-000000000025"
}
```
The quote shows:
* **Sending**: \$1.00 USD (including \$0.05 fee)
* **Receiving**: 0.0000081 BTC (810 satoshis)
* **Exchange rate**: 8.1 sats per USD cent (\~\$123,000 per BTC)
* **External account created**: The Spark wallet was automatically added as an external account during quote creation
The quote status changes to `PROCESSING` and the Bitcoin transfer is initiated. The external account is created, USD is debited from the internal account, Bitcoin is purchased, and then sent to the Spark wallet address.
You can track the status by:
1. Polling the quote endpoint: `GET /quotes/{quoteId}`
2. Waiting for a webhook notification
### Completion Webhook
When the Bitcoin transfer completes, you'll receive a webhook notification:
```json theme={null}
{
"transaction": {
"id": "Transaction:019542f5-b3e7-1d02-0000-000000000025",
"status": "COMPLETED",
"type": "OUTGOING",
"sentAmount": {
"amount": 100,
"currency": {
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"decimals": 2
}
},
"receivedAmount": {
"amount": 810,
"currency": {
"code": "BTC",
"name": "Bitcoin",
"symbol": "₿",
"decimals": 8
}
},
"settledAt": "2025-10-03T15:01:45Z",
"createdAt": "2025-10-03T15:00:00Z",
"description": "Bitcoin purchase for self-custody",
"exchangeRate": 8.1,
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000020"
},
"timestamp": "2025-10-03T15:02:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000030",
"type": "OUTGOING_PAYMENT"
}
```
Bitcoin transfers to Spark wallets typically complete within seconds, much
faster than traditional Bitcoin on-chain transactions.
## Summary
You've successfully completed a Bitcoin purchase and transfer to a self-custody Spark wallet! Here's what happened:
1. ✅ Listed internal accounts and obtained USD funding instructions
2. ✅ Funded the internal account with USD via ACH
3. ✅ Generated a Spark wallet for the customer
4. ✅ Created and executed a quote to purchase Bitcoin and send to the Spark wallet
The customer now has 810 Satoshis in their self-custody Spark wallet!
## Next Steps
* **Transaction history**: Use `GET /transactions` to track all Bitcoin purchases
* **Price monitoring**: Build price alerts using the lookup endpoint to monitor rates
* **Webhook verification**: Implement signature verification for webhook security (see [Webhooks guide](/rewards/platform-tools/webhooks))
## Related Resources
* [API Reference](/api-reference) - Complete API documentation
* [Platform Configuration](/rewards/developer-guides/platform-configuration) - Configure your platform settings
* [Webhooks](/rewards/platform-tools/webhooks) - Webhook security and verification
# Core Concepts
Source: https://ramps-feat-building-with-ai.mintlify.app/rewards/terminology
Core concepts and terminology for the Grid API
There are several key entities in the Grid API: **Platform**, **Customers**, **Internal Accounts**, **External Accounts**, **Quotes**, **Transactions**, and **UMA Addresses**.
## Businesses, People, and Accounts
### Platform
Your **platform** is you! It's the top-level entity that integrates with the Grid API. The platform:
* Has its own configuration (webhook endpoint, supported currencies, API tokens, etc.)
* A platform can have many customers both business and individual
* Manages multiple customers and their accounts
* Can hold platform-owned internal accounts for settlement and liquidity management
* Acts as the integration point between your application and the open Money Grid
### Customers
**Customers** are your end users who send and receive payments through your platform. Each customer:
* Can be an individual or business entity
* Has a KYC/KYB status that determines their ability to transact. If you are a regulated financial institution, this will typically be `APPROVED` since you do the KYC/KYB yourself.
* Is identified by both a system-generated ID and optionally your platform-specific customer ID
* May have associated internal accounts and external accounts
* May have a unique **UMA address** (e.g., `$john.doe@yourdomain.com`). If you don't assign an UMA address when creating a customer, they will be assigned a system-generated one.
### Internal Accounts
**Internal accounts** are Grid-managed accounts that hold balances in specific currencies. They can belong to either:
* **Platform internal accounts** - Owned by the platform for settlement, liquidity, and float management
* **Customer internal accounts** - Associated with specific customers for holding funds
Internal accounts:
* Have balances in a single currency (USD, EUR, MXN, etc.)
* Can be funded via bank transfers or crypto deposits using payment instructions
* Are used as sources or destinations for transactions instantly 24/7/365
* Track available balance for sending payments or receiving funds
### External Accounts
**External accounts** are traditional bank accounts, crypto wallets, or other payment instruments connected to customers
for on-ramping or off-ramping funds. Each external account:
* Are associated with a specific customer or the platform
* Represents a real-world bank account (with routing number, account number, IBAN, etc.), wallet, or payment instrument
* Has an associated beneficiary (individual or business) who receives payments from the customer or platform
* Has a status indicating screening status (ACTIVE, PENDING, INACTIVE, etc.)
* Can be used as a destination for quote-based transfers or same currency transfers like withdrawals
* For pullable sources like debit cards or ACH pulls, an external account can be used as a source for transfers-in to
fund internal accounts or to fund cross-border transfers via quotes.
## Entity Examples by Use Case
Understanding how entities map to your specific use case helps clarify your integration architecture. Here are common examples:
### B2B Payouts Platform (e.g., Bill.com, Routable)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------- | ------------------------------------------ |
| **Platform** | The payouts platform itself | Your company providing AP automation |
| **Customer** | Businesses sending payments to vendors | Acme Corp (your client company) |
| **External Account** | Vendors/suppliers receiving payments | Office supply vendor, freelance contractor |
**Flow**: Acme Corp (customer) uses your platform to pay their vendor invoices → funds move from Acme's internal account → to vendor's external bank account
### Direct Rewards Platform (Platform-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | ------------------------------------------- | ----------------------------------- |
| **Platform** | The app paying rewards directly to users | Your cashback app |
| **Customer** | (Not used in this model) | N/A |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Your platform sends micro-payouts directly from platform internal accounts → to users' external crypto wallets at scale. Common for cashback apps where the platform earns affiliate commissions and shares them with users.
### White-Label Rewards Platform (Customer-Funded Model)
| Entity Type | Who They Are | Example |
| -------------------- | -------------------------------------------- | ----------------------------------- |
| **Platform** | The rewards infrastructure provider | Your white-label rewards API |
| **Customer** | Brands or merchants running reward campaigns | Nike, Starbucks |
| **External Account** | End users' crypto wallets receiving rewards | Sarah's self-custody Bitcoin wallet |
**Flow**: Nike (customer) funds their internal account → your platform sends rewards on their behalf → to users' external crypto wallets. Common for brand loyalty programs where merchants manage their own reward budgets.
### Remittance/P2P App (e.g., Wise, Remitly)
| Entity Type | Who They Are | Example |
| -------------------- | ----------------------------------- | ---------------------------------------------------------------- |
| **Platform** | The remittance service | Your money transfer app |
| **Customer** | Both sender and recipient of funds | Maria (sender in US), Juan (recipient in Mexico) |
| **External Account** | Bank accounts for funding/receiving | Maria's US bank (funding), Juan's Mexican bank (receiving funds) |
**Flow**: Maria (customer) funds transfer from her external account → to Juan (also a customer) → who receives funds in his external bank account. Alternatively, Maria could send to Juan's UMA address directly.
## Transactions and Addressing Entities
### Quotes
**Quotes** provide locked-in exchange rates and payment instructions for transfers. A quote:
* Specifies a source (internal account, customer ID, or the platform itself) and destination (internal/external account or UMA address)
* Locks an exchange rate for a short period (typically 1-5 minutes) or can be immediately executed with the `immediatelyExecute` flag
* Calculates total fees and amounts for currency conversion
* Provides payment instructions for funding the transfer if needed, or can be funded via an internal account balance.
* Must be executed before it expires
* Creates a transaction when executed
### Transactions
**Transactions** represent completed or in-progress payment transfers. Each transaction:
* Has a type (INCOMING or OUTGOING from the platform's perspective)
* Has a status (PENDING, COMPLETED, FAILED, etc.)
* References a customer (sender for outgoing, recipient for incoming) or a platform internal account
* Specifies source and destination (accounts or UMA addresses)
* Includes amounts, currencies, and settlement information
* May include counterparty information for compliance purposes if required by your platform configuration
Transactions are created when:
* A quote is executed (either incoming or outgoing)
* A same currency transfer is initiated (transfer-in or transfer-out)
### UMA Addresses (optional)
**UMA addresses** are human-readable payment identifiers that follow the format `$username@domain.com`. They:
* Uniquely identify entities on the Grid network
* Enable sending and receiving payments across different platforms without knowing the recipient's underlying account details or personal information
* Support currency negotiation and cross-border transfers
* Work similar to email addresses but for payments
* Are an optional UX improvement for some use cases. Use of UMA addresses is not required in order to use the Grid API.