Creating Payment Links
This guide covers everything you need to know about creating payment links with the Zelta Pay API, including required fields, validation rules, and best practices.
Basic Payment Link Creation
Section titled “Basic Payment Link Creation”To create a payment link, send a POST request to the /v1/payment-links endpoint with the required parameters in the request body.
Endpoint
Section titled “Endpoint”POST /v1/payment-links
Request Body Parameters
Section titled “Request Body Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
concept | string | Yes | Description of what’s being paid for |
amount | integer | Yes | Amount in cents (minimum 100, maximum 1000000) |
customerName | string | Yes | Name of the customer (1-120 characters) |
isTest | boolean | Yes | Whether this is a test payment |
customerEmail | string | No | Customer’s email address (valid email format) |
requestCustomerEmail | boolean | No | Whether the payment page asks the customer for their email. Defaults to true when omitted. Sending true together with customerEmail is rejected; send false to skip the email step. |
redirectUrl | string | No | Custom http/https redirect URL after payment |
metadata | object | No | Custom metadata (max 20 keys, 8192 bytes) |
paymentMethods | string[] | No | Allowed payment methods ("card", "yappy", "paypal"). At least one required when specified. No duplicates allowed. |
expiresInDays | integer | No | How many days the link stays payable, from 1 to 60. Defaults to 3 days when omitted. |
Field Validation
Section titled “Field Validation”String values are trimmed of leading and trailing whitespace before they are validated.
Amount
Section titled “Amount”- Minimum: 100 cents ($1.00)
- Maximum: 1,000,000 cents ($10,000.00)
- Type: Positive integer
0, negative, non-integer and above-1,000,000 amounts return the 400 validation error (error.issues). An amount from 1 to 99 cents passes that validation and is then rejected with a coded 400 in the standard envelope; this check runs after the active-provider and payment-method checks:
{ "success": false, "message": "El monto mínimo para un link de pago es 1.00 USD (100 centavos); no se puede generar el link de pago", "error": { "code": "ERR_AMOUNT_BELOW_NMI_MINIMUM" }, "timestamp": "2024-01-15T10:30:00.000Z"}With a project-scoped API key, a payment_link.rejected webhook is also sent to the project’s webhook (if one is enabled).
Concept
Section titled “Concept”- Required: Cannot be empty
- Type: String
Customer Name
Section titled “Customer Name”- Required: Cannot be empty
- Length: 1-120 characters (after trimming)
- Type: String
Customer Email
Section titled “Customer Email”- Type: Valid email format
- Optional: Will prefill the email field on the hosted payment page
Redirect URL
Section titled “Redirect URL”- Type: Valid URL
- Maximum length: 2048 characters
- Protocol:
http://orhttps://only; any other scheme returns400(“redirectUrl must use the http or https scheme”). HTTPS recommended
Request Customer Email
Section titled “Request Customer Email”- Type: Boolean
- Optional: Defaults to
truewhen omitted - Constraint: An explicit
requestCustomerEmail: truesent together withcustomerEmailis rejected - With
customerEmail: If you sendcustomerEmailand omitrequestCustomerEmail, the link is stored and returned withrequestCustomerEmail: true, and the payment page still asks for the email (prefilled). SendrequestCustomerEmail: falseto skip the email step
Metadata
Section titled “Metadata”- Keys: 1-40 characters (any characters)
- Values: String (max 255 chars), number, boolean, or null
- Maximum keys: 20 per payment link
- Total size: Maximum 8192 bytes when JSON serialized
- Cannot be empty object: If metadata is provided, it must contain at least one key-value pair
Payment Methods
Section titled “Payment Methods”- Type: Array of strings
- Allowed values:
"card","yappy","paypal" - Minimum items: 1 (when specified)
- No duplicates: Each payment method can only appear once
- Validation: Each specified method must be active on the account for the link’s environment (
isTest: trueuses your sandbox providers,isTest: falseyour production providers) - Default: When omitted, the payment page offers every payment method active on the account for that environment
"paypal" lets the customer pay with a PayPal account, or with a debit/credit card through PayPal’s guest checkout (no PayPal account needed).
For example, to offer card and PayPal only:
{ "concept": "Order #12345", "amount": 4999, "customerName": "Maria Lopez", "isTest": true, "paymentMethods": ["card", "paypal"]}Expires In Days
Section titled “Expires In Days”- Type: Integer (whole days;
1.5or"7"are rejected) - Range: 1 to 60
- Optional: When omitted, the link expires 3 days after it is created
- Result:
expiresAtin the response is the creation time plusexpiresInDays× 24 hours
For example, a link that stays payable for 60 days:
{ "concept": "Invoice #2026-0042", "amount": 125000, "customerName": "Acme Corp", "isTest": false, "expiresInDays": 60}After expiresAt the link can no longer be paid: payment attempts are rejected with “Payment link has expired”. Links are not expired proactively and no webhook is sent on expiry. The stored status changes to expired when a listing (GET /v1/payment-links or the dashboard) returns the link on a page; that listing still shows pending, and later reads show expired. GET /v1/payment-links/{hashUrl} returns the stored status, so it keeps showing pending until then. Compare expiresAt with the current time to know whether a link is still payable.
Request Examples
Section titled “Request Examples”Basic Payment Link
Section titled “Basic Payment Link”cURL Example
Section titled “cURL Example”curl -X POST "https://api-pay.zelta.dev/v1/payment-links" \ -H "X-API-Key: your-api-key-here" \ -H "Content-Type: application/json" \ -d '{ "concept": "Consulting Service", "amount": 2500, "customerName": "John Doe", "isTest": true }'Node.js Example
Section titled “Node.js Example”const paymentLinkData = { concept: "Consulting Service", amount: 2500, // $25.00 in cents customerName: "John Doe", isTest: true};
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', { method: 'POST', headers: { 'X-API-Key': 'your-api-key-here', 'Content-Type': 'application/json' }, body: JSON.stringify(paymentLinkData)});
const result = await response.json();console.log(result);Cloudflare Workers Example
Section titled “Cloudflare Workers Example”export default { async fetch(request, env, ctx) { const paymentLinkData = { concept: "Consulting Service", amount: 2500, // $25.00 in cents customerName: "John Doe", isTest: true };
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', { method: 'POST', headers: { 'X-API-Key': env.ZELTA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify(paymentLinkData) });
const result = await response.json(); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); }};Hono with Cloudflare Workers Example
Section titled “Hono with Cloudflare Workers Example”import { Hono } from 'hono';import { cors } from 'hono/cors';
type Bindings = { ZELTA_API_KEY: string;};
const app = new Hono<{ Bindings: Bindings }>();
// Enable CORSapp.use('*', cors());
app.post('/payment-links', async (c) => { const paymentLinkData = { concept: "Consulting Service", amount: 2500, // $25.00 in cents customerName: "John Doe", isTest: true };
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', { method: 'POST', headers: { 'X-API-Key': c.env.ZELTA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify(paymentLinkData) });
const result = await response.json(); return c.json(result);});
export default app;Python Example
Section titled “Python Example”import requestsimport json
payment_link_data = { "concept": "Consulting Service", "amount": 2500, # $25.00 in cents "customerName": "John Doe", "isTest": True}
headers = { 'X-API-Key': 'your-api-key-here', 'Content-Type': 'application/json'}
response = requests.post( 'https://api-pay.zelta.dev/v1/payment-links', headers=headers, data=json.dumps(payment_link_data))
result = response.json()print(result)PHP Example
Section titled “PHP Example”$paymentLinkData = [ 'concept' => 'Consulting Service', 'amount' => 2500, // $25.00 in cents 'customerName' => 'John Doe', 'isTest' => true];
$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');curl_setopt($ch, CURLOPT_POST, true);curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($paymentLinkData));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);Payment Link with Metadata
Section titled “Payment Link with Metadata”cURL Example
Section titled “cURL Example”curl -X POST "https://api-pay.zelta.dev/v1/payment-links" \ -H "X-API-Key: your-api-key-here" \ -H "Content-Type: application/json" \ -d '{ "concept": "Order #12345 - Electronics Store", "amount": 15999, "customerName": "Alice Johnson", "isTest": false, "redirectUrl": "https://mystore.com/orders/12345/success", "metadata": { "orderId": "ORD-12345", "customerId": "CUST-789", "items": "laptop, mouse, keyboard", "shippingMethod": "express", "couponCode": "SAVE10" } }'Node.js Example
Section titled “Node.js Example”const paymentLinkData = { concept: "Order #12345 - Electronics Store", amount: 15999, // $159.99 in cents customerName: "Alice Johnson", isTest: false, redirectUrl: "https://mystore.com/orders/12345/success", metadata: { orderId: "ORD-12345", customerId: "CUST-789", items: "laptop, mouse, keyboard", shippingMethod: "express", couponCode: "SAVE10" }};
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', { method: 'POST', headers: { 'X-API-Key': 'your-api-key-here', 'Content-Type': 'application/json' }, body: JSON.stringify(paymentLinkData)});
const result = await response.json();console.log(result);Cloudflare Workers Example
Section titled “Cloudflare Workers Example”export default { async fetch(request, env, ctx) { const paymentLinkData = { concept: "Order #12345 - Electronics Store", amount: 15999, // $159.99 in cents customerName: "Alice Johnson", isTest: false, redirectUrl: "https://mystore.com/orders/12345/success", metadata: { orderId: "ORD-12345", customerId: "CUST-789", items: "laptop, mouse, keyboard", shippingMethod: "express", couponCode: "SAVE10" } };
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', { method: 'POST', headers: { 'X-API-Key': env.ZELTA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify(paymentLinkData) });
const result = await response.json(); return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json' } }); }};Hono with Cloudflare Workers Example
Section titled “Hono with Cloudflare Workers Example”import { Hono } from 'hono';import { cors } from 'hono/cors';
type Bindings = { ZELTA_API_KEY: string;};
const app = new Hono<{ Bindings: Bindings }>();
// Enable CORSapp.use('*', cors());
app.post('/payment-links', async (c) => { const paymentLinkData = { concept: "Order #12345 - Electronics Store", amount: 15999, // $159.99 in cents customerName: "Alice Johnson", isTest: false, redirectUrl: "https://mystore.com/orders/12345/success", metadata: { orderId: "ORD-12345", customerId: "CUST-789", items: "laptop, mouse, keyboard", shippingMethod: "express", couponCode: "SAVE10" } };
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', { method: 'POST', headers: { 'X-API-Key': c.env.ZELTA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify(paymentLinkData) });
const result = await response.json(); return c.json(result);});
export default app;Python Example
Section titled “Python Example”payment_link_data = { "concept": "Order #12345 - Electronics Store", "amount": 15999, # $159.99 in cents "customerName": "Alice Johnson", "isTest": False, "redirectUrl": "https://mystore.com/orders/12345/success", "metadata": { "orderId": "ORD-12345", "customerId": "CUST-789", "items": "laptop, mouse, keyboard", "shippingMethod": "express", "couponCode": "SAVE10" }}
headers = { 'X-API-Key': 'your-api-key-here', 'Content-Type': 'application/json'}
response = requests.post( 'https://api-pay.zelta.dev/v1/payment-links', headers=headers, data=json.dumps(payment_link_data))
result = response.json()print(result)Response Format
Section titled “Response Format”Success Response
Section titled “Success Response”{ "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-18T10:30:00.000Z", "isTest": true, "redirectUrl": "https://myapp.com/success", "metadata": { "orderId": "ORD-001", "service": "consulting" }, "paymentMethods": ["card", "yappy"], "requestCustomerEmail": true } }, "message": "Payment link created successfully", "timestamp": "2024-01-15T10:30:00.000Z"}Response Fields
Section titled “Response Fields”| Field | Type | Description |
|---|---|---|
id | string | Unique payment link identifier (a numeric string) |
paymentLinkUrl | string | URL for the payment link. Its last path segment is the hashUrl used by GET /v1/payment-links/{hashUrl} and cancel |
customerName | string | Name of the customer |
customerEmail | string (optional) | Customer’s email address. Omitted when not set |
concept | string | Description of what’s being paid for |
amount | integer | Amount in cents |
status | string | Payment link status (always “pending” for new links) |
createdAt | string | ISO 8601 timestamp of creation |
expiresAt | string | ISO 8601 timestamp of expiration: expiresInDays days after creation, or 3 days when expiresInDays is omitted |
cancelledAt | string (optional) | ISO 8601 timestamp of cancellation. Omitted when not set |
completedAt | string (optional) | ISO 8601 timestamp of completion. Omitted when not set |
isTest | boolean | Whether this is a test payment |
redirectUrl | string (optional) | Custom redirect URL. Omitted when not set |
metadata | object (optional) | Custom metadata. Omitted when not set |
paymentMethods | string[] (optional) | Allowed payment methods (e.g., ["card", "yappy", "paypal"]). Omitted when not set (every active method is offered) |
requestCustomerEmail | boolean | Whether the payment page requests the customer’s email |
Optional fields are left out of the response when they have no value; they are never null. customerEmail is returned by create and GET /v1/payment-links/{hashUrl}, but not by the list endpoint.
Status Codes
Section titled “Status Codes”| Code | Description |
|---|---|
201 | Created - Payment link created successfully |
400 | Bad Request - Validation error, amount below 100 cents, inactive payment method or no active payment provider |
401 | Unauthorized - Missing X-API-Key header |
403 | Forbidden - Account suspended (or, with a project key, project inactive) |
404 | Not Found - Unknown API key (ERR_ACCOUNT_API_KEY_NOT_FOUND) or account not found |
429 | Too Many Requests - Rate limit exceeded (plain-text body; wait Retry-After seconds) |
Error Responses
Section titled “Error Responses”Validation Error
Section titled “Validation Error”Invalid or missing fields return 400 with the validation result instead of the usual error envelope: error.issues lists the failing fields (path) with a readable message and a machine-readable code. There is no message, error.code or timestamp at the top level. An unknown field produces one unrecognized_keys issue with path: [] and the field names in keys. The cross-field check (customerEmail together with requestCustomerEmail: true) is usually reported together with the other issues, but some field errors stop it from running (for example a missing field or an invalid paymentMethods value), so it can appear on a later request after you fix them.
{ "success": false, "error": { "issues": [ { "code": "too_small", "minimum": 0, "type": "number", "inclusive": false, "exact": false, "message": "Amount must be a positive integer", "path": ["amount"] } ], "name": "ZodError" }}Amount Below Minimum
Section titled “Amount Below Minimum”An amount from 1 to 99 cents (see Amount):
{ "success": false, "message": "El monto mínimo para un link de pago es 1.00 USD (100 centavos); no se puede generar el link de pago", "error": { "code": "ERR_AMOUNT_BELOW_NMI_MINIMUM" }, "timestamp": "2024-01-15T10:30:00.000Z"}No Active Payment Provider
Section titled “No Active Payment Provider”Returned when no payment provider is enabled for the link’s environment: an isTest: true link needs an enabled sandbox provider, an isTest: false link an enabled production provider.
{ "success": false, "message": "No active payment provider found", "error": { "code": "ERR_NO_ACTIVE_PAYMENT_PROVIDER" }, "timestamp": "2024-01-15T10:30:00.000Z"}Missing Required Fields
Section titled “Missing Required Fields”A missing field reports "Required"; an empty concept reports "Concept is required".
{ "success": false, "error": { "issues": [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": ["concept"], "message": "Required" } ], "name": "ZodError" }}Invalid Amount
Section titled “Invalid Amount”{ "success": false, "error": { "issues": [ { "code": "too_big", "maximum": 1000000, "type": "number", "inclusive": true, "exact": false, "message": "Amount must be less than 10,000.00 USD (or 1,000,000 cents)", "path": ["amount"] } ], "name": "ZodError" }}Metadata Validation Error
Section titled “Metadata Validation Error”{ "success": false, "error": { "issues": [ { "code": "custom", "message": "Metadata cannot have more than 20 keys", "path": ["metadata"] } ], "name": "ZodError" }}Inactive Payment Method
Section titled “Inactive Payment Method”{ "success": false, "message": "Payment method(s) not active: yappy", "error": { "code": "ERR_INACTIVE_PAYMENT_METHOD" }, "timestamp": "2024-01-15T10:30:00.000Z"}Implementation Examples
Section titled “Implementation Examples”Basic Payment Link Creation
Section titled “Basic Payment Link Creation”Node.js Example
Section titled “Node.js Example”async function createPaymentLink(apiKey, data) { try { const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', { method: 'POST', headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
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 throw new Error(result.message ?? JSON.stringify(result.error?.issues)); }
return result.data.paymentLink; } catch (error) { console.error('Error creating payment link:', error); throw error; }}
// Usageconst paymentLink = await createPaymentLink('your-api-key', { concept: 'Consulting Service', amount: 2500, customerName: 'John Doe', isTest: true});
console.log('Payment link created:', paymentLink);Cloudflare Workers Example
Section titled “Cloudflare Workers Example”export default { async fetch(request, env, ctx) { async function createPaymentLink(apiKey, data) { try { const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', { method: 'POST', headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
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 throw new Error(result.message ?? JSON.stringify(result.error?.issues)); }
return result.data.paymentLink; } catch (error) { console.error('Error creating payment link:', error); throw error; } }
// Usage try { const paymentLink = await createPaymentLink(env.ZELTA_API_KEY, { concept: 'Consulting Service', amount: 2500, customerName: 'John Doe', isTest: true });
return new Response(JSON.stringify({ paymentLink }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } }};Hono with Cloudflare Workers Example
Section titled “Hono with Cloudflare Workers Example”import { Hono } from 'hono';import { cors } from 'hono/cors';
type Bindings = { ZELTA_API_KEY: string;};
const app = new Hono<{ Bindings: Bindings }>();
// Enable CORSapp.use('*', cors());
async function createPaymentLink(apiKey: string, data: any) { try { const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', { method: 'POST', headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
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 throw new Error(result.message ?? JSON.stringify(result.error?.issues)); }
return result.data.paymentLink; } catch (error) { console.error('Error creating payment link:', error); throw error; }}
app.post('/create-payment-link', async (c) => { try { const paymentLink = await createPaymentLink(c.env.ZELTA_API_KEY, { concept: 'Consulting Service', amount: 2500, customerName: 'John Doe', isTest: true });
return c.json({ paymentLink }); } catch (error) { return c.json({ error: error.message }, 500); }});
export default app;Python Example
Section titled “Python Example”import requestsimport json
def create_payment_link(api_key, data): try: headers = { 'X-API-Key': api_key, 'Content-Type': 'application/json' }
response = requests.post( 'https://api-pay.zelta.dev/v1/payment-links', headers=headers, data=json.dumps(data) )
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 creating payment link: {error}') raise
# Usagepayment_link = create_payment_link('your-api-key', { 'concept': 'Consulting Service', 'amount': 2500, 'customerName': 'John Doe', 'isTest': True})
print('Payment link created:', payment_link)E-commerce Order Payment Link
Section titled “E-commerce Order Payment Link”Node.js Example
Section titled “Node.js Example”async function createOrderPaymentLink(apiKey, order) { const paymentLinkData = { concept: `Order #${order.id} - ${order.storeName}`, amount: order.totalAmount, // Already in cents customerName: order.customerName, isTest: process.env.NODE_ENV !== 'production', redirectUrl: `${process.env.FRONTEND_URL}/orders/${order.id}/success`, metadata: { orderId: order.id, customerId: order.customerId, items: order.items.map(item => item.name).join(', '), shippingMethod: order.shippingMethod, couponCode: order.couponCode || null } };
return await createPaymentLink(apiKey, paymentLinkData);}
// Usageconst order = { id: 'ORD-12345', storeName: 'Electronics Store', totalAmount: 15999, customerName: 'Alice Johnson', customerId: 'CUST-789', items: [ { name: 'laptop' }, { name: 'mouse' }, { name: 'keyboard' } ], shippingMethod: 'express', couponCode: 'SAVE10'};
const paymentLink = await createOrderPaymentLink('your-api-key', order);console.log('Order payment link:', paymentLink);Python Example
Section titled “Python Example”def create_order_payment_link(api_key, order): payment_link_data = { 'concept': f'Order #{order["id"]} - {order["store_name"]}', 'amount': order['total_amount'], # Already in cents 'customerName': order['customer_name'], 'isTest': os.getenv('FLASK_ENV') != 'production', 'redirectUrl': f"{os.getenv('FRONTEND_URL')}/orders/{order['id']}/success", 'metadata': { 'orderId': order['id'], 'customerId': order['customer_id'], 'items': ', '.join([item['name'] for item in order['items']]), 'shippingMethod': order['shipping_method'], 'couponCode': order.get('coupon_code') } }
return create_payment_link(api_key, payment_link_data)
# Usageorder = { 'id': 'ORD-12345', 'store_name': 'Electronics Store', 'total_amount': 15999, 'customer_name': 'Alice Johnson', 'customer_id': 'CUST-789', 'items': [ {'name': 'laptop'}, {'name': 'mouse'}, {'name': 'keyboard'} ], 'shipping_method': 'express', 'coupon_code': 'SAVE10'}
payment_link = create_order_payment_link('your-api-key', order)print('Order payment link:', payment_link)Subscription Payment Link
Section titled “Subscription Payment Link”Node.js Example
Section titled “Node.js Example”async function createSubscriptionPaymentLink(apiKey, subscription) { const paymentLinkData = { concept: `${subscription.planName} - ${subscription.billingCycle}`, amount: subscription.amount, customerName: subscription.customerName, isTest: process.env.NODE_ENV !== 'production', redirectUrl: `${process.env.FRONTEND_URL}/subscriptions/${subscription.id}/success`, metadata: { subscriptionId: subscription.id, customerId: subscription.customerId, plan: subscription.planName, billingCycle: subscription.billingCycle, startDate: subscription.startDate, trialEndsAt: subscription.trialEndsAt } };
return await createPaymentLink(apiKey, paymentLinkData);}
// Usageconst subscription = { id: 'SUB-456', planName: 'Premium Plan', billingCycle: 'monthly', amount: 2999, customerName: 'Bob Smith', customerId: 'CUST-123', startDate: '2024-01-15', trialEndsAt: '2024-02-15'};
const paymentLink = await createSubscriptionPaymentLink('your-api-key', subscription);console.log('Subscription payment link:', paymentLink);Validation Helpers
Section titled “Validation Helpers”Amount Validation
Section titled “Amount Validation”Node.js Example
Section titled “Node.js Example”function validateAmount(dollars) { const cents = Math.round(dollars * 100);
if (cents < 100) { throw new Error('Amount must be at least $1.00'); }
if (cents > 1000000) { throw new Error('Amount cannot exceed $10,000.00'); }
return cents;}
// Usageconst amount = validateAmount(25.99); // Returns 2599Python Example
Section titled “Python Example”def validate_amount(dollars): cents = round(dollars * 100)
if cents < 100: raise ValueError('Amount must be at least $1.00')
if cents > 1000000: raise ValueError('Amount cannot exceed $10,000.00')
return cents
# Usageamount = validate_amount(25.99) # Returns 2599Metadata Validation
Section titled “Metadata Validation”Node.js Example
Section titled “Node.js Example”function validateMetadata(metadata) { if (!metadata || Object.keys(metadata).length === 0) { throw new Error('Metadata cannot be empty'); }
if (Object.keys(metadata).length > 20) { throw new Error('Metadata cannot have more than 20 keys'); }
for (const [key, value] of Object.entries(metadata)) { if (key.length < 1 || key.length > 40) { throw new Error('Metadata key must be 1-40 characters'); }
if (typeof value === 'string' && value.length > 255) { throw new Error('Metadata string value cannot exceed 255 characters'); } }
// Check total size const jsonString = JSON.stringify(metadata); if (new TextEncoder().encode(jsonString).byteLength > 8192) { throw new Error('Metadata is too large'); }
return metadata;}
// Usageconst metadata = validateMetadata({ orderId: 'ORD-001', customerId: 12345, isVip: true});Python Example
Section titled “Python Example”def validate_metadata(metadata): if not metadata or len(metadata) == 0: raise ValueError('Metadata cannot be empty')
if len(metadata) > 20: raise ValueError('Metadata cannot have more than 20 keys')
for key, value in metadata.items(): if len(key) < 1 or len(key) > 40: raise ValueError('Metadata key must be 1-40 characters')
if isinstance(value, str) and len(value) > 255: raise ValueError('Metadata string value cannot exceed 255 characters')
# Check total size json_string = json.dumps(metadata) if len(json_string.encode('utf-8')) > 8192: raise ValueError('Metadata is too large')
return metadata
# Usagemetadata = validate_metadata({ 'orderId': 'ORD-001', 'customerId': 12345, 'isVip': True})Best Practices
Section titled “Best Practices”1. Error Handling
Section titled “1. Error Handling”Always handle validation errors gracefully:
async function createPaymentLinkWithValidation(apiKey, data) { try { // Validate amount if (data.amount < 100 || data.amount > 1000000) { throw new Error('Amount must be between $1.00 and $10,000.00'); }
// Validate required fields if (!data.concept || !data.customerName) { throw new Error('Concept and customerName are required'); }
return await createPaymentLink(apiKey, data); } catch (error) { console.error('Validation error:', error.message); throw error; }}2. Use Test Mode
Section titled “2. Use Test Mode”Always use test mode during development:
const isTest = process.env.NODE_ENV !== 'production';
const paymentLinkData = { concept: 'Test Payment', amount: 100, customerName: 'Test User', isTest: isTest};3. Metadata Strategy
Section titled “3. Metadata Strategy”Use consistent metadata keys and appropriate data types:
const metadata = { orderId: 'ORD-001', // String customerId: 12345, // Number isVip: true, // Boolean notes: null // Null};4. Retry Logic
Section titled “4. Retry Logic”Implement retry logic for network errors. Don’t retry 400 validation errors, and after a 429 wait the number of seconds in the Retry-After header:
async function createPaymentLinkWithRetry(apiKey, data, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await createPaymentLink(apiKey, data); } catch (error) { if (attempt === maxRetries - 1) { throw error; }
// Exponential backoff await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000)); } }}Testing
Section titled “Testing”Test Payment Links
Section titled “Test Payment Links”Use test mode for development and testing:
{ "concept": "Test Payment", "amount": 100, "customerName": "Test User", "isTest": true}Validation Testing
Section titled “Validation Testing”Test validation rules:
// Test minimum amountconst minAmountTest = { concept: 'Test', amount: 50, // Below minimum customerName: 'Test', isTest: true};
// Test maximum amountconst maxAmountTest = { concept: 'Test', amount: 2000000, // Above maximum customerName: 'Test', isTest: true};Next Steps
Section titled “Next Steps”Now that you understand payment link creation, explore these guides:
- Webhooks - Receive real-time payment notifications
- Signature Verification - Secure webhook handling
- Use Cases - Real-world integration examples
- API Reference - Complete endpoint documentation