The Checkout API lets you collect payments directly from your website or app using Tsara's secure, pre-built payment interface.
You can:
- Redirect customers to a hosted Checkout page, or
- Embed Checkout directly in your app with a simple JavaScript snippet.
Tsara handles the entire payment flow — from customer input to authorization and verification — so you can focus on your product.
Overview
| Feature | Description |
|---|---|
| Hosted Checkout | Redirect customers to a Tsara-hosted payment page. |
| Embedded Checkout | Launch Checkout directly inside your app using JavaScript. |
| Supported Methods | Card, Bank Transfer, Stablecoin (USDC). |
| Currencies | NGN, USDC. |
| Webhooks | Real-time events for payment.success, payment.failed. |
Key Differences: Hosted vs Embedded
| Feature | Hosted Checkout | Embedded Checkout |
|---|---|---|
| Integration | Simple redirect to Tsara URL | Requires JavaScript SDK |
| User Experience | Full page redirect | Modal/popup on your site |
| Customization | Limited (logo, colors via dashboard) | Full control over trigger button |
| Mobile Friendly | Yes | Yes |
| Use Case | Quick integration, email links | Seamless in-app experience |
Create a Checkout Session
Generate a new checkout session that can be used for hosted or embedded checkout.
Endpoint
POST /checkout/create
Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer YOUR_SECRET_KEY | Yes |
Content-Type | application/json | Yes |
Request Parameters
| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
amount | number | Yes | Payment amount in minor units for NGN (kobo) or full units for USDC | 200000 (₦2,000.00) |
currency | string | Yes | Currency code. Accepts: NGN, USDC | "NGN" |
trx_id | string | Yes | Your unique transaction reference (max 100 chars, alphanumeric, hyphen, underscore) | "order_001" |
public_key | string | Yes | Your Tsara public key (starts with pk_test_ or pk_live_) | "pk_test_abc123..." |
email | string | Yes | Customer email address (valid email format) | "[email protected]" |
name | string | No | Customer full name (max 100 chars) | "John Doe" |
phone | string | No | Customer phone number (Nigerian format: 11 digits starting with 0) | "08012345678" |
redirect_url | string | No | URL to redirect after payment (any status). Overrides success_url and cancel_url | "https://example.com/callback" |
success_url | string | No | URL to redirect after successful payment (used if redirect_url not set) | "https://example.com/success" |
cancel_url | string | No | URL to redirect if payment cancelled or failed (used if redirect_url not set) | "https://example.com/cancel" |
meta | object | No | Custom metadata for tracking (max 10 keys, 500 chars per value) | {"order_id": "ORD-123"} |
customer | object | No | Customer details object (alternative to individual fields) | See below |
customizations | object | No | Checkout page customizations (title, description, logo) | See below |
Customer Object Structure
{
"customer": {
"email": "[email protected]",
"phone": "08012345678",
"name": "John Doe"
}
}Customizations Object Structure
{
"customizations": {
"title": "Your Business Name",
"description": "Payment for Order #123",
"logo": "https://example.com/logo.png"
}
}| Field | Type | Description |
|---|---|---|
title | string | Business name shown at top of checkout (max 50 chars) |
description | string | Payment description shown to customer (max 200 chars) |
logo | string | URL to your logo image (HTTPS only, max 2MB, square recommended) |
Important: Public Key vs Secret Key
- Secret Key (e.g.,
sk_test_...) - Use for server-side API calls (this endpoint) - Public Key (e.g.,
pk_test_...) - Include in request body for session creation and use in JavaScript SDK - Never expose your Secret Key in frontend code
Request Example
{
"public_key": "pk_test_0fbvh9l559sewud83m219wofxcpizxdm",
"amount": 200000,
"currency": "NGN",
"trx_id": "order_001_1738318449",
"email": "[email protected]",
"name": "John Doe",
"phone": "08012345678",
"redirect_url": "https://example.com/payment-callback",
"meta": {
"order_id": "ORD-123",
"customer_id": "cus_456"
},
"customizations": {
"title": "Acme Store",
"description": "Payment for Order #ORD-123",
"logo": "https://acmestore.com/logo.png"
}
}curl -X POST "https://sandbox.tsara.ng/v1/checkout/create" \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"public_key": "pk_test_0fbvh9l559sewud83m219wofxcpizxdm",
"amount": 200000,
"currency": "NGN",
"trx_id": "order_001_1738318449",
"email": "[email protected]",
"name": "John Doe",
"phone": "08012345678",
"redirect_url": "https://example.com/payment-callback",
"meta": {
"order_id": "ORD-123"
}
}'Response
{
"success": true,
"status": "success",
"status_code": 200,
"message": "Checkout Created",
"data": {
"id": "sess_695fc9b9d4e9920024465a46",
"status": "pending",
"checkout_url": "https://pay.sandbox.tsara.ng/checkout/sess_695fc9b9d4e9920024465a46",
"expires_at": "2026-01-08T16:54:09Z",
"amount": 200000,
"currency": "NGN",
"account": {
"account_number": "6028678511",
"account_name": "STL Checkout",
"bank_code": "090286",
"bank_name": "Safe Haven MFB",
"SOLANA": "FqVQHH3mSAEKXEKN4JtZeFuhosQ14B3HXyLdeT3Wqedg",
"_id": "695fc9b9d4e9920024465a46",
"business_uid": "66ac4ae2e5310c6d9bc615bf"
}
},
"url": "https://pay.sandbox.tsara.ng/checkout/sess_695fc9b9d4e9920024465a46",
"trx_id": "order_001_1738318449",
"account": {
"account_number": "6028678511",
"account_name": "STL Checkout",
"bank_code": "090286",
"bank_name": "Safe Haven MFB",
"SOLANA": "FqVQHH3mSAEKXEKN4JtZeFuhosQ14B3HXyLdeT3Wqedg",
"_id": "695fc9b9d4e9920024465a46",
"business_uid": "66ac4ae2e5310c6d9bc615bf"
}
}Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | Request success status |
status | string | Request status text |
status_code | number | HTTP status code |
message | string | Human-readable message |
data.id | string | Checkout session ID |
data.status | string | Session status. Options: pending, completed, failed, expired |
data.checkout_url | string | URL for hosted checkout page |
data.expires_at | string | Session expiration timestamp (ISO 8601) |
data.amount | number | Payment amount |
data.currency | string | Currency code |
data.account | object | Virtual account details for bank transfer payments |
data.account.account_number | string | Nigerian bank account number for transfers |
data.account.account_name | string | Account name |
data.account.bank_code | string | Nigerian bank code |
data.account.bank_name | string | Bank name |
data.account.SOLANA | string | Solana wallet address for USDC payments |
url | string | Checkout URL (same as data.checkout_url) |
trx_id | string | Your transaction reference |
account | object | Account details (same as data.account) |
Session Status Values
| Status | Description |
|---|---|
pending | Session created, awaiting payment |
completed | Payment successful |
failed | Payment failed |
expired | Session expired before payment (24 hours default) |
Session Expiration
- Default expiration: 24 hours from creation
- After expiration, the
checkout_urland virtual account become invalid - Create a new session for expired checkouts
Error Responses
{
"success": false,
"status_code": 400,
"error": {
"code": "validation_error",
"message": "Invalid request parameters",
"details": [
{
"field": "amount",
"message": "Amount must be greater than 0"
},
{
"field": "email",
"message": "Invalid email format"
}
]
}
}Common Errors
| Status Code | Error Code | Description |
|---|---|---|
| 400 | validation_error | Invalid request parameters (check details array) |
| 400 | invalid_currency | Currency not supported. Use NGN or USDC |
| 400 | duplicate_trx_id | Transaction ID already used. Use unique trx_id per checkout |
| 400 | invalid_public_key | Public key is invalid or doesn't match your account |
| 401 | unauthorized | Invalid or missing Secret Key in Authorization header |
| 429 | rate_limit_exceeded | Too many requests. Retry after delay |
Hosted Checkout Flow
Redirect the customer to the checkout_url provided in the API response.
Implementation
<a href="https://pay.sandbox.tsara.ng/checkout/sess_123" class="btn btn-primary">
Pay ₦2,000.00
</a>Or redirect programmatically:
const response = await fetch('https://sandbox.tsara.ng/v1/checkout/create', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_SECRET_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
public_key: 'pk_test_...',
amount: 200000,
currency: 'NGN',
trx_id: 'order_001',
email: '[email protected]',
redirect_url: 'https://example.com/callback'
})
});
const data = await response.json();
window.location.href = data.data.checkout_url;Payment Flow
- Customer clicks payment button
- Redirected to Tsara hosted checkout page
- Customer selects payment method (Card, Bank Transfer, USDC)
- Customer completes payment
- Tsara redirects back to your site
Redirect URLs
After payment, customers are redirected to:
redirect_url(if provided) - Used for all payment statusessuccess_url(ifredirect_urlnot set) - Used only for successful paymentscancel_url(ifredirect_urlnot set) - Used for cancelled/failed payments
Query Parameters in Redirect
Tsara appends these parameters to your redirect URL:
https://example.com/callback?status=success&trx_id=order_001&reference=pay_123
| Parameter | Description |
|---|---|
status | Payment status: success, failed, cancelled |
trx_id | Your transaction reference |
reference | Tsara payment ID |
⚠️ Always verify payment on your server - Don't trust client-side parameters alone.
Embedded Checkout (JavaScript SDK)
Embed Checkout directly inside your app using Tsara's JavaScript SDK for a seamless experience.
Step 1: Include the Script
Add the Tsara Checkout script to your HTML:
<script src="https://checkout.tsara.ng/inline.js?v=1"></script>Script Versions
- Latest:
https://checkout.tsara.ng/inline.js?v=1 - CDN (alternative):
https://js.tsara.ng/v1/checkout.js
Step 2: Add a Payment Button
<button id="payBtn" class="btn btn-primary">Pay Now</button>Step 3: Initialize Checkout
Method 1: Direct Initialization (Recommended)
document.getElementById('payBtn').addEventListener('click', function() {
tsara.pay({
public_key: "pk_test_0fbvh9l559sewud83m219wofxcpizxdm",
trx_id: "order_" + Date.now(),
amount: 200000,
currency: "NGN",
email: "[email protected]",
phone: "08012345678",
name: "John Doe",
meta: {
order_id: "ORD-123",
customer_id: "cus_456"
},
customizations: {
title: "Acme Store",
description: "Order Payment",
logo: "https://example.com/logo.png"
},
redirect_url: "https://example.com/payment-callback",
onSuccess: function(response) {
console.log('Payment successful:', response);
window.location.href = '/success?ref=' + response.reference;
},
onCancel: function() {
console.log('Payment cancelled');
alert('Payment was cancelled');
},
onError: function(error) {
console.error('Payment error:', error);
alert('Payment failed: ' + error.message);
}
});
});Method 2: Legacy Function Wrapper
function fundWithTsara(data) {
tsara.pay({
public_key: "pk_test_0fbvh9l559sewud83m219wofxcpizxdm",
trx_id: data.trx_id,
amount: data.amount,
currency: "NGN",
email: data.email,
phone: data.phone,
name: data.name,
meta: {
customer_id: data.user_id
},
customer: {
email: data.email,
phone: data.phone,
name: data.name
},
redirect_url: data.redirect_url,
customizations: {
title: "Your Business",
description: "Payment Description",
logo: "https://example.com/logo.png"
}
});
}
document.querySelector('.pay').addEventListener('click', function() {
fundWithTsara({
trx_id: generateTrxId(),
amount: 100000,
email: '[email protected]',
phone: '08012345678',
name: 'John Doe',
user_id: 'user_123',
redirect_url: 'https://example.com/callback'
});
});JavaScript SDK Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
public_key | string | Yes | Your Tsara public key |
trx_id | string | Yes | Unique transaction reference |
amount | number | Yes | Payment amount (minor units for NGN, full units for USDC) |
currency | string | Yes | Currency code (NGN or USDC) |
email | string | Yes | Customer email |
phone | string | No | Customer phone number |
name | string | No | Customer full name |
meta | object | No | Custom metadata |
customer | object | No | Customer details object |
customizations | object | No | Checkout customizations |
redirect_url | string | No | Post-payment redirect URL |
onSuccess | function | No | Callback for successful payment |
onCancel | function | No | Callback when payment is cancelled |
onError | function | No | Callback for payment errors |
Callback Functions
onSuccess: function(response) {
console.log('Payment successful');
console.log('Reference:', response.reference);
console.log('Transaction ID:', response.trx_id);
console.log('Amount:', response.amount);
}
onCancel: function() {
console.log('Payment cancelled by user');
}
onError: function(error) {
console.log('Payment error:', error.message);
console.log('Error code:', error.code);
}Callback Response Object
| Field | Type | Description |
|---|---|---|
reference | string | Tsara payment reference |
trx_id | string | Your transaction ID |
amount | number | Payment amount |
currency | string | Currency code |
status | string | Payment status |
Complete Embedded Checkout Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tsara Checkout Example</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="card">
<div class="card-body">
<h5 class="card-title">Order Summary</h5>
<p class="card-text">Amount: ₦2,000.00</p>
<button class="btn btn-primary" id="payBtn">Pay Now</button>
</div>
</div>
</div>
<script src="https://checkout.tsara.ng/inline.js?v=1"></script>
<script>
function generateTrxId() {
return 'trx_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6);
}
document.getElementById('payBtn').addEventListener('click', function() {
tsara.pay({
public_key: "pk_test_0fbvh9l559sewud83m219wofxcpizxdm",
trx_id: generateTrxId(),
amount: 200000,
currency: "NGN",
email: "[email protected]",
phone: "08012345678",
name: "John Doe",
meta: {
order_id: "ORD-123"
},
customizations: {
title: "Acme Store",
description: "Order Payment",
logo: "https://example.com/logo.png"
},
onSuccess: function(response) {
alert('Payment successful!');
window.location.href = '/success?ref=' + response.reference;
},
onCancel: function() {
alert('Payment cancelled');
},
onError: function(error) {
alert('Payment failed: ' + error.message);
}
});
});
</script>
</body>
</html>Payment Methods
When customers reach the checkout page (hosted or embedded), they can choose from:
1. Card Payment
- Supports Visa, Mastercard, Verve
- Instant confirmation
- 3D Secure authentication
- Best for: Immediate payments
2. Bank Transfer
- Virtual account generated for each session
- Account details shown in checkout
- Payment confirmed within minutes
- Best for: Larger amounts, customers without cards
Virtual Account Details (from API response)
{
"account_number": "6028678511",
"account_name": "STL Checkout",
"bank_name": "Safe Haven MFB",
"bank_code": "090286"
}Customers can transfer to this account via:
- Mobile banking apps
- USSD
- Internet banking
- Bank branch
3. USDC (Stablecoin)
- Solana blockchain
- Near-instant confirmation
- Low transaction fees
- Best for: Crypto-native customers, cross-border
USDC Wallet Address (from API response)
{
"SOLANA": "FqVQHH3mSAEKXEKN4JtZeFuhosQ14B3HXyLdeT3Wqedg"
}Important: USDC Amount Format
- NGN uses minor units:
200000= ₦2,000.00 - USDC uses full units:
50= 50 USDC (not 0.00050 USDC)
Verify Payment
Always verify payments on your server before fulfilling orders, even after receiving webhooks or redirect callbacks.
Endpoint
GET /payments?id={trx_id}
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Your transaction reference (trx_id) |
Example Request
curl -X GET "https://sandbox.tsara.ng/v1/payments?id=order_001" \
-H "Authorization: Bearer YOUR_SECRET_KEY"Response
{
"success": true,
"status": "success",
"status_code": 200,
"message": "Payment retrieved",
"data": {
"id": "pay_695fc9b9d4e992",
"reference": "order_001",
"status": "success",
"amount": 200000,
"currency": "NGN",
"payment_method": "card",
"customer": {
"email": "[email protected]",
"phone": "08012345678",
"name": "John Doe"
},
"meta": {
"order_id": "ORD-123"
},
"paid_at": "2025-01-31T12:00:00Z",
"created_at": "2025-01-31T11:55:00Z"
}
}Payment Status Values
| Status | Description | Action |
|---|---|---|
success | Payment successful | Fulfill order |
pending | Payment initiated, awaiting confirmation | Wait for webhook or poll |
failed | Payment failed | Show error, allow retry |
cancelled | User cancelled payment | Allow retry |
Verification Flow
async function verifyPayment(trxId) {
const response = await fetch(`https://sandbox.tsara.ng/v1/payments?id=${trxId}`, {
headers: {
'Authorization': 'Bearer YOUR_SECRET_KEY'
}
});
const data = await response.json();
if (data.success && data.data.status === 'success') {
await fulfillOrder(data.data.reference);
return true;
}
return false;
}Webhook Notifications
Tsara sends webhook events for every checkout payment attempt.
Event Types
| Event | Description |
|---|---|
payment.success | Payment completed successfully |
payment.failed | Payment attempt failed |
payment.pending | Payment initiated, awaiting confirmation (bank transfers) |
Example Webhook Payload: payment.success
{
"id": "evt_456",
"type": "payment.success",
"created_at": "2025-01-31T12:00:00Z",
"data": {
"payment": {
"id": "pay_695fc9b9d4e992",
"reference": "order_001",
"amount": 200000,
"currency": "NGN",
"status": "success",
"payment_method": "card",
"session_id": "sess_695fc9b9d4e9920024465a46",
"customer": {
"email": "[email protected]",
"phone": "08012345678",
"name": "John Doe"
},
"meta": {
"order_id": "ORD-123",
"customer_id": "cus_456"
},
"paid_at": "2025-01-31T12:00:00Z"
}
}
}Webhook Handler Example
const express = require('express');
const crypto = require('crypto');
app.post('/webhooks/tsara', express.raw({type: 'application/json'}), (req, res) => {
const payload = req.body.toString();
const signature = req.headers['x-tsara-signature'];
const expected = crypto
.createHmac('sha512', process.env.TSARA_WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (signature !== expected) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(payload);
if (event.type === 'payment.success') {
const payment = event.data.payment;
await fulfillOrder(payment.reference, payment);
}
res.sendStatus(200);
});See Webhooks Security for detailed webhook handling guide.
Use Cases & Examples
E-commerce Checkout
const cart = getShoppingCart();
const total = calculateTotal(cart);
const checkoutResponse = await createCheckoutSession({
amount: total,
currency: 'NGN',
trx_id: `order_${orderId}`,
email: customer.email,
meta: {
order_id: orderId,
items: cart.map(i => i.id).join(',')
},
success_url: `https://store.com/orders/${orderId}/success`,
cancel_url: `https://store.com/cart`
});
window.location.href = checkoutResponse.data.checkout_url;Subscription Payment
tsara.pay({
public_key: PUBLIC_KEY,
trx_id: `sub_${customerId}_${Date.now()}`,
amount: 500000,
currency: 'NGN',
email: customer.email,
meta: {
subscription_id: subscription.id,
plan: 'pro',
billing_cycle: 'monthly'
},
customizations: {
title: 'Monthly Subscription',
description: 'Pro Plan - ₦5,000/month'
},
onSuccess: function(response) {
activateSubscription(subscription.id);
}
});Donation Page
const donationAmount = document.getElementById('amount').value;
tsara.pay({
public_key: PUBLIC_KEY,
trx_id: `donation_${Date.now()}`,
amount: donationAmount * 100,
currency: 'NGN',
email: donor.email,
name: donor.name,
meta: {
cause: 'education',
campaign_id: 'camp_123'
},
customizations: {
title: 'Support Education',
description: 'Your donation makes a difference'
}
});USDC Crypto Payment
{
"amount": 100,
"currency": "USDC",
"trx_id": "crypto_payment_001",
"email": "[email protected]",
"meta": {
"payment_type": "crypto",
"product": "premium_plan"
}
}Note: Amount is in full USDC units (100 = 100 USDC, not 0.001 USDC)
Tips & Best Practices
-
Always verify payments server-side
app.get('/payment-callback', async (req, res) => { const isValid = await verifyPayment(req.query.trx_id); if (isValid) { await fulfillOrder(req.query.trx_id); } res.redirect('/order-confirmation'); }); -
Use unique transaction IDs
const trxId = `order_${orderId}_${Date.now()}_${randomString()}`;Prevents duplicate payments if user refreshes checkout page.
-
Handle webhooks asynchronously
app.post('/webhooks/tsara', (req, res) => { queue.add('process-payment', req.body); res.sendStatus(200); }); -
Add metadata for tracking
{ "meta": { "order_id": "ORD-123", "customer_id": "cus_456", "source": "mobile_app", "campaign": "summer_sale" } } -
Test in sandbox thoroughly
- Use
pk_test_...public keys - Test all payment methods (card, bank transfer, USDC)
- Test success, failure, and cancellation flows
- Use
-
Implement proper error handling
tsara.pay({ ...paymentData, onError: function(error) { logError(error); showUserFriendlyMessage(); enableRetryButton(); } }); -
Set meaningful customizations
{ "customizations": { "title": "Acme Store", "description": "Order #ORD-123 - 3 items", "logo": "https://acmestore.com/logo.png" } } -
Monitor session expiration
- Sessions expire after 24 hours
- Show countdown timer on checkout page
- Create new session if expired
-
Handle mobile users
- Embedded checkout works in mobile browsers
- Test on iOS Safari and Android Chrome
- Ensure redirect URLs are mobile-friendly
-
Amount formatting
- NGN:
₦2,000.00=200000(kobo) - USDC:
50 USDC=50(full units)
- NGN:
Troubleshooting
Checkout page not loading
Cause: Invalid session ID or expired session.
Solution: Check session expiration and create a new session:
const response = await createCheckoutSession(paymentData);
if (response.data.expires_at < new Date()) {
console.log('Session expired, creating new one');
}Duplicate transaction error
Cause: trx_id already used for a previous checkout.
Solution: Generate unique transaction IDs:
const trxId = `order_${orderId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;JavaScript SDK not loading
Cause: Script blocked by ad blocker or CSP policy.
Solution:
- Check browser console for errors
- Add Tsara domains to CSP whitelist:
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://checkout.tsara.ng;"> - Test in incognito mode without extensions
Payment successful but webhook not received
Cause: Webhook URL not configured or endpoint returning errors.
Solution:
- Configure webhook URL in dashboard: Settings → Developers → Webhooks
- Ensure endpoint returns 200 OK
- Verify signature validation is correct
- Check webhook logs in dashboard
Redirect URL not working
Cause: URL not properly encoded or contains invalid characters.
Solution: Use proper URL encoding:
const redirectUrl = encodeURIComponent('https://example.com/callback?order=123&customer=456');Amount showing incorrectly
Cause: Wrong amount format for currency.
Solution:
- NGN: Use kobo (minor units)
- ₦2,000.00 →
200000 - ₦50.50 →
5050
- ₦2,000.00 →
- USDC: Use full units
- 50 USDC →
50 - 0.5 USDC →
0.5
- 50 USDC →
Customizations not showing
Cause: Logo URL inaccessible or wrong format.
Solution:
- Logo URL must be HTTPS
- Image must be publicly accessible
- Recommended: Square image, max 2MB
- Supported formats: PNG, JPG, SVG
Related Pages
- Payment Links — Create simple shareable links for collection
- Verify Payments — Confirm payment status programmatically
- Webhooks — Learn how to handle event callbacks securely
- Webhooks Security — Signature verification guide
- Errors & Status Codes — Complete error reference