Skip to main content

Payment Links API

Automate payment link creation and management using the Omise Payment Links API. Perfect for businesses that need to generate many links, integrate with existing systems, or create dynamic payment experiences.

Overviewโ€‹

The Payment Links API allows you to:

  • Create payment links programmatically
  • Retrieve link details and status
  • Update link configuration
  • List all payment links
  • Track link performance
  • Handle webhooks for payment events

Authenticationโ€‹

All API requests require authentication using your secret key:

curl https://api.omise.co/links \
-u skey_test_xxx:

Important:

  • Use skey_test_xxx for test mode
  • Use skey_live_xxx for live mode
  • Never expose secret keys in client-side code
  • Keep secret keys secure

API Endpointsโ€‹

Base URLโ€‹

https://api.omise.co

Available Endpointsโ€‹

MethodEndpointDescription
POST/linksCreate a payment link
GET/links/:idRetrieve a payment link
PATCH/links/:idUpdate a payment link
GET/linksList all payment links

Basic Creationโ€‹

Create a simple payment link:

const omise = require('omise')({
secretKey: 'skey_test_xxx'
});

omise.links.create({
amount: 100000, // Amount in smallest currency unit (1,000.00 THB)
currency: 'thb',
title: 'Product Purchase',
description: 'Premium widget purchase',
multiple: true // Multi-use link
}, function(error, link) {
if (error) {
console.error('Error:', error);
} else {
console.log('Link created:', link.payment_uri);
}
});

Response:

{
"object": "link",
"id": "payl_test_5xyx8z94w46ixr9l",
"livemode": false,
"location": "/links/payl_test_5xyx8z94w46ixr9l",
"amount": 100000,
"currency": "thb",
"title": "Product Purchase",
"description": "Premium widget purchase",
"multiple": true,
"payment_uri": "https://pay.omise.co/links/payl_test_5xyx8z94w46ixr9l",
"used": false,
"charges": {
"object": "list",
"data": [],
"total": 0
},
"created_at": "2026-02-06T10:30:00Z"
}

Using cURLโ€‹

curl https://api.omise.co/links \
-u skey_test_xxx: \
-d "amount=100000" \
-d "currency=thb" \
-d "title=Product Purchase" \
-d "description=Premium widget purchase" \
-d "multiple=true"

Using Pythonโ€‹

import omise

omise.api_secret = 'skey_test_xxx'

link = omise.Link.create(
amount=100000,
currency='thb',
title='Product Purchase',
description='Premium widget purchase',
multiple=True
)

print(f"Link URL: {link.payment_uri}")

Using PHPโ€‹

<?php
require_once 'vendor/autoload.php';

define('OMISE_SECRET_KEY', 'skey_test_xxx');

$link = OmiseLink::create([
'amount' => 100000,
'currency' => 'thb',
'title' => 'Product Purchase',
'description' => 'Premium widget purchase',
'multiple' => true
]);

echo "Link URL: " . $link['payment_uri'];
?>

Using Rubyโ€‹

require 'omise'

Omise.api_key = 'skey_test_xxx'

link = Omise::Link.create({
amount: 100000,
currency: 'thb',
title: 'Product Purchase',
description: 'Premium widget purchase',
multiple: true
})

puts "Link URL: #{link.payment_uri}"

Request Parametersโ€‹

Required Parametersโ€‹

ParameterTypeDescription
amountintegerAmount in smallest currency unit (e.g., satangs for THB)
currencystringThree-letter ISO currency code (e.g., thb, usd)
titlestringLink title shown to customers (max 200 chars)

Optional Parametersโ€‹

ParameterTypeDefaultDescription
descriptionstringnullDetailed description (max 1000 chars)
multiplebooleanfalseAllow multiple uses (true) or single-use (false)
metadataobjectCustom metadata (key-value pairs)

Advanced Creation Examplesโ€‹

Perfect for invoices:

