Skip to content

List Payment Links

Retrieve multiple payment links with pagination and filtering options.

GET /v1/payment-links

Requires API key authentication via X-API-Key header.

An account-level key lists every payment link of the account. A project API key lists only the links of its project, that is, the links created with the project’s key (POST /v1/payment-links and the collection endpoints). Links created from the dashboard or with another project’s key are not included, and total does not count them.

ParameterTypeRequiredDescription
limintegerNoItems per page, 1-100 (default: 10; a value above 100 returns 400)
offintegerNoOffset from start (non-negative integer, default: 0)
statusstringNoFilter by stored status (pending, processing, completed, cancelled, expired). See Expired links
isTestbooleanNoFilter by test mode (true or false)
linkTypestringNoFilter by link type (one_time, customer_collection, customer_payment)
searchstringNoOnly links whose customer name contains this text (1-100 characters)
paymentProviderstringNoAccepted but currently ignored for API-key requests: the list and total are not filtered by provider (an unknown value still returns 400). Allowed values: nmi, emetec, yappy, paypal

Results come newest first (createdAt descending). Unknown query parameters are ignored. The response has no pagination object: to walk every page, increase off by the page size until you have total items (see Paginated Retrieval).

processing means a payment for the link is being confirmed. With PayPal this can last from minutes to several days while PayPal reviews the payment; the link then becomes completed, or goes back to pending if the payment is declined or abandoned.

status=expired only returns links already stored as expired. A pending link past its expiresAt is stored as expired the first time a listing (API or dashboard) returns it on a page; that same response still shows it as pending, and later reads show expired. Because of that, paging with status=pending and off skips links: each page moves the expired ones out of the filtered set, so the next offset lands past links you never saw. To find every expired link, page through without a status filter and keep the links whose status is expired, or pending with an expiresAt in the past (as in Analytics and Reporting). No webhook is sent when a link expires.

Terminal window
curl -X GET "https://api-pay.zelta.dev/v1/payment-links" \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json"
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key-here',
'Content-Type': 'application/json'
}
});
const result = await response.json();
console.log(result);
import requests
headers = {
'X-API-Key': 'your-api-key-here',
'Content-Type': 'application/json'
}
response = requests.get('https://api-pay.zelta.dev/v1/payment-links', headers=headers)
result = response.json()
print(result)
Terminal window
curl -X GET "https://api-pay.zelta.dev/v1/payment-links?lim=20&off=20" \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json"
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links?lim=20&off=20', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key-here',
'Content-Type': 'application/json'
}
});
const result = await response.json();
console.log(result);
Terminal window
curl -X GET "https://api-pay.zelta.dev/v1/payment-links?status=completed&isTest=false" \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json"
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links?status=completed&isTest=false', {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key-here',
'Content-Type': 'application/json'
}
});
const result = await response.json();
console.log(result);
{
"success": true,
"data": {
"paymentLinks": [
{
"id": "7312345678901234567",
"paymentLinkUrl": "https://pay.zelta.dev/Xk3v9Q_aB7mN2pL8rT4wYz1c",
"customerName": "John Doe",
"concept": "Consulting Service",
"amount": 2500,
"status": "pending",
"createdAt": "2024-01-15T10:30:00.000Z",
"expiresAt": "2024-01-22T10:30:00.000Z",
"isTest": true,
"redirectUrl": "https://myapp.com/success",
"metadata": {
"orderId": "ORD-001",
"service": "consulting"
},
"paymentMethods": ["card", "yappy"],
"requestCustomerEmail": true
}
],
"total": 25
},
"message": "Operation completed successfully",
"timestamp": "2024-01-15T10:30:00.000Z"
}
FieldTypeDescription
idstringUnique payment link identifier (a numeric string)
paymentLinkUrlstringURL for the payment link. Its last path segment is the hashUrl used by Get and Cancel
customerNamestringName of the customer
conceptstringDescription of what’s being paid for
amountintegerAmount in cents
statusstringStored payment link status (pending, processing, completed, cancelled, expired). A link past expiresAt can still read pending; see Expired links
createdAtstringISO 8601 timestamp of creation
expiresAtstringISO 8601 timestamp of expiration
cancelledAtstring (optional)ISO 8601 timestamp of cancellation. Omitted when not set
completedAtstring (optional)ISO 8601 timestamp of completion. Omitted when not set
isTestbooleanWhether this is a test payment
redirectUrlstring (optional)Custom redirect URL. Omitted when not set
metadataobject (optional)Custom metadata. Omitted when not set
paymentMethodsstring[] (optional)Allowed payment methods (e.g., ["card", "yappy", "paypal"]). Omitted when not set
requestCustomerEmailbooleanWhether the payment page requests the customer’s email

