Cancel Payment Link
Cancel a payment link to prevent further payments.
Endpoint
Section titled “Endpoint”PATCH /v1/payment-links/{hashUrl}/cancelAuthentication
Section titled “Authentication”Requires API key authentication via X-API-Key header.
An account-level key can cancel any payment link of the account. A project API key can only cancel 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, and the link is left as it was.
Path Parameters
Section titled “Path Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
hashUrl | string | Yes | The 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 |
Request Example
Section titled “Request Example”cURL Example
Section titled “cURL Example”curl -X PATCH "https://api-pay.zelta.dev/v1/payment-links/Xk3v9Q_aB7mN2pL8rT4wYz1c/cancel" \ -H "X-API-Key: your-api-key-here" \ -H "Content-Type: application/json"Node.js Example
Section titled “Node.js Example”const hashUrl = 'Xk3v9Q_aB7mN2pL8rT4wYz1c';const response = await fetch(`https://api-pay.zelta.dev/v1/payment-links/${hashUrl}/cancel`, { method: 'PATCH', headers: { 'X-API-Key': 'your-api-key-here', 'Content-Type': 'application/json' }});
const result = await response.json();console.log(result);Python Example
Section titled “Python Example”import requests
hash_url = 'Xk3v9Q_aB7mN2pL8rT4wYz1c'headers = { 'X-API-Key': 'your-api-key-here', 'Content-Type': 'application/json'}
response = requests.patch( f'https://api-pay.zelta.dev/v1/payment-links/{hash_url}/cancel', headers=headers)
result = response.json()print(result)PHP Example
Section titled “PHP Example”$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}/cancel");curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');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);Response Format
Section titled “Response Format”Success Response
Section titled “Success Response”{ "success": true, "data": { "paymentLink": { "hashUrl": "Xk3v9Q_aB7mN2pL8rT4wYz1c", "customerName": "John Doe", "concept": "Consulting Service", "amount": 2500, "status": "cancelled", "createdAt": "2024-01-15T10:30:00.000Z", "cancelledAt": "2024-01-15T12:00:00.000Z" } }, "message": "Payment link cancelled successfully", "timestamp": "2024-01-15T12:00:00.000Z"}Response Fields
Section titled “Response Fields”| Field | Type | Description |
|---|---|---|
hashUrl | string | Hash identifier of the payment link (the last path segment of paymentLinkUrl) |
customerName | string | Name of the customer |
concept | string | Description of what’s being paid for |
amount | integer | Amount in cents |
status | string | Payment link status (now “cancelled”) |
createdAt | string | ISO 8601 timestamp of creation |
cancelledAt | string | ISO 8601 timestamp of cancellation |
Status Codes
Section titled “Status Codes”| Code | Description |
|---|---|
200 | OK - Payment link cancelled successfully |
400 | Bad Request - Invalid state for cancellation (ERR_INVALID_STATE) |
401 | Unauthorized - Missing X-API-Key header |
403 | Forbidden - Account suspended |
404 | Not 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 |
409 | Conflict - A payment for this link is being confirmed (ERR_PAYMENT_IN_PROGRESS) |
429 | Too Many Requests - Rate limit exceeded (plain-text body; wait Retry-After seconds) |
Error Responses
Section titled “Error Responses”Payment Link Not Found
Section titled “Payment Link Not Found”{ "success": false, "message": "Payment link not found", "error": { "code": "ERR_PAYMENT_LINK_NOT_FOUND" }, "timestamp": "2024-01-15T12:00:00.000Z"}Invalid State for Cancellation
Section titled “Invalid State for Cancellation”Returned for a completed or cancelled link. The message names the current status:
{ "success": false, "message": "Cannot cancel a payment link with status 'completed'", "error": { "code": "ERR_INVALID_STATE" }, "timestamp": "2024-01-15T12:00:00.000Z"}Payment in Progress
Section titled “Payment in Progress”Returned while the link is processing: a customer’s payment is being confirmed (for example, a PayPal payment that PayPal is still reviewing, which can take up to several days). Cancelling now could leave a charged customer with a cancelled link, so the request is refused.
{ "success": false, "message": "A payment for this link is being confirmed", "error": { "code": "ERR_PAYMENT_IN_PROGRESS" }, "timestamp": "2024-01-15T12:00:00.000Z"}Wait for the outcome instead of retrying in a tight loop: the payment.success webhook tells you the link was paid (it can no longer be cancelled); if the payment is declined or abandoned the link returns to pending and can be cancelled.
Missing API Key
Section titled “Missing API Key”{ "success": false, "message": "Missing X-API-Key header", "error": { "code": "ERR_API_KEY_MISSING" }, "timestamp": "2024-01-15T12:00:00.000Z"}Cancellation Rules
Section titled “Cancellation Rules”Allowed States
Section titled “Allowed States”Payment links can only be cancelled if they are in the following states:
pending- Waiting for payment (including a link past itsexpiresAtwhose stored status still readspending)expired- Past itsexpiresAtand stored as expired
Not Allowed States
Section titled “Not Allowed States”Payment links cannot be cancelled if they are in the following states:
completed- Payment successful (400 ERR_INVALID_STATE)cancelled- Already cancelled (400 ERR_INVALID_STATE)processing- A payment is being confirmed (409 ERR_PAYMENT_IN_PROGRESS)
A link that has sat in processing for more than 10 minutes, with no payment that could still settle it, is treated as abandoned and can be cancelled.
The cancellation is atomic: if a customer’s payment starts at the same moment, the payment wins and the cancel returns 409.
Implementation Examples
Section titled “Implementation Examples”Basic Cancellation
Section titled “Basic Cancellation”Node.js Example
Section titled “Node.js Example”async function cancelPaymentLink(apiKey, hashUrl) { try { const response = await fetch(`https://api-pay.zelta.dev/v1/payment-links/${hashUrl}/cancel`, { method: 'PATCH', 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 cancelling payment link:', error); throw error; }}
// Usageconst cancelledLink = await cancelPaymentLink('your-api-key', 'Xk3v9Q_aB7mN2pL8rT4wYz1c');console.log('Payment link cancelled:', cancelledLink);Python Example
Section titled “Python Example”import requests
def cancel_payment_link(api_key, hash_url): try: headers = { 'X-API-Key': api_key, 'Content-Type': 'application/json' }
response = requests.patch( f'https://api-pay.zelta.dev/v1/payment-links/{hash_url}/cancel', 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 cancelling payment link: {error}') raise
# Usagecancelled_link = cancel_payment_link('your-api-key', 'Xk3v9Q_aB7mN2pL8rT4wYz1c')print('Payment link cancelled:', cancelled_link)Safe Cancellation with State Check
Section titled “Safe Cancellation with State Check”Node.js Example
Section titled “Node.js Example”async function safeCancelPaymentLink(apiKey, hashUrl) { try { // First, check the current status const getResponse = await fetch(`https://api-pay.zelta.dev/v1/payment-links/${hashUrl}`, { headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' } });
if (getResponse.status === 429) { // Rate limited: plain-text body, wait Retry-After seconds throw new Error(`Rate limited, retry in ${getResponse.headers.get('Retry-After')}s`); }
const getResult = await getResponse.json();
if (!getResponse.ok) { throw new Error(getResult.message ?? JSON.stringify(getResult.error?.issues)); }
const paymentLink = getResult.data.paymentLink;
// Check if cancellation is allowed if (paymentLink.status === 'completed') { throw new Error('Cannot cancel completed payment link'); }
if (paymentLink.status === 'processing') { throw new Error('A payment for this link is being confirmed; try again later'); }
if (paymentLink.status === 'cancelled') { console.log('Payment link is already cancelled'); return paymentLink; }
// Proceed with cancellation return await cancelPaymentLink(apiKey, hashUrl); } catch (error) { console.error('Error in safe cancellation:', error); throw error; }}
// Usageconst result = await safeCancelPaymentLink('your-api-key', 'Xk3v9Q_aB7mN2pL8rT4wYz1c');console.log('Cancellation result:', result);Use Cases
Section titled “Use Cases”1. Order Cancellation
Section titled “1. Order Cancellation”Cancel payment links when orders are cancelled:
async function cancelOrderPaymentLink(apiKey, orderId) { try { // Get the payment link for the order (every page, see List Payment Links) const paymentLinks = await getAllPaymentLinksPaginated(apiKey); const orderPaymentLink = paymentLinks.find(link => link.metadata?.orderId === orderId );
if (!orderPaymentLink) { throw new Error('Payment link not found for order'); }
// Cancel the payment link const cancelledLink = await cancelPaymentLink(apiKey, orderPaymentLink.paymentLinkUrl.split('/').pop());
// Update order status await updateOrderStatus(orderId, 'cancelled');
return cancelledLink; } catch (error) { console.error('Error cancelling order payment link:', error); throw error; }}2. Expired Link Cleanup
Section titled “2. Expired Link Cleanup”Cancel expired payment links:
async function cleanupExpiredPaymentLinks(apiKey) { try { const allLinks = await getAllPaymentLinksPaginated(apiKey); // Only open links can be cancelled: completed/cancelled links past // expiresAt would return 400 ERR_INVALID_STATE const expiredLinks = allLinks.filter(link => link.status === 'expired' || (link.status === 'pending' && new Date(link.expiresAt) < new Date()) );
const results = await Promise.allSettled( expiredLinks.map(link => cancelPaymentLink(apiKey, link.paymentLinkUrl.split('/').pop()) ) );
const successful = results.filter(result => result.status === 'fulfilled'); const failed = results.filter(result => result.status === 'rejected');
console.log(`Cancelled ${successful.length} expired payment links`); if (failed.length > 0) { console.log(`Failed to cancel ${failed.length} payment links`); }
return { successful: successful.length, failed: failed.length }; } catch (error) { console.error('Error cleaning up expired payment links:', error); throw error; }}3. Customer Request Cancellation
Section titled “3. Customer Request Cancellation”Handle customer requests to cancel payments:
// hashUrl: the last path segment of paymentLinkUrl (the link id doesn't work here)async function handleCustomerCancellationRequest(apiKey, customerId, hashUrl) { try { // Verify the payment link belongs to the customer const paymentLink = await getPaymentLink(apiKey, hashUrl);
if (paymentLink.metadata?.customerId !== customerId) { throw new Error('Payment link does not belong to customer'); }
// Check if cancellation is allowed if (paymentLink.status === 'completed') { throw new Error('Cannot cancel completed payment'); }
if (paymentLink.status === 'cancelled') { return { message: 'Payment link is already cancelled', paymentLink }; }
// Cancel the payment link const cancelledLink = await cancelPaymentLink(apiKey, hashUrl);
// Send cancellation confirmation await sendCancellationConfirmationEmail(customerId, cancelledLink);
return { message: 'Payment link cancelled successfully', paymentLink: cancelledLink }; } catch (error) { console.error('Error handling customer cancellation:', error); throw error; }}Best Practices
Section titled “Best Practices”1. Check Status Before Cancellation
Section titled “1. Check Status Before Cancellation”Always check the current status before attempting cancellation:
async function cancelPaymentLinkSafely(apiKey, hashUrl) { try { const paymentLink = await getPaymentLink(apiKey, hashUrl);
if (paymentLink.status === 'completed') { throw new Error('Cannot cancel completed payment link'); }
if (paymentLink.status === 'cancelled') { console.log('Payment link is already cancelled'); return paymentLink; }
return await cancelPaymentLink(apiKey, hashUrl); } catch (error) { console.error('Error in safe cancellation:', error); throw error; }}2. Handle Errors Gracefully
Section titled “2. Handle Errors Gracefully”Implement proper error handling for different scenarios:
async function cancelPaymentLinkWithErrorHandling(apiKey, hashUrl) { try { return await cancelPaymentLink(apiKey, hashUrl); } catch (error) { if (error.code === 'ERR_PAYMENT_LINK_NOT_FOUND') { return { success: false, error: 'Payment link not found' }; }
if (error.code === 'ERR_INVALID_STATE') { return { success: false, error: 'Payment link cannot be cancelled' }; }
if (error.code === 'ERR_PAYMENT_IN_PROGRESS') { return { success: false, error: 'A payment is being confirmed; wait for its outcome' }; }
throw error; }}3. Log Cancellation Events
Section titled “3. Log Cancellation Events”Log all cancellation events for audit purposes:
async function cancelPaymentLinkWithLogging(apiKey, hashUrl, reason = 'Manual cancellation') { try { const paymentLink = await cancelPaymentLink(apiKey, hashUrl);
// Log the cancellation event console.log('Payment link cancelled:', { hashUrl: paymentLink.hashUrl, reason: reason, cancelledAt: paymentLink.cancelledAt, timestamp: new Date().toISOString() });
return paymentLink; } catch (error) { console.error('Failed to cancel payment link:', { hashUrl: hashUrl, reason: reason, error: error.message, timestamp: new Date().toISOString() }); throw error; }}4. Rate Limiting for Batch Operations
Section titled “4. Rate Limiting for Batch Operations”Implement rate limiting for batch cancellation operations:
async function cancelPaymentLinksWithRateLimit(apiKey, hashUrls, delay = 1000) { const results = [];
for (const hashUrl of hashUrls) { try { const paymentLink = await cancelPaymentLink(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;}Related Endpoints
Section titled “Related Endpoints”- List Payment Links - Retrieve multiple payment links
- Get Payment Link - Retrieve a single payment link
- Create Payment Link - Create a new payment link