Skip to main content

Testing

Comprehensive guide to testing your Omise integration using test mode, test cards, and simulated payment scenarios across all payment methods.

Overviewโ€‹

Omise provides a complete testing environment that mirrors production behavior without processing real transactions. Test mode allows you to validate your integration, test error handling, and simulate various payment scenarios before going live.

Key Features:

  • โœ… Separate test keys - Dedicated API keys for testing
  • โœ… Test cards - Simulate success, decline, and error scenarios
  • โœ… All payment methods - Test QR, wallets, banking, and more
  • โœ… Webhook testing - Receive test webhooks
  • โœ… No real money - Safe testing environment
  • โœ… Full feature parity - Test environment mirrors production

Test Mode vs Live Modeโ€‹

FeatureTest ModeLive Mode
API Keyspkey_test_..., skey_test_...pkey_..., skey_...
Real MoneyNoYes
Test CardsAcceptedRejected
Real CardsRejectedAccepted
WebhooksDeliveredDelivered
DashboardTest data onlyLive transactions
Important

Test and live modes are completely isolated. Test keys cannot process real transactions, and live keys cannot use test data.

Getting Test API Keysโ€‹

From Dashboardโ€‹

  1. Log in to Omise Dashboard
  2. Switch to Test Mode using the toggle
  3. Navigate to Settings โ†’ API Keys
  4. Copy your test keys:
    • Public Key: pkey_test_... (client-side)
    • Secret Key: skey_test_... (server-side)

Test Key Formatโ€‹

# Public key (safe to use client-side)
pkey_test_5rt6s9vah5lkvi1rh9c

# Secret key (server-side only, never expose)
skey_test_5rt6s9vah5lkvi1rh9c
Security

Never commit test keys to public repositories or expose them in client-side code. While test keys don't process real money, they can still access sensitive test data.

Test Cardsโ€‹

Success Scenariosโ€‹

Card NumberBrand3DSResult
4242 4242 4242 4242VisaNoSuccess
4242 4242 4242 4242VisaYesSuccess (with 3DS)
5555 5555 5555 4444MastercardNoSuccess
5555 5555 5555 4444MastercardYesSuccess (with 3DS)
3566 1111 1111 1111JCBNoSuccess
3782 822463 10005AmexNoSuccess

Decline Scenariosโ€‹

Card NumberError CodeMessage
4000 0000 0000 0002payment_rejectedCharge was rejected
4000 0000 0000 0010insufficient_fundInsufficient funds
4000 0000 0000 0028stolen_or_lost_cardCard reported stolen/lost
4000 0000 0000 9995failed_processingProcessing error

Fraud Detectionโ€‹

Card NumberFraud ResultBehavior
4000 0000 0000 0101FlaggedMarked for manual review
4000 0000 0000 0044BlockedTransaction blocked by fraud rules

3D Secure Testingโ€‹

note

3DS testing requires a 3DS-enabled test account. Contact support@omise.co to enable.

Card Number3DS BehaviorResult
4242 4242 4242 4242Frictionless (no challenge)Success
4111 1111 1115 0002Enrollment failureDeclined
4111 1111 1114 0003Authentication failureDeclined

Test 3DS Authentication:

  • Any expiration date in the future works
  • Use any 3-4 digit CVV
  • Use any cardholder name

Card Details for Testingโ€‹

// Success card
{
name: "John Doe",
number: "4242424242424242",
expiration_month: 12,
expiration_year: 2027,
security_code: "123"
}

// Decline card
{
name: "Jane Smith",
number: "4000000000000002",
expiration_month: 12,
expiration_year: 2027,
security_code: "123"
}
Expiration Dates

Any future expiration date works for test cards. We recommend using dates at least 1 year in the future.

Testing Payment Methodsโ€‹

Credit/Debit Cardsโ€‹

// Using Omise.js
Omise.setPublicKey("pkey_test_YOUR_KEY");

