Create Pay Link
Create a new payment link with specific validity modes.
POST
https://dev.mytpe.app/api/v2/mytpe-pay/links/storeCreates a new payment link for a Mytpe Pay instance.
Authentication: API Key (X-API-KEY + X-API-SECRET headers)
This API requires authentication via X-API-KEY and X-API-SECRET headers.
Constraint: Link Types & Modes
It is critical to understand the relationship between type and payment_mode.
- Dynamic Links: You set a fixed price that the customer must pay. These support all 3 modes (
reusable,one_shot,limited). - Static Links: The customer enters the amount (e.g., Donation). These are always Reusable. You cannot set them to
one_shotorlimited.
Validation Rule
If you send type: "static", the system will ignore any payment_mode or max_payments values you provide. These parameters are only respected when type is dynamic.
Validity & Modes (Dynamic Only)
For Dynamic links, you can define the lifecycle:
reusable(Default): The link never expires. Ideal for e-commerce products.one_shot: The link accepts exactly 1 payment and auto-closes. Ideal for specific invoices.limited: The link accepts a quota defined bymax_payments. Ideal for event tickets.
Body Parameters
Prop
Type
Settings
You can configure link behavior via the settings object. All settings have sensible defaults — you only need to include the ones you want to override.
Prop
Type
Form-data format
settings[show_retry_button]:false
settings[allow_anonymous]:trueDefault Behavior
If you don't provide settings, all defaults apply:
show_retry_buttondefaults totrueallow_anonymousdefaults tofalse(payer details are required)
Example: Disable Retry Button
To create a link where the customer cannot retry payment after a failure:
const form = new FormData();
form.append('mytpe_pay_id', 'mytpe_pay_xyz789');
form.append('title', 'One-time Invoice');
form.append('details', 'Invoice #1234');
form.append('type', 'dynamic');
form.append('amount', '5000.00');
form.append('slug', 'invoice-1234');
form.append('payment_mode', 'one_shot');
form.append('settings[show_retry_button]', 'false');
const response = await fetch('https://dev.mytpe.app/api/v2/mytpe-pay/links/store', {
method: 'POST',
headers: {
'X-API-KEY': 'your_api_key',
'X-API-SECRET': 'your_api_secret',
},
body: form,
});When a payment fails on this link, the results page will not show a "Retry" button — the customer will only see the "Home" button.
Example: Allow Anonymous Payments
To create a donation-style link where customers can pay without revealing identity:
const form = new FormData();
form.append('mytpe_pay_id', 'mytpe_pay_xyz789');
form.append('title', 'Donate to our cause');
form.append('details', 'Anonymous donations welcome.');
form.append('type', 'static');
form.append('slug', 'donate');
form.append('settings[allow_anonymous]', 'true');
const response = await fetch('https://dev.mytpe.app/api/v2/mytpe-pay/links/store', {
method: 'POST',
headers: {
'X-API-KEY': 'your_api_key',
'X-API-SECRET': 'your_api_secret',
},
body: form,
});The checkout page will display an "Anonymous payment" checkbox. When checked, the name, email, and phone fields are hidden and the transaction is recorded with no payer details.
Privacy Trade-off
Anonymous payments are useful for donations, charities, or sensitive transactions, but you will not be able to send payment receipts or contact the payer. Use only when payer identification is not required.
Inactivity Expiry
What is inactivity expiry?
A one_shot or limited link can auto-close (status → expired) if it doesn't receive any successful payment within a configurable window. This is useful for time-sensitive use cases like delivery payments, ticket reservations, or limited promotions.
The timeout is resolved with a 2-level fallback chain:
- Per-link override —
inactivity_timeout_minuteson this request - Instance default —
default_inactivity_timeout_minuteson the parent Mytpe Pay instance (Update Instance) - Otherwise — the link never inactivity-expires (only the existing
max_paymentsquota can close it)
Rules
- Unit: minutes (integer, range
1–525600= 1 minute to 1 year). - Modes: only
one_shotandlimited. Reusable links never inactivity-expire — sending this field on a reusable link is ignored. - Activity definition: the clock resets only on successful payments (status
completed). Pending/failed/refused transactions do not reset the clock. The clock starts at link creation. - Resulting status: when a link auto-expires, its
statusbecomesexpiredand apay_link.expiredwebhook fires (see Webhook Events). - Reactivation: expired links are terminal — they cannot be reactivated. Create a new link instead.
Form-data format
inactivity_timeout_minutes:2880(2880 minutes = 48 hours)
Example: 30-minute reservation hold
A "Reserve seat" link that auto-expires if the customer doesn't complete payment within 30 minutes:
const form = new FormData();
form.append('mytpe_pay_id', 'mytpe_pay_xyz789');
form.append('title', 'Seat reservation #A12');
form.append('details', 'Hold expires in 30 minutes if unpaid.');
form.append('type', 'dynamic');
form.append('amount', '1500.00');
form.append('slug', 'seat-a12-reservation');
form.append('payment_mode', 'one_shot');
form.append('inactivity_timeout_minutes', '30');
const response = await fetch('https://dev.mytpe.app/api/v2/mytpe-pay/links/store', {
method: 'POST',
headers: {
'X-API-KEY': 'your_api_key',
'X-API-SECRET': 'your_api_secret',
},
body: form,
});Example: 48-hour delivery link
form.append('inactivity_timeout_minutes', '2880'); // 48h × 60 = 2880Example: Inherit from instance default
Simply omit inactivity_timeout_minutes from the request body. The link will use whatever default the parent instance has configured (or never expire if the instance has no default either).
User-Defined Fields (UDFs)
New Feature
UDFs let you attach custom data to every payment made through a link. This data is sent to the payment gateway and appears on the transaction receipt — useful for tracking invoices, customer references, or any business-specific identifier.
You can attach up to 5 UDFs (udf1 through udf5) to a payment link. Each UDF has:
value(required): The data you want to attach (max 20 alphanumeric characters — letters and numbers only, no special characters or spaces).label(optional): A human-readable name displayed on the receipt (max 50 characters).
udf1 — Bank Receipt Field
udf1 is special: its value is displayed on the bank payment receipt, making it ideal for reconciliation references (e.g., invoice number, order ID). If you don't set udf1, the system auto-generates an order number for it. The other UDFs (udf2–udf5) are only stored on the SATIM platform and MyTPE — they do not appear on the receipt.
When a customer pays on this link, the UDFs are automatically forwarded to the payment gateway.
UDF Format (form-data)
udfs[udf1][value]:INV2026001
udfs[udf1][label]:Invoice Number
udfs[udf2][value]:JohnDoe
udfs[udf2][label]:Customer NameUDF Parameters
Prop
Type
Example Use Cases
- E-commerce: Attach an invoice number and customer name to every payment.
- Events: Attach a ticket ID and seat number.
- Services: Attach a contract reference and service date.
Example Request
const form = new FormData();
form.append('mytpe_pay_id', 'mytpe_pay_xyz789');
form.append('title', 'Limited Offer Product');
form.append('details', 'A special limited edition product.');
form.append('type', 'dynamic');
form.append('amount', '5000.00');
form.append('slug', 'limited-offer');
form.append('payment_mode', 'limited');
form.append('max_payments', '50');
form.append('metadata[internal_id]', 'PROD_123');
form.append('udfs[udf1][value]', 'INV2026001');
form.append('udfs[udf1][label]', 'Invoice Number');
form.append('udfs[udf2][value]', 'JohnDoe');
form.append('udfs[udf2][label]', 'Customer Name');
form.append('logo', document.querySelector('#logo').files[0]);
const response = await fetch('https://dev.mytpe.app/api/v2/mytpe-pay/links/store', {
method: 'POST',
headers: {
'X-API-KEY': 'your_api_key',
'X-API-SECRET': 'your_api_secret',
},
body: form,
});
const data = await response.json();
console.log(data);interface PayLinkResponse {
success: boolean;
message: string;
data: {
id: string;
title: string;
slug: string;
type: string;
amount: string;
status: string;
payment_mode: string;
max_payments: number | null;
payment_link: string;
payment_status: string;
};
}
const form = new FormData();
form.append('mytpe_pay_id', 'mytpe_pay_xyz789');
form.append('title', 'Limited Offer Product');
form.append('details', 'A special limited edition product.');
form.append('type', 'dynamic');
form.append('amount', '5000.00');
form.append('slug', 'limited-offer');
form.append('payment_mode', 'limited');
form.append('max_payments', '50');
form.append('metadata[internal_id]', 'PROD_123');
form.append('udfs[udf1][value]', 'INV2026001');
form.append('udfs[udf1][label]', 'Invoice Number');
form.append('udfs[udf2][value]', 'JohnDoe');
form.append('udfs[udf2][label]', 'Customer Name');
const fileInput = document.querySelector<HTMLInputElement>('#logo');
if (fileInput?.files?.[0]) {
form.append('logo', fileInput.files[0]);
}
const response = await fetch('https://dev.mytpe.app/api/v2/mytpe-pay/links/store', {
method: 'POST',
headers: {
'X-API-KEY': 'your_api_key',
'X-API-SECRET': 'your_api_secret',
},
body: form,
});
const data: PayLinkResponse = await response.json();
console.log(data);import requests
url = 'https://dev.mytpe.app/api/v2/mytpe-pay/links/store'
headers = {
'X-API-KEY': 'your_api_key',
'X-API-SECRET': 'your_api_secret',
}
payload = {
'mytpe_pay_id': 'mytpe_pay_xyz789',
'title': 'Limited Offer Product',
'details': 'A special limited edition product.',
'type': 'dynamic',
'amount': '5000.00',
'slug': 'limited-offer',
'payment_mode': 'limited',
'max_payments': '50',
'metadata[internal_id]': 'PROD_123',
'udfs[udf1][value]': 'INV2026001',
'udfs[udf1][label]': 'Invoice Number',
'udfs[udf2][value]': 'JohnDoe',
'udfs[udf2][label]': 'Customer Name',
}
files = {
'logo': ('logo.png', open('/path/to/logo.png', 'rb'), 'image/png'),
}
response = requests.post(url, headers=headers, data=payload, files=files)
data = response.json()
print(data)use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'X-API-KEY' => 'your_api_key',
'X-API-SECRET' => 'your_api_secret',
])
->attach('logo', file_get_contents('/path/to/logo.png'), 'logo.png')
->post('https://dev.mytpe.app/api/v2/mytpe-pay/links/store', [
'mytpe_pay_id' => 'mytpe_pay_xyz789',
'title' => 'Limited Offer Product',
'details' => 'A special limited edition product.',
'type' => 'dynamic',
'amount' => '5000.00',
'slug' => 'limited-offer',
'payment_mode' => 'limited',
'max_payments' => 50,
'metadata' => ['internal_id' => 'PROD_123'],
'udfs' => [
'udf1' => ['value' => 'INV2026001', 'label' => 'Invoice Number'],
'udf2' => ['value' => 'JohnDoe', 'label' => 'Customer Name'],
],
]);
$data = $response->json();<?php
$curl = curl_init();
$boundary = uniqid();
$delimiter = '-------------' . $boundary;
$postData = '';
$fields = [
'mytpe_pay_id' => 'mytpe_pay_xyz789',
'title' => 'Limited Offer Product',
'details' => 'A special limited edition product.',
'type' => 'dynamic',
'amount' => '5000.00',
'slug' => 'limited-offer',
'payment_mode' => 'limited',
'max_payments' => '50',
'metadata[internal_id]' => 'PROD_123',
'udfs[udf1][value]' => 'INV2026001',
'udfs[udf1][label]' => 'Invoice Number',
'udfs[udf2][value]' => 'JohnDoe',
'udfs[udf2][label]' => 'Customer Name',
];
foreach ($fields as $name => $value) {
$postData .= "--{$delimiter}\r\n";
$postData .= "Content-Disposition: form-data; name=\"{$name}\"\r\n\r\n";
$postData .= "{$value}\r\n";
}
$postData .= "--{$delimiter}\r\n";
$postData .= "Content-Disposition: form-data; name=\"logo\"; filename=\"logo.png\"\r\n";
$postData .= "Content-Type: image/png\r\n\r\n";
$postData .= file_get_contents('/path/to/logo.png') . "\r\n";
$postData .= "--{$delimiter}--\r\n";
curl_setopt_array($curl, [
CURLOPT_URL => 'https://dev.mytpe.app/api/v2/mytpe-pay/links/store',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $postData,
CURLOPT_HTTPHEADER => [
'X-API-KEY: your_api_key',
'X-API-SECRET: your_api_secret',
'Content-Type: multipart/form-data; boundary=' . $delimiter,
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const form = new FormData();
form.append('mytpe_pay_id', 'mytpe_pay_xyz789');
form.append('title', 'Limited Offer Product');
form.append('details', 'A special limited edition product.');
form.append('type', 'dynamic');
form.append('amount', '5000.00');
form.append('slug', 'limited-offer');
form.append('payment_mode', 'limited');
form.append('max_payments', '50');
form.append('metadata[internal_id]', 'PROD_123');
form.append('udfs[udf1][value]', 'INV2026001');
form.append('udfs[udf1][label]', 'Invoice Number');
form.append('udfs[udf2][value]', 'JohnDoe');
form.append('udfs[udf2][label]', 'Customer Name');
form.append('logo', fs.createReadStream('/path/to/logo.png'));
axios.post('https://dev.mytpe.app/api/v2/mytpe-pay/links/store', form, {
headers: {
...form.getHeaders(),
'X-API-KEY': 'your_api_key',
'X-API-SECRET': 'your_api_secret',
},
})
.then(response => console.log(response.data))
.catch(error => console.error(error.response?.data));Example Success Response (201 Created)
201 Created
{
"success": true,
"message": "Pay link created successfully",
"data": {
"mytpe_pay_id": "instance-001",
"title": "Demo Product",
"details": "A detailed description of the demo product.",
"slug": "demo-product",
"type": "dynamic",
"amount": "2500.00",
"status": "active",
"logo": null,
"payment_mode": "reusable",
"max_payments": null,
"successful_payments_count": 0,
"metadata": {
"internal_reference": "REF-123"
},
"udfs": {
"udf1": { "value": "INV2026001", "label": "Invoice Number" },
"udf2": { "value": "JohnDoe", "label": "Customer Name" }
},
"settings": {
"show_retry_button": true,
"allow_anonymous": false
},
"id": "link-demo-123",
"updated_at": "2026-01-17T10:00:00.000Z",
"created_at": "2026-01-17T10:00:00.000Z",
"payment_link": "https://frontdev.mytpe.app/pay/demo-product",
"payment_status": "active",
"remaining_payments": null
}
}Understanding payment_status
Newly created links will have one of these payment_status values:
active: Reusable or limited links ready to accept paymentsawaiting_payment: One-shot links waiting for their first payment
The payment_status field automatically updates based on payment activity and helps you track the payment state of each link.
Error Responses
422 Validation Error
{
"message": "The given data was invalid.",
"errors": {
"slug": ["The slug has already been taken."],
"amount": ["The amount field is required when type is dynamic."]
}
}