Headless Payments
Run complete payment flows via API - no UI required
Using Helio, you can make end-to-end headless payments with no UI required. This is ideal for seamless integrations with third-party services, allowing merchants to accept payments through Helio without a UI.
This feature is currently supported only on the Solana network.
How It Works
- Merchants generate Pay Links that define the payment request, including supported currencies.
- Payers prepare a transaction via the API using the Pay Link and their public key.
- Helio provides the transaction payload (serialized transaction, message, and token) needed for signing.
- Payers sign and submit the transaction, completing the payment fully through the API without a UI.
How to Get Started
- Create a Pay Link
The merchant creates and shares a Pay Link ID. Optionally, they may also specify a currency ID.
- If no currency is provided, it will default to USDC.
- If USDC is not available, the first enabled currency on the Pay Link will be used.
- Prepare the Transaction
The payer calls the prepare endpoint, passing the following parameters:
paymentRequestId
- The Pay Link IDsenderPublicKey
- the payer's public keycurrencyId
(optional)- any other required fields (see API reference for full list)
- Sign the Transaction
The /prepare
endpoint responds with:
transactionToken
- Identifier required to submit the signed transaction.transactionMessage
- Metadata or message tied to the transaction.serializedTransaction
- Unsigned transaction data to be signed by the payer.addressLookupTableAccounts
- Accounts used to resolve addresses in the transaction.
To sign the transaction, you can use the WalletService
helper shown below:
import { Keypair, VersionedTransaction } from '@solana/web3.js'
import bs58 from 'bs58'
import { PrepareTransactionExtended } from '@heliofi/common'
class WalletService {
private getPrivateKey(): string {
// The base58 encoded private key.
const privateKey = import.meta.env.VITE_WALLET_PRIVATE_KEY
if (!privateKey) {
throw new Error('Wallet private key not configured.')
}
return privateKey
}
createWallet() {
try {
const privateKey = this.getPrivateKey()
// The Keypair object built from the private key.
return Keypair.fromSecretKey(bs58.decode(privateKey))
} catch (error) {
console.error('Error creating wallet:', error)
throw error
}
}
async signTransaction(
preparedTransaction: PrepareTransactionExtended,
): Promise<string> {
try {
const wallet = this.createWallet()
// The serialized transaction from the prepared transaction (The response from the prepare endpoint).
const { serializedTransaction } = preparedTransaction
// Create a VersionedTransaction object from the serialized transaction.
const vtx = VersionedTransaction.deserialize(
Buffer.from(serializedTransaction!, 'base64'),
)
// Sign the transaction with the wallet.
vtx.sign([wallet])
// Serialize the signed transaction and return it as a base64 string.
const signedAsBufferJson = Buffer.from(vtx.serialize()).toString('base64')
return signedAsBufferJson
} catch (error) {
console.error('Error signing transaction:', error)
throw new Error(
`Failed to sign transaction: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
getWalletAddress(): string {
try {
const wallet = this.createWallet()
return wallet.publicKey.toBase58()
} catch (error) {
console.error('Error getting wallet address:', error)
throw error
}
}
}
export const walletService = new WalletService()
- Submit the Transaction
The payer calls the submit endpoint, providing:
- the
signedTransaction
value - the
transactionToken
from the/prepare
response.
This submits the fully signed transaction for processing.
Updated 13 days ago