Skip to content

Get Payment Link

Retrieve a single payment link by its hash URL.

GET /v1/payment-links/{hashUrl}

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

An account-level key can read any payment link of the account. A project API key can only read the links of its project (the links created with the project’s key); any other link returns the same 404 ERR_PAYMENT_LINK_NOT_FOUND as a link that does not exist.

ParameterTypeRequiredDescription
hashUrlstringYesThe link’s 24-character hash: the last path segment of paymentLinkUrl (for https://pay.zelta.dev/Xk3v9Q_aB7mN2pL8rT4wYz1c it is Xk3v9Q_aB7mN2pL8rT4wYz1c). The link’s id cannot be used here
Terminal window
curl -X GET "https://api-pay.zelta.dev/v1/payment-links/Xk3v9Q_aB7mN2pL8rT4wYz1c" \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json"
const hashUrl = 'Xk3v9Q_aB7mN2pL8rT4wYz1c';
const response = await fetch(`https://api-pay.zelta.dev/v1/payment-links/${hashUrl}`, {
method: 'GET',
headers: {
'X-API-Key': 'your-api-key-here',
'Content-Type': 'application/json'
}
});
const result = await response.json();
console.log(result);
import requests
hash_url = 'Xk3v9Q_aB7mN2pL8rT4wYz1c'
headers = {
'X-API-Key': 'your-api-key-here',
'Content-Type': 'application/json'
}
response = requests.get(
f'https://api-pay.zelta.dev/v1/payment-links/{hash_url}',
headers=headers
)
result = response.json()
print(result)
$hashUrl = 'Xk3v9Q_aB7mN2pL8rT4wYz1c';
$headers = [
'X-API-Key: your-api-key-here',
'Content-Type: application/json'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api-pay.zelta.dev/v1/payment-links/{$hashUrl}");
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($response, true);
{
"success": true,
"data": {
"paymentLink": {
"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
}
},
"message": "Operation completed successfully",
"timestamp": "2024-01-15T10:30:00.000Z"
}
FieldTypeDescription
idstringUnique payment link identifier (a numeric string)
paymentLinkUrlstringURL for the payment link
customerNamestringName of the customer
customerEmailstring (optional)Customer’s email address. Omitted when not set
conceptstringDescription of what’s being paid for
amountintegerAmount in cents
statusstringStored payment link status: pending, processing, completed, cancelled or expired (see Payment Link Statuses)
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)Payment methods allowed when the link was created: any of "card", "yappy", "paypal". Omitted when none were specified (every active method is offered).
requestCustomerEmailbooleanWhether the payment page requests the customer’s email
StatusMeaning
pendingWaiting for payment
processingA payment is being confirmed. With PayPal this can last from minutes to several days while PayPal reviews the payment. The link cannot be paid again or cancelled meanwhile.
completedPayment successful
cancelledManually cancelled
expiredPast its expiresAt and stored as expired

This endpoint returns the stored status and never updates it. Links are not expired proactively and no webhook is sent on expiry: a pending link past its expiresAt is stored as expired only when a listing (GET /v1/payment-links or the dashboard) returns it on a page, so until then this endpoint keeps returning pending. Compare expiresAt with the current time to know whether the link can still be paid.

A processing link normally ends as completed (you receive the payment.success webhook), or goes back to pending if the payment is declined or abandoned, so the customer can try again.

CodeDescription
200OK - Payment link retrieved successfully
401Unauthorized - Missing X-API-Key header
403Forbidden - Account suspended
404Not Found - Unknown API key (ERR_ACCOUNT_API_KEY_NOT_FOUND), or payment link not found or not reachable with this key (ERR_PAYMENT_LINK_NOT_FOUND): another account’s link or, with a project key, a link outside the project
429Too Many Requests - Rate limit exceeded (plain-text body; wait Retry-After seconds)
{
"success": false,
"message": "Payment link not found",
"error": {
"code": "ERR_PAYMENT_LINK_NOT_FOUND"
},
"timestamp": "2024-01-15T10:30:00.000Z"
}
{
"success": false,
"message": "Missing X-API-Key header",
"error": {
"code": "ERR_API_KEY_MISSING"
},
"timestamp": "2024-01-15T10:30:00.000Z"
}

An unknown, regenerated or deleted key returns 404:

{
"success": false,
"message": "Api key not found",
"error": {
"code": "ERR_ACCOUNT_API_KEY_NOT_FOUND"
},
"timestamp": "2024-01-15T10:30:00.000Z"
}
async function getPaymentLink(apiKey, hashUrl) {
try {
const response = await fetch(`https://api-pay.zelta.dev/v1/payment-links/${hashUrl}`, {
method: 'GET',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
}
});
if (response.status === 429) {
// Rate limited: plain-text body, wait Retry-After seconds
throw new Error(`Rate limited, retry in ${response.headers.get('Retry-After')}s`);
}
const result = await response.json();
if (!response.ok) {
// 400 validation errors carry error.issues and no message
const error = new Error(result.message ?? JSON.stringify(result.error?.issues));
error.code = result.error?.code;
throw error;
}
return result.data.paymentLink;
} catch (error) {
console.error('Error retrieving payment link:', error);
throw error;
}
}
// Usage
const paymentLink = await getPaymentLink('your-api-key', 'Xk3v9Q_aB7mN2pL8rT4wYz1c');
console.log('Payment link:', paymentLink);
import requests
def get_payment_link(api_key, hash_url):
try:
headers = {
'X-API-Key': api_key,
'Content-Type': 'application/json'
}
response = requests.get(
f'https://api-pay.zelta.dev/v1/payment-links/{hash_url}',
headers=headers
)
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']['paymentLink']
except Exception as error:
print(f'Error retrieving payment link: {error}')
raise
# Usage
payment_link = get_payment_link('your-api-key', 'Xk3v9Q_aB7mN2pL8rT4wYz1c')
print('Payment link:', payment_link)
async function checkPaymentLinkStatus(apiKey, hashUrl) {
try {
const paymentLink = await getPaymentLink(apiKey, hashUrl);
// The stored status can still read 'pending' after expiresAt
const pastExpiry = new Date(paymentLink.expiresAt) <= new Date();
return {
status: paymentLink.status,
isCompleted: paymentLink.status === 'completed',
isPending: paymentLink.status === 'pending' && !pastExpiry,
isCancelled: paymentLink.status === 'cancelled',
isExpired: paymentLink.status === 'expired' || (paymentLink.status === 'pending' && pastExpiry),
completedAt: paymentLink.completedAt,
cancelledAt: paymentLink.cancelledAt,
expiresAt: paymentLink.expiresAt
};
} catch (error) {
console.error('Error checking payment link status:', error);
throw error;
}
}
// Usage
const status = await checkPaymentLinkStatus('your-api-key', 'Xk3v9Q_aB7mN2pL8rT4wYz1c');
console.log('Payment link status:', status);
from datetime import datetime, timezone
def check_payment_link_status(api_key, hash_url):
try:
payment_link = get_payment_link(api_key, hash_url)
# The stored status can still read 'pending' after expiresAt
expires_at = datetime.fromisoformat(payment_link['expiresAt'].replace('Z', '+00:00'))
past_expiry = expires_at <= datetime.now(timezone.utc)
status = payment_link['status']
return {
'status': status,
'is_completed': status == 'completed',
'is_pending': status == 'pending' and not past_expiry,
'is_cancelled': status == 'cancelled',
'is_expired': status == 'expired' or (status == 'pending' and past_expiry),
# Optional fields are omitted when not set
'completed_at': payment_link.get('completedAt'),
'cancelled_at': payment_link.get('cancelledAt'),
'expires_at': payment_link['expiresAt']
}
except Exception as error:
print(f'Error checking payment link status: {error}')
raise
# Usage
status = check_payment_link_status('your-api-key', 'Xk3v9Q_aB7mN2pL8rT4wYz1c')
print('Payment link status:', status)
async function checkOrderStatus(apiKey, orderId) {
try {
// Get all payment links (every page, see List Payment Links)
const allLinks = await getAllPaymentLinksPaginated(apiKey);
// Find payment link for this order
const orderPaymentLink = allLinks.find(link =>
link.metadata?.orderId === orderId
);
if (!orderPaymentLink) {
throw new Error('Payment link not found for order');
}
// Get detailed payment link info
const hashUrl = orderPaymentLink.paymentLinkUrl.split('/').pop();
const paymentLink = await getPaymentLink(apiKey, hashUrl);
return {
orderId: orderId,
paymentLinkId: paymentLink.id,
paymentLinkUrl: paymentLink.paymentLinkUrl,
status: paymentLink.status,
amount: paymentLink.amount,
customerName: paymentLink.customerName,
concept: paymentLink.concept,
createdAt: paymentLink.createdAt,
completedAt: paymentLink.completedAt,
cancelledAt: paymentLink.cancelledAt,
expiresAt: paymentLink.expiresAt
};
} catch (error) {
console.error('Error checking order status:', error);
throw error;
}
}
// Usage
const orderStatus = await checkOrderStatus('your-api-key', 'ORD-001');
console.log('Order status:', orderStatus);
def check_order_status(api_key, order_id):
try:
# Get all payment links (every page, see List Payment Links)
all_links = get_all_payment_links_paginated(api_key)
# Find payment link for this order
order_payment_link = None
for link in all_links:
if link.get('metadata', {}).get('orderId') == order_id:
order_payment_link = link
break
if not order_payment_link:
raise Exception('Payment link not found for order')
# Get detailed payment link info
hash_url = order_payment_link['paymentLinkUrl'].split('/')[-1]
payment_link = get_payment_link(api_key, hash_url)
return {
'orderId': order_id,
'paymentLinkId': payment_link['id'],
'paymentLinkUrl': payment_link['paymentLinkUrl'],
'status': payment_link['status'],
'amount': payment_link['amount'],
'customerName': payment_link['customerName'],
'concept': payment_link['concept'],
'createdAt': payment_link['createdAt'],
# Optional fields are omitted when not set
'completedAt': payment_link.get('completedAt'),
'cancelledAt': payment_link.get('cancelledAt'),
'expiresAt': payment_link['expiresAt']
}
except Exception as error:
print(f'Error checking order status: {error}')
raise
# Usage
order_status = check_order_status('your-api-key', 'ORD-001')
print('Order status:', order_status)