Omise.createToken("card", {
name: "Test Card",
number: "4242424242424242",
expiration_month: 12,
expiration_year: 2027,
security_code: "123"
}, function(statusCode, response) {
if (statusCode === 200) {
console.log('Token created:', response.id);
// Use token to create charge
}
});

QR Payments (PromptPay, PayNow, etc.)โ€‹

// Create test PromptPay source
const source = await omise.sources.create({
type: 'promptpay',
amount: 35000,
currency: 'THB'
});

// QR code is generated
console.log('QR Code URL:', source.scannable_code.image.download_uri);

// Simulate payment in dashboard
// 1. Go to dashboard โ†’ Charges
// 2. Find the pending charge
// 3. Click "Mark as Successful" or "Mark as Failed"

E-Wallets (TrueMoney, GrabPay, etc.)โ€‹

// Create test wallet source
const source = await omise.sources.create({
type: 'truemoney',
amount: 50000,
currency: 'THB',
phone_number: '+66876543210' // Any Thai number works in test
});

// Simulate in dashboard
// Dashboard โ†’ Charges โ†’ Mark as Successful

Mobile Bankingโ€‹

// Create test mobile banking source
const source = await omise.sources.create({
type: 'mobile_banking_kbank',
amount: 100000,
currency: 'THB'
});

// Simulate in dashboard
// Dashboard โ†’ Charges โ†’ Actions โ†’ Mark as Successful

Simulating Payment Outcomesโ€‹

Via Dashboardโ€‹

  1. Log in to Test Dashboard
  2. Ensure Test Mode is enabled
  3. Navigate to Charges
  4. Find your pending charge
  5. Click "Actions" dropdown
  6. Select:
    • "Mark as Successful" - Simulate successful payment
    • "Mark as Failed" - Simulate payment failure

Via API (Test Webhooks)โ€‹

// Charges automatically trigger webhooks
// Check your webhook endpoint logs

app.post('/webhooks/omise', (req, res) => {
const event = req.body;

console.log('Test webhook received:', event.key);
console.log('Charge ID:', event.data.id);
console.log('Status:', event.data.status);

res.sendStatus(200);
});

Testing Webhooksโ€‹

Local Testing with ngrokโ€‹

# Install ngrok
npm install -g ngrok

# Start your local server
node server.js # Runs on port 3000

# Create tunnel
ngrok http 3000

# Output:
# Forwarding https://abc123.ngrok.io -> http://localhost:3000

Configure webhook in dashboard:

  1. Copy ngrok HTTPS URL
  2. Go to Settings โ†’ Webhooks
  3. Add endpoint: https://abc123.ngrok.io/webhooks/omise
  4. Select test events
  5. Save

Test Webhook Deliveryโ€‹

// Webhook handler
app.post('/webhooks/omise', (req, res) => {
const event = req.body;

// Log for debugging
console.log('=== Webhook Received ===');
console.log('Event:', event.key);
console.log('Object:', event.object);
console.log('Data:', JSON.stringify(event.data, null, 2));

// Verify signature (important!)
if (!verifyWebhookSignature(req)) {
console.error('Invalid signature!');
return res.sendStatus(401);
}

// Handle event
switch (event.key) {
case 'charge.complete':
handleChargeComplete(event.data);
break;
case 'charge.failed':
handleChargeFailed(event.data);
break;
}

res.sendStatus(200);
});

Manual Webhook Testingโ€‹

# Simulate webhook with cURL
curl -X POST http://localhost:3000/webhooks/omise \
-H "Content-Type: application/json" \
-d '{
"object": "event",
"id": "evnt_test_123",
"key": "charge.complete",
"data": {
"object": "charge",
"id": "chrg_test_123",
"status": "successful",
"amount": 100000
}
}'

Testing Refundsโ€‹

Full Refundโ€‹

// Create charge in test mode
const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: testTokenId
});

// Wait for successful status
// Then refund
const refund = await omise.charges.refund(charge.id, {
amount: 100000
});

console.log('Refund status:', refund.status);

Partial Refundโ€‹