omise.links.create({
amount: 250000, // 2,500.00 THB
currency: 'thb',
title: 'Invoice #INV-001',
description: 'Payment for consulting services',
multiple: false, // Single-use only
metadata: {
invoice_id: 'INV-001',
customer_id: 'CUST-123',
customer_email: 'customer@example.com'
}
}, function(error, link) {
if (!error) {
console.log('Invoice link:', link.payment_uri);
console.log('Link ID:', link.id);
}
});

Store custom information:

omise.links.create({
amount: 150000,
currency: 'thb',
title: 'Consultation Booking',
description: '1-hour consultation session',
multiple: true,
metadata: {
service_type: 'consultation',
duration: '60_minutes',
category: 'business',
booking_date: '2026-03-15',
time_slot: '14:00',
consultant_id: 'CONS-456'
}
}, function(error, link) {
if (!error) {
console.log('Booking link created');
// Store link.id in your database
}
});

Create links in different currencies:

// USD link for international customers
omise.links.create({
amount: 5000, // $50.00
currency: 'usd',
title: 'International Product',
description: 'Available for global shipping',
multiple: true
}, function(error, usdLink) {
console.log('USD link:', usdLink.payment_uri);
});

// THB link for domestic customers
omise.links.create({
amount: 150000, // 1,500.00 THB
currency: 'thb',
title: 'Thailand Product',
description: 'เธชเธณเธซเธฃเธฑเธšเธฅเธนเธเธ„เน‰เธฒเนƒเธ™เธ›เธฃเธฐเน€เธ—เธจเน„เธ—เธข',
multiple: true
}, function(error, thbLink) {
console.log('THB link:', thbLink.payment_uri);
});

Retrieve a link by ID:

omise.links.retrieve('payl_test_5xyx8z94w46ixr9l', function(error, link) {
if (!error) {
console.log('Link status:', link.used ? 'Used' : 'Active');
console.log('Total charges:', link.charges.total);
console.log('Created:', link.created_at);
}
});

Using cURL:

curl https://api.omise.co/links/payl_test_5xyx8z94w46ixr9l \
-u skey_test_xxx:

Response:

{
"object": "link",
"id": "payl_test_5xyx8z94w46ixr9l",
"livemode": false,
"amount": 100000,
"currency": "thb",
"title": "Product Purchase",
"description": "Premium widget purchase",
"multiple": true,
"payment_uri": "https://pay.omise.co/links/payl_test_5xyx8z94w46ixr9l",
"used": false,
"charges": {
"object": "list",
"data": [
{
"object": "charge",
"id": "chrg_test_xxx",
"amount": 100000,
"status": "successful",
"created_at": "2026-02-06T11:00:00Z"
}
],
"total": 1
},
"created_at": "2026-02-06T10:30:00Z"
}

Get paginated list of all links:

omise.links.list({
limit: 20,
offset: 0,
order: 'reverse_chronological'
}, function(error, list) {
if (!error) {
console.log('Total links:', list.total);
list.data.forEach(function(link) {
console.log(`${link.title}: ${link.payment_uri}`);
});
}
});

Using cURL:

curl https://api.omise.co/links?limit=20&offset=0 \
-u skey_test_xxx:

List Parameters:

ParameterTypeDefaultDescription
limitinteger20Number of links per page (max 100)
offsetinteger0Number of links to skip
orderstringchronologicalSort order: chronological or reverse_chronological
fromstringnullFilter links created from date (ISO 8601)
tostringnullFilter links created until date (ISO 8601)

Update link details (limited fields):

omise.links.update('payl_test_5xyx8z94w46ixr9l', {
description: 'Updated product description with more details',
metadata: {
updated_at: '2026-02-06',
reason: 'price_change',
updated_by: 'admin'
}
}, function(error, link) {
if (!error) {
console.log('Link updated successfully');
}
});

Using cURL:

curl https://api.omise.co/links/payl_test_5xyx8z94w46ixr9l \
-X PATCH \
-u skey_test_xxx: \
-d "description=Updated product description"