Check payment status for order confirmation pages:

async function getOrderPaymentStatus(apiKey, orderId) {
try {
const orderStatus = await checkOrderStatus(apiKey, orderId);
// The stored status can still read 'pending' after expiresAt
const pastExpiry = new Date(orderStatus.expiresAt) <= new Date();
return {
orderId: orderStatus.orderId,
paymentStatus: orderStatus.status,
amount: orderStatus.amount,
customerName: orderStatus.customerName,
concept: orderStatus.concept,
paymentUrl: orderStatus.paymentLinkUrl, // use the URL as returned
isPaid: orderStatus.status === 'completed',
isPending: orderStatus.status === 'pending' && !pastExpiry,
isCancelled: orderStatus.status === 'cancelled',
isExpired: orderStatus.status === 'expired' || (orderStatus.status === 'pending' && pastExpiry),
completedAt: orderStatus.completedAt,
expiresAt: orderStatus.expiresAt
};
} catch (error) {
console.error('Error getting order payment status:', error);
throw error;
}
}
// Usage in order status page
const paymentStatus = await getOrderPaymentStatus('your-api-key', 'ORD-001');
console.log('Payment status:', paymentStatus);

Verify payment completion before processing orders:

async function verifyPaymentCompletion(apiKey, orderId) {
try {
const orderStatus = await checkOrderStatus(apiKey, orderId);
if (orderStatus.status === 'completed') {
// Payment is complete, process the order
await processOrder(orderId);
return {
success: true,
message: 'Payment verified and order processed',
orderId: orderId,
amount: orderStatus.amount,
completedAt: orderStatus.completedAt
};
} else if (orderStatus.status === 'pending' && new Date(orderStatus.expiresAt) > new Date()) {
// Payment is still pending (and the link has not expired)
return {
success: false,
message: 'Payment is still pending',
orderId: orderId,
status: orderStatus.status,
expiresAt: orderStatus.expiresAt
};
} else {
// Payment failed, cancelled, or expired
return {
success: false,
message: 'Payment not completed',
orderId: orderId,
status: orderStatus.status,
cancelledAt: orderStatus.cancelledAt,
expiresAt: orderStatus.expiresAt
};
}
} catch (error) {
console.error('Error verifying payment completion:', error);
throw error;
}
}
// Usage
const verification = await verifyPaymentCompletion('your-api-key', 'ORD-001');
console.log('Payment verification:', verification);