// Partial refund (50%)
const partialRefund = await omise.charges.refund(charge.id, {
amount: 50000
});

// Multiple partial refunds (up to 15 per charge)
const refund2 = await omise.charges.refund(charge.id, {
amount: 25000
});

Void vs Refundโ€‹

// Void (within 24 hours) - instant cancellation
const voidRefund = await omise.charges.refund(charge.id, {
amount: 100000
});

if (voidRefund.voided) {
console.log('Charge was voided (no settlement)');
} else {
console.log('Charge was refunded (settled then refunded)');
}

Testing 3D Secureโ€‹

Enable 3DS in Testโ€‹

// Include return_uri to trigger 3DS
const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: tokenId,
return_uri: 'http://localhost:3000/payment/callback'
});

if (charge.authorize_uri) {
console.log('3DS required, redirect to:', charge.authorize_uri);
// In browser: window.location = charge.authorize_uri
}

Test 3DS Flowsโ€‹

// Frictionless flow (no challenge)
const token = await Omise.createToken("card", {
number: "4242424242424242", // Passes without challenge
// ... other details
});

// Challenge flow (authentication required)
const token2 = await Omise.createToken("card", {
number: "4000000000003220", // Requires authentication
// ... other details
});

// Failed authentication
const token3 = await Omise.createToken("card", {
number: "4000000000003238", // Fails 3DS
// ... other details
});

Testing Error Handlingโ€‹

Network Errorsโ€‹

async function createChargeWithRetry(chargeData, maxRetries = 3) {
let attempt = 0;

while (attempt < maxRetries) {
try {
const charge = await omise.charges.create(chargeData);
return charge;
} catch (error) {
attempt++;

if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
console.log(`Network error, retry ${attempt}/${maxRetries}`);
await sleep(1000 * attempt); // Exponential backoff
} else {
throw error; // Don't retry other errors
}
}
}

throw new Error('Max retries exceeded');
}

API Errorsโ€‹

try {
const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: invalidToken
});
} catch (error) {
// Handle specific errors
switch (error.code) {
case 'authentication_failure':
console.error('Invalid API key');
break;
case 'invalid_card':
console.error('Card declined');
showCardError(error.message);
break;
case 'insufficient_fund':
console.error('Insufficient funds');
offerAlternativeMethod();
break;
default:
console.error('Payment error:', error.message);
}
}

Validation Errorsโ€‹

// Test missing required fields
try {
const charge = await omise.charges.create({
amount: 100000
// Missing currency and payment source
});
} catch (error) {
console.log('Validation error:', error.message);
// Error: currency is required
}

// Test invalid amount
try {
const charge = await omise.charges.create({
amount: -100, // Negative amount
currency: 'THB',
card: tokenId
});
} catch (error) {
console.log('Invalid amount:', error.message);
}

Testing Customers APIโ€‹

Create Test Customerโ€‹

// Create customer with test card
const customer = await omise.customers.create({
email: 'test@example.com',
description: 'Test Customer',
card: testTokenId
});

console.log('Customer ID:', customer.id);
console.log('Default card:', customer.default_card);

Update Customer Cardโ€‹

// Update with new test card
const updatedCustomer = await omise.customers.update(customer.id, {
card: newTestTokenId
});

console.log('New default card:', updatedCustomer.default_card);

Charge Saved Cardโ€‹

// Charge customer without token
const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
customer: customer.id
});

console.log('Charged saved card:', charge.card.last_digits);

Testing Checklistโ€‹

Before going live, ensure you've tested:

Basic Integrationโ€‹

  • Create successful charge with test card (4242...)
  • Create declined charge (4000 0000 0000 0002)
  • Handle declined card gracefully
  • Display clear error messages to user
  • Verify charge status via API
  • Test webhook delivery and handling

Payment Methodsโ€‹

  • Test all enabled payment methods
  • QR code display (if applicable)
  • Mobile banking redirect flow
  • E-wallet integration
  • Return URL handling for redirect methods