Updatable Fields:

  • description - Link description
  • metadata - Custom metadata

Non-Updatable Fields:

  • amount - Cannot change amount
  • currency - Cannot change currency
  • title - Cannot change title
  • multiple - Cannot change type

Note: For significant changes, create a new link and disable the old one.

Practical Integration Examplesโ€‹

Generate links for each product:

const products = [
{ id: 'PROD-001', name: 'T-Shirt Red', price: 45000 },
{ id: 'PROD-002', name: 'T-Shirt Blue', price: 45000 },
{ id: 'PROD-003', name: 'T-Shirt Green', price: 45000 }
];

products.forEach(product => {
omise.links.create({
amount: product.price,
currency: 'thb',
title: product.name,
description: `Purchase ${product.name}`,
multiple: true,
metadata: {
product_id: product.id,
product_name: product.name,
category: 'apparel'
}
}, function(error, link) {
if (!error) {
// Store link.payment_uri in database
console.log(`${product.name}: ${link.payment_uri}`);

// Save to database
saveToDatabase({
product_id: product.id,
payment_link: link.payment_uri,
link_id: link.id
});
}
});
});

Example 2: Dynamic Invoice Generationโ€‹

Create invoice links on-demand:

function createInvoiceLink(invoice) {
return new Promise((resolve, reject) => {
omise.links.create({
amount: invoice.total,
currency: invoice.currency,
title: `Invoice ${invoice.number}`,
description: `Payment for ${invoice.description}`,
multiple: false, // Single-use
metadata: {
invoice_number: invoice.number,
customer_id: invoice.customer_id,
customer_email: invoice.customer_email,
due_date: invoice.due_date,
items: JSON.stringify(invoice.items)
}
}, function(error, link) {
if (error) {
reject(error);
} else {
resolve(link);
}
});
});
}

// Usage
const invoice = {
number: 'INV-2026-001',
total: 350000, // 3,500.00 THB
currency: 'thb',
description: 'Web development services',
customer_id: 'CUST-123',
customer_email: 'customer@example.com',
due_date: '2026-03-06',
items: [
{ description: 'Homepage design', amount: 150000 },
{ description: 'Contact page', amount: 100000 },
{ description: 'Mobile responsive', amount: 100000 }
]
};

createInvoiceLink(invoice)
.then(link => {
console.log('Invoice link:', link.payment_uri);

// Send email to customer
sendInvoiceEmail(invoice.customer_email, {
invoice_number: invoice.number,
payment_link: link.payment_uri,
amount: invoice.total,
due_date: invoice.due_date
});
})
.catch(error => {
console.error('Error creating invoice link:', error);
});

Example 3: Event Registration Systemโ€‹

Generate tickets with links:

const events = {
'workshop-2026': {
name: 'Web Development Workshop 2026',
tickets: [
{ type: 'early_bird', price: 150000, available: 50 },
{ type: 'regular', price: 200000, available: 100 },
{ type: 'vip', price: 350000, available: 20 }
]
}
};

async function generateEventLinks(eventId, event) {
const links = [];

for (const ticket of event.tickets) {
const link = await new Promise((resolve, reject) => {
omise.links.create({
amount: ticket.price,
currency: 'thb',
title: `${event.name} - ${ticket.type.replace('_', ' ').toUpperCase()} Ticket`,
description: `Register for ${event.name}`,
multiple: true,
metadata: {
event_id: eventId,
ticket_type: ticket.type,
tickets_available: ticket.available
}
}, (error, link) => {
if (error) reject(error);
else resolve(link);
});
});

links.push({
ticket_type: ticket.type,
link: link.payment_uri,
link_id: link.id
});
}

return links;
}

// Generate links for event
generateEventLinks('workshop-2026', events['workshop-2026'])
.then(links => {
console.log('Event links generated:');
links.forEach(l => {
console.log(`${l.ticket_type}: ${l.link}`);
});
});

Create monthly payment links:

