Skip to content

Setting up payments

Integrating Funtico payments into your game or application involves creating transactions, handling user authentication, and managing payment flows. This guide covers the essential patterns and best practices for a successful integration using a secure backend approach.

The Funtico payment integration consists of three main components:

  1. Transaction Creation - Your backend creates transactions via API
  2. User Authentication - Users authenticate with Funtico to access their TICO balance
  3. Payment Processing - Users complete payments through Funtico’s secure checkout

Before integrating payments, ensure you have:

  • Funtico Account - Register as a developer at Funtico Developer Portal
  • API Credentials - PSP Client ID and Secret for transaction creation
  • Auth Credentials - Auth Client ID and Secret for user authentication
  • SDK Installation - Install the appropriate SDK for your platform
Terminal window
npm install @pillarex/funtico-sdk

Transactions are created on your backend server to ensure security and prevent client-side manipulation:

import { FunticoSDK } from '@pillarex/funtico-sdk';
const sdk = new FunticoSDK({
pspClientId: process.env.FUNTICO_PSP_CLIENT_ID,
pspClientSecret: process.env.FUNTICO_PSP_CLIENT_SECRET,
env: process.env.NODE_ENV === 'production' ? 'production' : 'staging'
});
// Create a transaction
const transaction = await sdk.createTransaction({
items: [
{
name: "Magic Sword",
description: "A powerful weapon for your character",
image_url: "https://your-game.com/images/magic-sword.png",
quantity: 1,
unit_price: 10.0, // 10.0 TICO
currency: "tico",
metadata: {
item_id: "sword_001",
rarity: "epic",
category: "weapon"
}
}
],
success_url: "https://your-game.com/success",
cancel_url: "https://your-game.com/cancel",
ui_mode: "redirect",
expiration_sec: 3600, // 1 hour
metadata: {
user_id: "user_123",
session_id: "session_456"
}
});
  • Required fields: name, description, image_url, metadata, quantity, unit_price, currency
  • Currency: Use "tico" for TICO cryptocurrency payments or "usd" for USD
  • Pricing: Specify prices as numbers (e.g., 10.0 for 10.0 TICO)
  • Description/Image: Can be null but must be provided in the request
  • redirect - User is redirected to checkout and back to your game
  • new_tab - Checkout opens in new tab/window and closes when complete

Set appropriate expiration times based on your use case:

  • Short sessions (5-15 minutes): 900-1800 seconds
  • Standard purchases (30-60 minutes): 1800-3600 seconds
  • Complex flows (1-2 hours): 3600-7200 seconds

Users must authenticate with Funtico to access their TICO balance. Handle authentication on your backend for security:

// Backend: Initialize auth SDK
const authSDK = new FunticoSDK({
authClientId: process.env.FUNTICO_AUTH_CLIENT_ID,
authClientSecret: process.env.FUNTICO_AUTH_CLIENT_SECRET,
env: process.env.NODE_ENV === 'production' ? 'production' : 'staging'
});
// Backend: POST /auth/login
app.post('/auth/login', async (req, res) => {
try {
const { codeVerifier, redirectUrl, state } = await authSDK.signInWithFuntico({
callbackUrl: "https://your-game.com/auth/callback",
scopes: ['openid', 'profile', 'email', 'offline_access', 'balance:read', 'transactions:read']
});
// Store codeVerifier in secure cookie
res.cookie(`state_${state}`, codeVerifier, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 10 * 60 * 1000 // 10 minutes
});
res.json({ redirectUrl });
} catch (error) {
res.status(500).json({ error: 'Failed to initiate login' });
}
});

After user authentication, exchange the authorization code for tokens on your backend:

// Backend: Handle OAuth callback
app.get('/auth/callback', async (req, res) => {
try {
const state = req.query.state as string;
const code = req.query.code as string;
if (!state || !code) {
return res.status(400).json({ error: 'Missing state or code parameter' });
}
const codeVerifier = req.cookies[`state_${state}`];
if (!codeVerifier) {
return res.status(400).json({ error: 'Invalid or expired state' });
}
res.clearCookie(`state_${state}`);
// Exchange code for tokens
const { accessToken, refreshToken } = await authSDK.getTokens({
codeVerifier,
url: req.url
});
// Store tokens in secure cookies
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 1000 // 1 hour
});
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.redirect('https://your-game.com/dashboard?login=success');
} catch (error) {
res.status(500).json({ error: 'Failed to handle callback' });
}
});

Your frontend handles the user journey from purchase intent to payment completion:

// 1. User clicks purchase button
async function handlePurchase(itemId: string) {
try {
// 2. Create transaction on backend with authentication cookies
const response = await fetch('/api/transactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Include authentication cookies
body: JSON.stringify({ itemId })
});
if (!response.ok) {
if (response.status === 401) {
// User not authenticated, redirect to login
window.location.href = '/login';
return;
}
throw new Error('Failed to create transaction');
}
const { transaction } = await response.json();
// 3. Redirect to Funtico checkout
window.location.href = transaction.checkout_url;
} catch (error) {
console.error('Failed to create transaction:', error);
// Show user-friendly error message
showError('Unable to start payment. Please try again.');
}
}
// Helper function for login
async function initiateLogin() {
try {
const response = await fetch('/auth/login', { method: 'POST' });
const { redirectUrl } = await response.json();
window.location.href = redirectUrl;
} catch (error) {
console.error('Failed to start login:', error);
}
}