3D Secure (if enabled)โ€‹

  • Test frictionless flow (no challenge)
  • Test challenge flow (authentication required)
  • Test failed authentication
  • Verify return_uri handling
  • Check webhook triggers correctly

Refundsโ€‹

  • Create full refund
  • Create partial refund
  • Test void (within 24 hours)
  • Verify refund webhooks
  • Check refund status in dashboard

Error Handlingโ€‹

  • Invalid API key
  • Expired token
  • Insufficient funds
  • Network timeout
  • Invalid charge amount
  • Missing required fields

Securityโ€‹

  • Verify webhook signatures
  • Test with HTTPS only
  • Validate user input
  • Check PCI compliance (no card data on server)
  • Test rate limiting

Customer Experienceโ€‹

  • Mobile responsive checkout
  • Clear payment instructions
  • Loading states during processing
  • Success confirmation page
  • Failure handling with retry option
  • Email confirmations (test emails)

Test Data Best Practicesโ€‹

1. Use Realistic Test Dataโ€‹

// Good - realistic test data
{
email: 'test.customer@example.com',
name: 'Test Customer',
description: 'Test order #12345'
}

// Bad - obvious test data that might get flagged
{
email: 'test@test.com',
name: 'Test',
description: 'test'
}

2. Clean Up Test Data Regularlyโ€‹

// Delete old test charges (via dashboard)
// Or keep test and live data separate

3. Tag Test Transactionsโ€‹

const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: tokenId,
metadata: {
environment: 'test',
test_scenario: 'successful_payment',
tester: 'john@company.com'
}
});

4. Document Test Scenariosโ€‹

// Test scenario documentation
const TEST_SCENARIOS = {
successful_payment: {
card: '4242424242424242',
expected: 'successful',
description: 'Standard successful payment'
},
declined_payment: {
card: '4000000000000002',
expected: 'failed',
description: 'Card declined by bank'
},
insufficient_funds: {
card: '4000000000000010',
expected: 'failed',
description: 'Insufficient funds'
}
};

Troubleshootingโ€‹

Common Testing Issuesโ€‹

Issue: Test Charges Not Appearing in Dashboardโ€‹

Symptoms:

  • Created charges via API but don't see them in dashboard
  • Dashboard shows empty test data

Causes:

  • Using live keys instead of test keys
  • Viewing wrong mode in dashboard
  • API errors not caught

Solutions:

// 1. Verify you're using test keys
const omise = require('omise')({
secretKey: 'skey_test_...' // Must start with skey_test_
});

// 2. Check for API errors
try {
const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: tokenId
});
console.log('Charge created:', charge.id);
} catch (error) {
console.error('API Error:', error.message);
console.error('Error code:', error.code);
}

// 3. Ensure dashboard is in test mode
// Dashboard โ†’ Toggle "Test Mode" at top

Issue: Test Cards Rejectedโ€‹

Symptoms:

  • Test card numbers not working
  • Getting "invalid card" errors
  • Charges failing in test mode

Causes:

  • Using real card numbers in test mode
  • Incorrect test card number format
  • Invalid expiration date or CVV

Solutions:

// โœ“ CORRECT: Use test card numbers
const testToken = await Omise.createToken("card", {
number: "4242424242424242", // Test card
expiration_month: 12,
expiration_year: 2027, // Future date
security_code: "123"
});

// โœ— INCORRECT: Real card numbers don't work in test
const realCard = await Omise.createToken("card", {
number: "5555111122223333", // Real card = rejected
// ...
});

// Verify test mode
if (!Omise.getPublicKey().includes('test')) {
console.error('Not using test public key!');
}

Valid Test Cards:

const TEST_CARDS = {
visa_success: '4242424242424242',
mastercard_success: '5555555555554444',
declined: '4000000000000002',
insufficient_funds: '4000000000000010'
};

Issue: Webhooks Not Received in Test Modeโ€‹

Symptoms:

  • No webhook deliveries to endpoint
  • Missing webhook events
  • Timeout errors in dashboard