function generateMonthlyLink(subscription) {
const currentDate = new Date();
const month = currentDate.toLocaleString('en-US', { month: 'long' });
const year = currentDate.getFullYear();

return omise.links.create({
amount: subscription.monthly_amount,
currency: 'thb',
title: `${subscription.plan_name} - ${month} ${year}`,
description: `Monthly payment for ${subscription.plan_name} subscription`,
multiple: false, // Each month gets unique link
metadata: {
subscription_id: subscription.id,
customer_id: subscription.customer_id,
plan: subscription.plan_name,
billing_month: `${year}-${currentDate.getMonth() + 1}`,
auto_generated: 'true'
}
});
}

// Example: Generate for all active subscriptions
const activeSubscriptions = [
{
id: 'SUB-001',
customer_id: 'CUST-123',
plan_name: 'Premium',
monthly_amount: 99000
},
{
id: 'SUB-002',
customer_id: 'CUST-456',
plan_name: 'Business',
monthly_amount: 299000
}
];

activeSubscriptions.forEach(subscription => {
generateMonthlyLink(subscription)
.then(link => {
console.log(`Link for ${subscription.customer_id}:`, link.payment_uri);

// Email customer
emailCustomer(subscription.customer_id, {
subject: 'Monthly Subscription Payment',
payment_link: link.payment_uri
});
})
.catch(error => {
console.error(`Error for ${subscription.id}:`, error);
});
});

Generate many links efficiently:

async function bulkCreateLinks(items) {
const results = {
successful: [],
failed: []
};

// Process in batches to avoid rate limits
const batchSize = 10;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);

const promises = batch.map(item =>
new Promise((resolve) => {
omise.links.create({
amount: item.amount,
currency: 'thb',
title: item.title,
description: item.description,
multiple: true,
metadata: item.metadata || {}
}, (error, link) => {
if (error) {
resolve({ success: false, item, error });
} else {
resolve({ success: true, item, link });
}
});
})
);

const batchResults = await Promise.all(promises);

batchResults.forEach(result => {
if (result.success) {
results.successful.push(result);
} else {
results.failed.push(result);
}
});

// Rate limiting delay
if (i + batchSize < items.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}

return results;
}

// Usage
const products = [
{ title: 'Product A', amount: 50000, description: 'Description A' },
{ title: 'Product B', amount: 75000, description: 'Description B' },
{ title: 'Product C', amount: 100000, description: 'Description C' },
// ... more products
];

bulkCreateLinks(products)
.then(results => {
console.log(`Successfully created: ${results.successful.length}`);
console.log(`Failed: ${results.failed.length}`);

// Export successful links
const csv = results.successful.map(r =>
`${r.item.title},${r.link.payment_uri}`
).join('\n');

console.log('\nCSV Export:\n', csv);
});

Webhooks Integrationโ€‹

Receive real-time notifications about payment link events:

Setting Up Webhooksโ€‹

  1. Configure webhook endpoint in dashboard
  2. Listen for payment link events
  3. Verify webhook signatures
  4. Process events

Webhook Eventsโ€‹

const express = require('express');
const app = express();

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

switch(event.key) {
case 'charge.create':
handleChargeCreated(event.data);
break;
case 'charge.complete':
handleChargeCompleted(event.data);
break;
case 'charge.failed':
handleChargeFailed(event.data);
break;
}

res.sendStatus(200);
});

function handleChargeCompleted(charge) {
// Check if charge is from a payment link
if (charge.source && charge.source.type === 'link') {
const linkId = charge.source.id;

console.log('Payment received for link:', linkId);

// Retrieve link details
omise.links.retrieve(linkId, (error, link) => {
if (!error) {
const metadata = link.metadata;

// Process based on metadata
if (metadata.invoice_id) {
markInvoicePaid(metadata.invoice_id);
} else if (metadata.product_id) {
fulfillOrder(metadata.product_id, charge);
}

// Send confirmation email
sendConfirmationEmail(charge.customer_email, {
amount: charge.amount,
description: link.title
});
}
});
}
}