Retrieve payment information for customer support:

async function getCustomerPaymentInfo(apiKey, customerId) {
try {
// Get all payment links (every page, see List Payment Links)
const allLinks = await getAllPaymentLinksPaginated(apiKey);
// Filter by customer ID
const customerPayments = allLinks.filter(link =>
link.metadata?.customerId === customerId
);
// Get detailed information for each payment
const detailedPayments = await Promise.all(
customerPayments.map(async (link) => {
const hashUrl = link.paymentLinkUrl.split('/').pop();
const paymentLink = await getPaymentLink(apiKey, hashUrl);
return {
paymentLinkId: paymentLink.id,
status: paymentLink.status,
amount: paymentLink.amount,
concept: paymentLink.concept,
createdAt: paymentLink.createdAt,
completedAt: paymentLink.completedAt,
cancelledAt: paymentLink.cancelledAt,
expiresAt: paymentLink.expiresAt,
metadata: paymentLink.metadata
};
})
);
return {
customerId: customerId,
totalPayments: detailedPayments.length,
completedPayments: detailedPayments.filter(p => p.status === 'completed').length,
pendingPayments: detailedPayments.filter(p =>
p.status === 'pending' && new Date(p.expiresAt) > new Date()
).length,
cancelledPayments: detailedPayments.filter(p => p.status === 'cancelled').length,
// A link past expiresAt can still read 'pending'
expiredPayments: detailedPayments.filter(p =>
p.status === 'expired' ||
(p.status === 'pending' && new Date(p.expiresAt) <= new Date())
).length,
totalAmount: detailedPayments
.filter(p => p.status === 'completed')
.reduce((sum, p) => sum + p.amount, 0),
payments: detailedPayments
};
} catch (error) {
console.error('Error getting customer payment info:', error);
throw error;
}
}
// Usage
const customerInfo = await getCustomerPaymentInfo('your-api-key', 'CUST-123');
console.log('Customer payment info:', customerInfo);

