# 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**. Entity relationships diagram showing platform, customers, internal accounts, external accounts, transactions, and UMA addresses Entity relationships diagram showing platform, customers, internal accounts, external accounts, 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 Global P2P hero 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 Global P2P quickstart hero Global P2P quickstart hero 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**. Entity relationships diagram showing platform, customers, internal accounts, external accounts, transactions, and UMA addresses Entity relationships diagram showing platform, customers, internal accounts, external accounts, 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
Grid Documentation

Build applications and financial products that move money globally across fiat, stablecoins, and Bitcoin with a single API.

Ready to test? Explore the Grid Sandbox Request sandbox access
Understand Grid
Learn how Grid works before you integrate
Start building
Jump into a quickstart guide
Capabilities Primitives for moving money
Send
Move funds to any destination
Receive
Accept incoming funds
Convert
Exchange currencies and assets
Hold
Store multi-currency balances
Ramp
Bridge fiat and crypto
Bill
Invoice and collect payments
Program
Create programmable flows
Identify
KYC/KYB verification
What you can build Combine primitives, ship products
Building on Grid
Coinbase SoFi Nu Revolut Bitso
# 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 Payouts & B2B hero 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 Payouts quickstart hero Payouts quickstart hero 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**. Entity relationships diagram showing platform, customers, internal accounts, external accounts, transactions, and UMA addresses Entity relationships diagram showing platform, customers, internal accounts, external accounts, 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 Building with AI hero Building with AI hero 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 Capabilities hero Capabilities hero 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**. Entity relationships diagram showing platform, customers, internal accounts, external accounts, transactions, and UMA addresses Entity relationships diagram showing platform, customers, internal accounts, external accounts, 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 What is Grid hero What is Grid hero 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 Ramps hero 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 Ramps quickstart hero Ramps quickstart hero 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**. Entity relationships diagram showing platform, customers, internal accounts, external accounts, transactions, and UMA addresses Entity relationships diagram showing platform, customers, internal accounts, external accounts, 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 Rewards hero 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 Rewards quickstart hero Rewards quickstart hero 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**. Entity relationships diagram showing platform, customers, internal accounts, external accounts, transactions, and UMA addresses Entity relationships diagram showing platform, customers, internal accounts, external accounts, 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.