Developers › Get paid without a website
Payment links
Verified 6 November 2026Explain this to your team →
A payment link is a page we host, at a URL you can print, message or turn into a QR code. It charges one of your prices. Everyone who opens it gets their own payment — two people scanning the same code on your counter produce two payments, and both can pay.
There is nothing to build. No keys on your side, no page to write, no webhook you have to have working before you can take the first payment.
plink_ id is 24 random characters and it is the only thing guarding the page, exactly like a checkout session. That is by design — the payer has no account and no key — but it does mean anyone with the URL can open it. A link does not expire; turn it off when you are done with it.Create one
curl https://api.secureprocessing.app/v1/payment_links \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"price": "price_3cD5eF7gH9jK1lM3nP5qR7sT",
"quantity": 1,
"after_completion": { "type": "redirect", "url": "https://example.bz/thanks" }
}'const link = await sp.paymentLinks.create({
price: price.id,
adjustable_quantity: { enabled: true, minimum: 1, maximum: 6 },
collect_customer_name: true,
custom_fields: [{ key: "boat_name", label: "Boat name", type: "text" }],
});
console.log(link.url);
// https://checkout.secureprocessing.app/pay/plink_7nQ9rS1tU3vW5xY7zA9bC1dEShare link.url. That is the whole integration.
What the customer sees
Your business name, the product, its first image and description, and the amount. Then their email — always, so we can send a receipt — plus a name, a phone number and up to three questions of your own if you asked for them. Pressing Pay by card takes them to a secure page hosted by the bank, so the card number never reaches your servers or ours.
Afterwards they land on our confirmation page, with an optional message from you, or we send them to a page of yours with ?session_id= appended. A redirect has to be https: the payer arrives there with a session id in the URL.
The answers to your questions arrive on each payment’s metadata, alongside payment_link — so a fulfilment job reading the payment has everything it needs without a second call. Metadata values are strings, always, so the answers come as one JSON string under payment_link_fields:
const answers = JSON.parse(intent.metadata.payment_link_fields ?? "{}");
answers.boat_name; // "Kraken"
intent.metadata.payment_link; // "plink_7nQ9rS1tU3vW5xY7zA9bC1dE"
intent.metadata.payment_link_name; // present when you collected a name
intent.metadata.payment_link_phone; // present when you collected a phone numberThe amount comes from the price
The one exception is a price with a custom_unit_amount — a donation, a deposit, a tip. There the payer names a figure, and it is checked against the minimum and maximum you published before it is used. Every amount here is an integer in minor units: 6000 is $60.00.
Likewise the quantity. A link with adjustable_quantity: { enabled: true, minimum: 1, maximum: 6 } shows a picker; one without it charges for exactly what it says, and a request asking for anything else is refused rather than quietly obeyed. Both bounds are required when the picker is on, because the quantity multiplies the amount.
A link’s price cannot be changed
active, the after-completion behaviour, the questions, the quantity picker, whether to collect a name or a phone number, and the metadata can all be updated. The price and the quantity cannot, and an attempt is refused with an explanation rather than ignored.
The reason is the same one that makes a price immutable, only sharper: the URL is already on a poster, in a QR code on a counter and in messages nobody can recall. The people who will pay it have been told what it costs, in a medium we cannot reach. To charge something else, create a new link and turn the old one off.
await sp.paymentLinks.update(link.id, { active: false });Turning a link off is not deleting it. The page starts answering 404 — naming nothing about your business — but payments already taken stay attributed to it, and a customer who is on the bank’s page at that moment can still finish.
What a link has taken
A link has no counter, because a counter is a second number to disagree with the first. Its payments are simply the payments that carry its id:
curl "https://api.secureprocessing.app/v1/payment_intents?payment_link=plink_7nQ9rS1tU3vW5xY7zA9bC1dE" \
-H "Authorization: Bearer sk_test_..."const paid = await sp.paymentIntents.list({
payment_link: link.id,
status: "succeeded",
});?created_via=payment_link is the broader filter: every payment that came from any link, as opposed to api, checkout, dashboard or invoice.
Events
| Event | When |
|---|---|
| payment_link.created | A link was created. Its URL is live from that moment. |
| payment_link.updated | A link was turned off, or had its fields or metadata changed. Never its price. |
| payment_intent.created | Somebody opened the link and started a payment. One per visitor who submits the form. |
| payment_intent.succeeded | The money is yours. Fulfil here, not on the redirect. |
payment_intent.succeeded, never on the customer arriving at your redirect. A browser that closes on the bank’s page still produces a paid payment, and a customer can open your thank-you URL without having paid at all.