Always handle different error scenarios:

async function getPaymentLinkSafely(apiKey, hashUrl) {
try {
return await getPaymentLink(apiKey, hashUrl);
} catch (error) {
if (error.code === 'ERR_PAYMENT_LINK_NOT_FOUND') {
return null; // Payment link doesn't exist (or belongs to another account)
}
if (error.code === 'ERR_ACCOUNT_API_KEY_NOT_FOUND' || error.code === 'ERR_API_KEY_MISSING') {
throw new Error('Authentication failed');
}
throw error; // Re-throw other errors
}
}

Cache payment link data to reduce API calls:

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

Implement rate limiting for batch operations:

async function getMultiplePaymentLinks(apiKey, hashUrls, delay = 1000) {
const results = [];
for (const hashUrl of hashUrls) {
try {
const paymentLink = await getPaymentLink(apiKey, hashUrl);
results.push({
hashUrl,
success: true,
paymentLink
});
} catch (error) {
results.push({
hashUrl,
success: false,
error: error.message
});
}
// Rate limiting delay
await new Promise(resolve => setTimeout(resolve, delay));
}
return results;
}

Log payment link retrieval for audit purposes:

async function getPaymentLinkWithLogging(apiKey, hashUrl, reason = 'Manual retrieval') {
try {
const paymentLink = await getPaymentLink(apiKey, hashUrl);
console.log('Payment link retrieved:', {
paymentLinkId: paymentLink.id,
hashUrl: hashUrl,
reason: reason,
status: paymentLink.status,
timestamp: new Date().toISOString()
});
return paymentLink;
} catch (error) {
console.error('Failed to retrieve payment link:', {
hashUrl: hashUrl,
reason: reason,
error: error.message,
timestamp: new Date().toISOString()
});
throw error;
}
}

Test retrieval of payment links in different states:

// Test pending payment link
const pendingLink = await getPaymentLink('your-api-key', 'pending-hash');
// Test completed payment link
const completedLink = await getPaymentLink('your-api-key', 'completed-hash');
// Test cancelled payment link
const cancelledLink = await getPaymentLink('your-api-key', 'cancelled-hash');

Test error handling:

// Test non-existent payment link
try {
await getPaymentLink('your-api-key', 'non-existent-hash');
} catch (error) {
console.log('Expected error:', error.message);
}
// Test invalid API key (404 ERR_ACCOUNT_API_KEY_NOT_FOUND)
try {
await getPaymentLink('invalid-key', 'valid-hash');
} catch (error) {
console.log('Expected error:', error.code, error.message);
}