app.listen(3000);

Webhook Signature Verificationโ€‹

Verify webhook authenticity:

const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
const computed = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

return computed === signature;
}

app.post('/webhooks/omise', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['omise-signature'];
const secret = process.env.WEBHOOK_SECRET;

if (!verifyWebhook(req.body, signature, secret)) {
return res.status(401).send('Invalid signature');
}

const event = JSON.parse(req.body);
// Process event...

res.sendStatus(200);
});

Error Handlingโ€‹

Common Errorsโ€‹

omise.links.create({
amount: 100000,
currency: 'thb',
title: 'Product'
}, function(error, link) {
if (error) {
switch(error.code) {
case 'invalid_amount':
console.error('Invalid amount:', error.message);
break;
case 'invalid_currency':
console.error('Currency not supported:', error.message);
break;
case 'authentication_failure':
console.error('Invalid API key:', error.message);
break;
default:
console.error('Error:', error.message);
}
}
});

Error Response Formatโ€‹

{
"object": "error",
"location": "https://www.omise.co/api-errors#authentication-failure",
"code": "authentication_failure",
"message": "authentication failed"
}

Common Error Codesโ€‹

CodeDescriptionSolution
authentication_failureInvalid API keyCheck your secret key
invalid_amountAmount is invalidUse positive integer in smallest unit
invalid_currencyUnsupported currencyUse supported currency code
invalid_cardCard validation failedCheck card details
insufficient_fundInsufficient balanceCustomer needs to add funds
failed_processingPayment processing failedRetry or contact support

Retry Logicโ€‹

Implement exponential backoff:

async function createLinkWithRetry(data, maxRetries = 3) {
let lastError;

for (let i = 0; i < maxRetries; i++) {
try {
const link = await new Promise((resolve, reject) => {
omise.links.create(data, (error, link) => {
if (error) reject(error);
else resolve(link);
});
});

return link;
} catch (error) {
lastError = error;

// Don't retry certain errors
if (error.code === 'authentication_failure' ||
error.code === 'invalid_amount') {
throw error;
}

// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}

throw lastError;
}

Rate Limitsโ€‹

Limitsโ€‹

  • 60 requests per minute per API key
  • 1000 requests per hour per API key

Handling Rate Limitsโ€‹

const rateLimit = {
requests: 0,
resetAt: Date.now() + 60000
};

function checkRateLimit() {
if (Date.now() > rateLimit.resetAt) {
rateLimit.requests = 0;
rateLimit.resetAt = Date.now() + 60000;
}

if (rateLimit.requests >= 60) {
const waitTime = rateLimit.resetAt - Date.now();
throw new Error(`Rate limit exceeded. Try again in ${waitTime}ms`);
}

rateLimit.requests++;
}

async function createLinkSafe(data) {
checkRateLimit();

return new Promise((resolve, reject) => {
omise.links.create(data, (error, link) => {
if (error) reject(error);
else resolve(link);
});
});
}

Best Practicesโ€‹

1. Use Metadata Effectivelyโ€‹

Store all relevant information:

omise.links.create({
amount: 100000,
currency: 'thb',
title: 'Product Purchase',
metadata: {
// Business context
order_id: 'ORD-123',
customer_id: 'CUST-456',

// Product details
product_id: 'PROD-789',
product_sku: 'WIDGET-001',

// Fulfillment info
shipping_required: 'true',
warehouse: 'BKK-01',

// Tracking
campaign: 'summer-sale',
referrer: 'social-media',

// Contact
customer_email: 'customer@example.com',
customer_phone: '+66812345678'
}
});

2. Implement Idempotencyโ€‹

Prevent duplicate links:

const linkCache = new Map();

async function createLinkIdempotent(key, data) {
// Check if link already exists
if (linkCache.has(key)) {
return linkCache.get(key);
}

// Create new link
const link = await new Promise((resolve, reject) => {
omise.links.create(data, (error, link) => {
if (error) reject(error);
else resolve(link);
});
});

// Cache the link
linkCache.set(key, link);

return link;
}

// Usage
const invoiceId = 'INV-001';
const link = await createLinkIdempotent(invoiceId, {
amount: 100000,
currency: 'thb',
title: `Invoice ${invoiceId}`,
metadata: { invoice_id: invoiceId }
});

Always store link IDs in your database:

async function createAndStoreLinkInfo(orderData) {
// Create link
const link = await createLink(orderData);

// Store in database
await database.orders.update(orderData.id, {
payment_link_id: link.id,
payment_link_url: link.payment_uri,
link_created_at: link.created_at
});

return link;
}

4. Handle Webhooks Properlyโ€‹

app.post('/webhooks', async (req, res) => {
// Respond quickly
res.sendStatus(200);

// Process asynchronously
processWebhook(req.body).catch(error => {
console.error('Webhook processing error:', error);
// Log for manual review
});
});

async function processWebhook(event) {
// Verify signature
// Prevent duplicate processing
// Update database
// Send notifications
}

Track link usage:

async function getLinkStats(linkId) {
const link = await omise.links.retrieve(linkId);

return {
total_charges: link.charges.total,
successful_payments: link.charges.data.filter(c =>
c.status === 'successful'
).length,
failed_payments: link.charges.data.filter(c =>
c.status === 'failed'
).length,
total_revenue: link.charges.data
.filter(c => c.status === 'successful')
.reduce((sum, c) => sum + c.amount, 0),
conversion_rate: calculateConversionRate(link)
};
}

Testingโ€‹

Test Modeโ€‹

Use test API keys:

const omise = require('omise')({
secretKey: 'skey_test_xxx' // Test key
});

Test Cardsโ€‹

// Test successful payment
const testCardSuccess = {
number: '4242424242424242',
name: 'Test User',
expiration_month: 1,
expiration_year: 2026,
security_code: '123'
};

// Test failed payment
const testCardFailed = {
number: '4000000000000002',
name: 'Test User',
expiration_month: 1,
expiration_year: 2026,
security_code: '123'
};

Test Webhooksโ€‹

Use tools like ngrok:

# Start ngrok
ngrok http 3000

# Use ngrok URL in dashboard webhook settings
https://abc123.ngrok.io/webhooks/omise

Frequently Asked Questionsโ€‹

Q: Can I create links without amount (customer decides)?
A: No, the API currently requires a fixed amount. For flexible amounts, use the dashboard interface.

Q: How do I delete a payment link?
A: Links cannot be deleted via API. They can only be marked as inactive by reaching their use limit or expiration.

Q: Can I customize the payment page design via API?
A: No, design customization is only available through the dashboard. The API focuses on link creation and management.

Q: Is there a limit to how many links I can create?
A: Standard accounts can have 100 active links. Contact support for higher limits. Inactive/used links don't count toward the limit.

Q: How do I track which channel a payment came from?
A: Use metadata to tag links with channel information, or create separate links for each channel.

Q: Can I create recurring payment links? A: Payment Links are for one-time payments. For recurring charges, use the Subscriptions API.

Q: How quickly are links available after creation?
A: Links are immediately available after creation. No propagation delay.

Q: Can I update the amount of an existing link?
A: No, amounts cannot be changed. Create a new link with the updated amount.

Q: Do I need to create separate links for different payment methods?
A: No, one link supports all payment methods enabled in your account. Customers choose their preferred method.

Q: How do I know when a payment is made through a link?
A: Use webhooks for real-time notifications, or poll the charges list using the API.

Q: Can I use links in mobile apps?
A: Yes, open the payment_uri in a webview or external browser. The page is mobile-optimized.

Q: What happens if I exceed rate limits?
A: You'll receive a 429 Too Many Requests error. Implement exponential backoff and retry.

Next Stepsโ€‹

Additional Resourcesโ€‹


Need help? Contact our support team at support@omise.co or visit the Help Center.