Causes:

  • Endpoint not publicly accessible
  • HTTPS certificate invalid
  • Webhook signature verification failing
  • Firewall blocking requests

Solutions:

// 1. Use ngrok for local testing
// Terminal 1: Start server
node server.js // Port 3000

// Terminal 2: Start ngrok
ngrok http 3000
// Use: https://abc123.ngrok.io/webhooks/omise

// 2. Check endpoint accessibility
curl https://your-domain.com/webhooks/omise
// Should return 405 Method Not Allowed (POST required)

// 3. Verify SSL certificate
// Use: https://www.ssllabs.com/ssltest/

// 4. Test webhook handler locally
curl -X POST http://localhost:3000/webhooks/omise \
-H "Content-Type: application/json" \
-d '{"object":"event","key":"charge.complete"}'

// 5. Check signature verification
app.post('/webhooks/omise', (req, res) => {
console.log('Webhook received');
console.log('Signature:', req.headers['omise-signature']);
console.log('Body:', req.body);

// Don't verify signature during initial testing
// if (!verifyWebhookSignature(req)) {
// return res.sendStatus(401);
// }

res.sendStatus(200);
});

Issue: API Key Authentication Failuresโ€‹

Symptoms:

  • "authentication_failure" errors
  • 401 Unauthorized responses
  • "Invalid API key" messages

Causes:

  • Wrong API key format
  • Using public key instead of secret key
  • Keys from wrong environment
  • API key not properly encoded

Solutions:

// โœ“ CORRECT: Use secret key for server-side
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY // skey_test_...
});

// โœ— INCORRECT: Public key on server
const omise = require('omise')({
secretKey: 'pkey_test_...' // Wrong! Public key
});

// Check key format
function validateApiKey(key) {
if (key.startsWith('skey_test_')) {
return 'test_secret';
} else if (key.startsWith('skey_')) {
return 'live_secret';
} else if (key.startsWith('pkey_test_')) {
return 'test_public';
} else if (key.startsWith('pkey_')) {
return 'live_public';
}
return 'invalid';
}

// Verify in .env file
console.log('Key type:', validateApiKey(process.env.OMISE_SECRET_KEY));

// Test authentication
try {
const balance = await omise.balance.retrieve();
console.log('Authentication successful');
console.log('Available:', balance.available / 100);
} catch (error) {
console.error('Auth failed:', error.message);
}

Issue: Test Card Payment Failuresโ€‹

Symptoms:

  • All test cards failing
  • Unexpected error codes
  • Inconsistent test results

Causes:

  • Incorrect charge parameters
  • Invalid amount format
  • Missing required fields
  • Token expired or already used

Solutions:

// Common mistakes and fixes

// โœ— INCORRECT: Amount as decimal
const charge = await omise.charges.create({
amount: 100.00, // Wrong!
currency: 'THB'
});

// โœ“ CORRECT: Amount in smallest unit (satang)
const charge = await omise.charges.create({
amount: 10000, // เธฟ100.00
currency: 'THB',
card: tokenId
});

// โœ— INCORRECT: Reusing token
const token = 'tokn_test_123';
await omise.charges.create({ amount: 10000, currency: 'THB', card: token });
await omise.charges.create({ amount: 10000, currency: 'THB', card: token }); // Fails!

// โœ“ CORRECT: Create new token for each charge
const token1 = await createNewToken();
await omise.charges.create({ amount: 10000, currency: 'THB', card: token1 });

const token2 = await createNewToken();
await omise.charges.create({ amount: 10000, currency: 'THB', card: token2 });

// โœ— INCORRECT: Missing required fields
await omise.charges.create({
amount: 10000
// Missing: currency, payment source
});

// โœ“ CORRECT: All required fields
await omise.charges.create({
amount: 10000,
currency: 'THB',
card: tokenId // or customer, or source
});

Issue: Webhook Testing Problemsโ€‹

Symptoms:

  • Webhook signature verification always fails
  • Duplicate webhook events
  • Missing webhook events

