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

  1. 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.
  1. Prepare the Transaction

The payer calls the prepare endpoint, passing the following parameters:

  • paymentRequestId - The Pay Link ID
  • senderPublicKey - the payer's public key
  • currencyId (optional)
  • any other required fields (see API reference for full list)
  1. 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()
  1. 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.