Optional fields are left out when they have no value; they are never null. The list does not include customerEmail; use GET /v1/payment-links/{hashUrl} to read it.

FieldTypeDescription
totalintegerTotal number of payment links matching the query, among the links the API key can see
CodeDescription
200OK - Payment links retrieved successfully
400Bad Request - Invalid query parameters (or unknown fields / any expand)
401Unauthorized - Missing X-API-Key header
403Forbidden - Account suspended
404Not Found - Unknown API key (ERR_ACCOUNT_API_KEY_NOT_FOUND)
429Too Many Requests - Rate limit exceeded (plain-text body; wait Retry-After seconds)

Invalid query parameters return 400 with the validation result: error.issues lists each failing parameter (path). Example for lim=500:

{
"success": false,
"error": {
"issues": [
{
"code": "too_big",
"maximum": 100,
"type": "number",
"inclusive": true,
"exact": false,
"message": "Number must be less than or equal to 100",
"path": ["lim"]
}
],
"name": "ZodError"
}
}
{
"success": false,
"message": "Missing X-API-Key header",
"error": {
"code": "ERR_API_KEY_MISSING"
},
"timestamp": "2024-01-15T10:30:00.000Z"
}
async function getAllPaymentLinks(apiKey, options = {}) {
try {
const params = new URLSearchParams();
if (options.lim) params.append('lim', options.lim);
if (options.off) params.append('off', options.off);
if (options.status) params.append('status', options.status);
if (options.isTest !== undefined) params.append('isTest', options.isTest);
const url = `https://api-pay.zelta.dev/v1/payment-links?${params}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
}
});
if (response.status === 429) {
// Rate limited: plain-text body, wait Retry-After seconds
const error = new Error('Rate limited');
error.retryAfter = Number(response.headers.get('Retry-After') ?? 60);
throw error;
}
const result = await response.json();
if (!response.ok) {
// 400 validation errors carry error.issues and no message
throw new Error(result.message ?? JSON.stringify(result.error?.issues));
}
return result.data; // { paymentLinks, total }
} catch (error) {
console.error('Error retrieving payment links:', error);
throw error;
}
}
// Usage
const data = await getAllPaymentLinks('your-api-key');
console.log('Payment links:', data.paymentLinks);
console.log('Total matching links:', data.total);
import requests
def get_all_payment_links(api_key, options=None):
try:
params = {}
if options:
if 'lim' in options:
params['lim'] = options['lim']
if 'off' in options:
params['off'] = options['off']
if 'status' in options:
params['status'] = options['status']
if 'isTest' in options:
params['isTest'] = str(options['isTest']).lower() # 'true' or 'false'
headers = {
'X-API-Key': api_key,
'Content-Type': 'application/json'
}
response = requests.get(
'https://api-pay.zelta.dev/v1/payment-links',
headers=headers,
params=params
)
if response.status_code == 429:
# Rate limited: plain-text body, wait Retry-After seconds
raise Exception(f"Rate limited, retry in {response.headers.get('Retry-After')}s")
result = response.json()
if not response.ok:
# 400 validation errors carry error.issues and no message
raise Exception(result.get('message') or result['error'].get('issues'))
return result['data']
except Exception as error:
print(f'Error retrieving payment links: {error}')
raise
# Usage
data = get_all_payment_links('your-api-key')
print('Payment links:', data['paymentLinks'])
print('Total matching links:', data['total'])
async function getAllPaymentLinksPaginated(apiKey) {
try {
let allPaymentLinks = [];
let off = 0;
const lim = 100; // maximum page size
let hasNext = true;
while (hasNext) {
const data = await getAllPaymentLinks(apiKey, { lim, off });
allPaymentLinks = allPaymentLinks.concat(data.paymentLinks);
hasNext = data.paymentLinks.length > 0 && off + data.paymentLinks.length < data.total;
off += lim;
}
return allPaymentLinks;
} catch (error) {
console.error('Error retrieving all payment links:', error);
throw error;
}
}
// Usage
const allLinks = await getAllPaymentLinksPaginated('your-api-key');
console.log(`Retrieved ${allLinks.length} payment links`);
def get_all_payment_links_paginated(api_key):
try:
all_payment_links = []
off = 0
lim = 100 # maximum page size
has_next = True
while has_next:
data = get_all_payment_links(api_key, {'lim': lim, 'off': off})
all_payment_links.extend(data['paymentLinks'])
has_next = len(data['paymentLinks']) > 0 and off + len(data['paymentLinks']) < data['total']
off += lim
return all_payment_links
except Exception as error:
print(f'Error retrieving all payment links: {error}')
raise
# Usage
all_links = get_all_payment_links_paginated('your-api-key')
print(f'Retrieved {len(all_links)} payment links')
async function getPaymentLinksByStatus(apiKey, status) {
try {
const data = await getAllPaymentLinks(apiKey, { status });
return data.paymentLinks;
} catch (error) {
console.error('Error retrieving payment links by status:', error);
throw error;
}
}
async function getTestPaymentLinks(apiKey) {
try {
const data = await getAllPaymentLinks(apiKey, { isTest: true });
return data.paymentLinks;
} catch (error) {
console.error('Error retrieving test payment links:', error);
throw error;
}
}
// Usage
const completedLinks = await getPaymentLinksByStatus('your-api-key', 'completed');
const testLinks = await getTestPaymentLinks('your-api-key');
def get_payment_links_by_status(api_key, status):
try:
data = get_all_payment_links(api_key, {'status': status})
return data['paymentLinks']
except Exception as error:
print(f'Error retrieving payment links by status: {error}')
raise
def get_test_payment_links(api_key):
try:
data = get_all_payment_links(api_key, {'isTest': True})
return data['paymentLinks']
except Exception as error:
print(f'Error retrieving test payment links: {error}')
raise
# Usage
completed_links = get_payment_links_by_status('your-api-key', 'completed')
test_links = get_test_payment_links('your-api-key')

Get recent payment links for dashboard display:

async function getDashboardData(apiKey) {
try {
// Get recent payment links (last 20)
const recentData = await getAllPaymentLinks(apiKey, { lim: 20 });
// Get completed payments for this month
const completedData = await getAllPaymentLinks(apiKey, {
status: 'completed',
lim: 100
});
// Calculate totals
const totalCompleted = completedData.paymentLinks.reduce((sum, link) => sum + link.amount, 0);
const completedCount = completedData.paymentLinks.length;
return {
recentLinks: recentData.paymentLinks,
totalCompleted,
completedCount,
total: recentData.total
};
} catch (error) {
console.error('Error getting dashboard data:', error);
throw error;
}
}
// Usage
const dashboardData = await getDashboardData('your-api-key');
console.log('Dashboard data:', dashboardData);

Get payment links for specific orders:

async function getOrderPaymentLinks(apiKey, orderIds) {
try {
// Get all payment links
const allLinks = await getAllPaymentLinksPaginated(apiKey);
// Filter by order IDs
const orderLinks = allLinks.filter(link =>
link.metadata?.orderId && orderIds.includes(link.metadata.orderId)
);
return orderLinks;
} catch (error) {
console.error('Error getting order payment links:', error);
throw error;
}
}
// Usage
const orderLinks = await getOrderPaymentLinks('your-api-key', ['ORD-001', 'ORD-002']);
console.log('Order payment links:', orderLinks);

Get payment history for a specific customer:

async function getCustomerPaymentHistory(apiKey, customerId) {
try {
// Get all payment links
const allLinks = await getAllPaymentLinksPaginated(apiKey);
// Filter by customer ID
const customerLinks = allLinks.filter(link =>
link.metadata?.customerId === customerId
);
// Sort by creation date (newest first)
customerLinks.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
return customerLinks;
} catch (error) {
console.error('Error getting customer payment history:', error);
throw error;
}
}
// Usage
const customerHistory = await getCustomerPaymentHistory('your-api-key', 'CUST-123');
console.log('Customer payment history:', customerHistory);

Generate payment analytics:

async function generatePaymentAnalytics(apiKey) {
try {
// Get all payment links
const allLinks = await getAllPaymentLinksPaginated(apiKey);
// Calculate analytics
const analytics = {
total: allLinks.length,
completed: allLinks.filter(link => link.status === 'completed').length,
pending: allLinks.filter(link =>
link.status === 'pending' && new Date(link.expiresAt) > new Date()
).length,
processing: allLinks.filter(link => link.status === 'processing').length,
cancelled: allLinks.filter(link => link.status === 'cancelled').length,
// A link past expiresAt can still read 'pending' (see Expired links)
expired: allLinks.filter(link =>
link.status === 'expired' ||
(link.status === 'pending' && new Date(link.expiresAt) <= new Date())
).length,
totalAmount: allLinks
.filter(link => link.status === 'completed')
.reduce((sum, link) => sum + link.amount, 0),
averageAmount: 0,
statusDistribution: {},
monthlyTrends: {}
};
// Calculate average amount
if (analytics.completed > 0) {
analytics.averageAmount = analytics.totalAmount / analytics.completed;
}
// Calculate status distribution
allLinks.forEach(link => {
analytics.statusDistribution[link.status] =
(analytics.statusDistribution[link.status] || 0) + 1;
});
// Calculate monthly trends
allLinks.forEach(link => {
const month = new Date(link.createdAt).toISOString().substring(0, 7); // YYYY-MM
if (!analytics.monthlyTrends[month]) {
analytics.monthlyTrends[month] = 0;
}
analytics.monthlyTrends[month]++;
});
return analytics;
} catch (error) {
console.error('Error generating payment analytics:', error);
throw error;
}
}
// Usage
const analytics = await generatePaymentAnalytics('your-api-key');
console.log('Payment analytics:', analytics);

Use appropriate page sizes for your use case:

// For dashboard: small pages
const dashboardData = await getAllPaymentLinks(apiKey, { lim: 20 });
// For bulk operations: larger pages
const bulkData = await getAllPaymentLinks(apiKey, { lim: 100 });
// For complete dataset: paginated retrieval
const allData = await getAllPaymentLinksPaginated(apiKey);

Cache frequently accessed data:

const paymentLinkCache = new Map();
async function getCachedPaymentLinks(apiKey, cacheKey, ttl = 300000) { // 5 minutes
const cached = paymentLinkCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const data = await getAllPaymentLinks(apiKey);
paymentLinkCache.set(cacheKey, {
data: data,
timestamp: Date.now()
});
return data;
}

Handle different error scenarios:

async function getPaymentLinksSafely(apiKey, options = {}) {
try {
return await getAllPaymentLinks(apiKey, options);
} catch (error) {
if (error.retryAfter) {
// 429: wait the Retry-After seconds, then retry once
await new Promise(resolve => setTimeout(resolve, error.retryAfter * 1000));
return await getAllPaymentLinks(apiKey, options);
}
throw error;
}
}

Implement rate limiting for batch operations:

async function getPaymentLinksWithRateLimit(apiKey, options = {}, delay = 1000) {
try {
const data = await getAllPaymentLinks(apiKey, options);
// Rate limiting delay
await new Promise(resolve => setTimeout(resolve, delay));
return data;
} catch (error) {
console.error('Error with rate limiting:', error);
throw error;
}
}

Test pagination functionality:

// Test first page
const page1 = await getAllPaymentLinks('your-api-key', { lim: 5, off: 0 });
// Test second page
const page2 = await getAllPaymentLinks('your-api-key', { lim: 5, off: 5 });
// Test pagination info: total is the same on every page
console.log('Page 1:', page1.paymentLinks.length, 'of', page1.total);
console.log('Page 2:', page2.paymentLinks.length, 'of', page2.total);

Test filtering functionality:

// Test status filter
const completedLinks = await getAllPaymentLinks('your-api-key', { status: 'completed' });
// Test test mode filter
const testLinks = await getAllPaymentLinks('your-api-key', { isTest: true });
// Test combined filters
const completedTestLinks = await getAllPaymentLinks('your-api-key', {
status: 'completed',
isTest: true
});