Your backend API creates transactions securely:

// POST /api/transactions
app.post('/api/transactions', async (req, res) => {
try {
const { itemId } = req.body;
// Validate user authentication via cookies
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({ error: 'Authentication required' });
}
// Get user ID from token or session
const userId = req.user?.id; // Assuming middleware sets req.user
// Get item details from your database
const item = await getItemById(itemId);
// Create transaction
const transaction = await sdk.createTransaction({
items: [{
name: item.name,
description: item.description,
image_url: item.imageUrl,
quantity: 1,
unit_price: item.price, // Price as decimal number
currency: "tico",
metadata: {
item_id: item.id,
category: item.category
}
}],
success_url: `${process.env.GAME_URL}/purchase/success`,
cancel_url: `${process.env.GAME_URL}/purchase/cancel`,
ui_mode: "redirect",
expiration_sec: 1800, // 30 minutes
metadata: {
user_id: userId,
item_id: itemId
}
});
res.json({ transaction });
} catch (error) {
console.error('Transaction creation failed:', error);
res.status(500).json({ error: 'Failed to create transaction' });
}
});

For simple integrations, poll the transaction status:

// Check transaction status periodically
async function pollTransactionStatus(transactionId: string) {
const maxAttempts = 60; // 5 minutes with 5-second intervals
let attempts = 0;
const poll = async () => {
try {
const transaction = await sdk.getTransactionById(transactionId);
if (transaction) {
switch (transaction.current_status) {
case 'completed':
handlePaymentSuccess(transaction);
return;
case 'failed':
case 'cancelled':
case 'expired':
handlePaymentFailure(transaction);
return;
case 'created':
case 'confirmed':
// Continue polling
break;
}
}
attempts++;
if (attempts < maxAttempts) {
setTimeout(poll, 5000); // Poll every 5 seconds
} else {
handlePaymentTimeout();
}
} catch (error) {
console.error('Failed to check transaction status:', error);
}
};
poll();
}

For production applications, implement webhook handling:

// Webhook endpoint
app.post('/webhooks/funtico', async (req, res) => {
try {
const { transaction_id, status, reason } = req.body;
// Verify webhook signature (implement signature verification)
if (!verifyWebhookSignature(req)) {
return res.status(401).send('Unauthorized');
}
// Update transaction status in your database
await updateTransactionStatus(transaction_id, status, reason);
// Handle status-specific logic
switch (status) {
case 'completed':
await grantItemToUser(transaction_id);
break;
case 'failed':
await handlePaymentFailure(transaction_id, reason);
break;
}
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing failed:', error);
res.status(500).send('Internal Server Error');
}
});
// Handle different error types
try {
const transaction = await sdk.createTransaction(transactionData);
} catch (error) {
if (isSDKError(error)) {
switch (error.name) {
case 'invalid_body_format':
// Handle malformed request data
console.error('Invalid transaction data:', error);
break;
case 'transaction_not_found':
// Handle missing transaction
console.error('Transaction not found:', error);
break;
default:
// Handle other SDK errors
console.error('SDK error:', error);
}
} else {
// Handle network or other errors
console.error('Unexpected error:', error);
}
}
function getErrorMessage(error: unknown): string {
if (isSDKError(error)) {
switch (error.name) {
case 'invalid_body_format':
return 'Invalid payment request. Please try again.';
case 'transaction_not_found':
return 'Payment session expired. Please try again.';
default:
return 'Payment service temporarily unavailable. Please try again later.';
}
}
return 'An unexpected error occurred. Please try again.';
}
  • Never expose API credentials in frontend code
  • Use HTTP-only cookies for token storage, never localStorage
  • Validate all input data before creating transactions
  • Implement webhook signature verification for production
  • Use HTTPS for all API communications
  • Backend-only authentication - handle OAuth flows on backend
  • Token expiration - Access tokens expire in 1 hour, refresh tokens in 7 days
  • Provide clear feedback during payment processing
  • Handle all transaction states (success, failure, cancellation, expiration)
  • Implement retry logic for failed transactions
  • Show appropriate loading states during API calls
  • Display transaction details before user confirmation
  • Cache transaction data when appropriate
  • Implement efficient polling with reasonable intervals
  • Use webhooks for real-time updates in production
  • Optimize API calls to minimize latency
  • Use sandbox environment for development and testing
  • Test all transaction states and error scenarios
  • Verify webhook handling with test transactions
  • Test expiration handling with short timeouts
const sdk = new FunticoSDK({
pspClientId: process.env.FUNTICO_PSP_CLIENT_ID,
pspClientSecret: process.env.FUNTICO_PSP_CLIENT_SECRET,
env: 'staging'
});
const sdk = new FunticoSDK({
pspClientId: process.env.FUNTICO_PSP_CLIENT_ID,
pspClientSecret: process.env.FUNTICO_PSP_CLIENT_SECRET,
env: 'production'
});