Causes:

  • Body parser modifying request body
  • Not storing processed event IDs
  • Network issues causing retries

Solutions:

// Problem 1: Signature verification fails
// โœ— INCORRECT: Body already parsed
app.use(express.json()); // Parses body
app.post('/webhooks', (req, res) => {
verifySignature(req.body); // Body modified!
});

// โœ“ CORRECT: Preserve raw body
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));

app.post('/webhooks', (req, res) => {
verifySignature(req.rawBody); // Use raw body
});

// Problem 2: Duplicate events
// โœ“ CORRECT: Track processed events
const processedEvents = new Set();

app.post('/webhooks', (req, res) => {
const event = req.body;

// Check if already processed
if (processedEvents.has(event.id)) {
console.log('Duplicate event:', event.id);
return res.sendStatus(200); // Still return 200
}

// Process event
handleEvent(event);

// Mark as processed
processedEvents.add(event.id);

res.sendStatus(200);
});

// Problem 3: Debugging webhooks
app.post('/webhooks', (req, res) => {
console.log('=== WEBHOOK DEBUG ===');
console.log('Headers:', req.headers);
console.log('Body:', JSON.stringify(req.body, null, 2));
console.log('Raw Body:', req.rawBody);
console.log('Event ID:', req.body.id);
console.log('Event Key:', req.body.key);
console.log('====================');

res.sendStatus(200);
});

Quick Troubleshooting Checklistโ€‹

When things go wrong, check:

API Keys:

  • Using test keys (start with test_)
  • Using secret key on server (not public key)
  • Keys loaded from environment variables
  • Dashboard in correct mode (test/live)

Test Cards:

  • Using valid test card numbers
  • Future expiration date
  • Valid CVV (any 3-4 digits)
  • Not reusing tokens

Webhooks:

  • Endpoint is HTTPS with valid certificate
  • Endpoint is publicly accessible
  • Returning 200 OK status
  • Signature verification correct
  • Handling duplicate events

Common Errors:

  • Amount in correct format (smallest unit)
  • All required fields provided
  • Valid currency code
  • Token not expired

Getting Helpโ€‹

If issues persist:

  1. Check API Logs:
// Enable debug logging
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY,
debug: true
});
  1. Review Dashboard Logs:

    • Go to Dashboard โ†’ API Logs
    • Check request/response details
    • Look for error messages
  2. Test with cURL:

curl https://api.omise.co/charges \
-u skey_test_YOUR_KEY: \
-d "amount=100000" \
-d "currency=THB" \
-d "card=tokn_test_..."
  1. Contact Support:
    • Email: support@omise.co
    • Include: Test charge IDs, error messages, code samples
    • Specify: Test mode, SDK version, programming language

FAQโ€‹

Can I use real cards in test mode?

No, test mode only accepts test card numbers. Real cards will be rejected. This prevents accidental real charges during development.

Do test mode webhooks actually deliver?

Yes! Test webhooks are delivered to your endpoints just like live webhooks. Use ngrok for local testing or deploy to a staging server.

How do I test payment methods without test cards?

For QR codes, e-wallets, and mobile banking in test mode:

  1. Create the source/charge
  2. Go to your test dashboard
  3. Find the pending charge
  4. Click "Mark as Successful" or "Mark as Failed"
Are test API keys safe to commit to git?

While test keys don't process real money, they should still be kept in environment variables and not committed to public repositories. They can access test data and be used to create test charges.

Can I switch between test and live mode easily?

Yes, use environment variables:

const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY // Switch via env
});
How long do test charges remain in the dashboard?

Test data remains indefinitely but can be manually deleted. Consider cleaning up old test data periodically to keep your test dashboard organized.

Do test transactions affect my live analytics?

No, test and live modes are completely separate. Test transactions don't appear in live analytics or settlement reports.

Next Stepsโ€‹

  1. Get test API keys
  2. Test with success card
  3. Test declined scenarios
  4. Set up webhook testing
  5. Complete testing checklist
  6. Go live