Implementation guides
Payment links
Qualy has two kinds of payment link. Pick by whether you already know who's paying.
| You want… | Use | Like |
|---|---|---|
| A link for one student and one bill | The payment's own link (links.short) | An invoice link |
| One link you can post anywhere, that anyone can pay | A public service with a price template | Stripe Payment Links, a "pay here" button |
One-off links
Every payment intent comes with a link. Create the payment and send links.short:
const res = await fetch('https://api.qualyhq.com/v1/payment-intents/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
body: JSON.stringify({
intentType: 'ad-hoc',
contact: 'john.doe@example.com',
currency: 'AUD',
items: [{ name: 'Tuition fee', amount: 135000 }],
}),
});
const { data: payment } = await res.json();
console.log(payment.links.short);
See Collecting payments for what the student sees.
Reusable links
A reusable link is not its own object. It's three things working together:
- A service — what's being sold (an application fee, a deposit, a short course).
- A channel flag on the service —
displayEcommerce: truemakes it public, so anyone with the link can buy it.displayPortal: trueshows it only to contacts signed in to your portal. - A template — the price. It belongs to the service and must carry the same channel flag.
The link is your portal address plus the service:
https://{subdomain}.qualyhqportal.com/services/{serviceId}?action=buy-now
{subdomain} is your account's nickname (or your tenant id). With ?action=buy-now the visitor goes straight to checkout; without it they see the service page first.
Both flags, both places
A service that isn't public produces a link that leads nowhere for someone who isn't signed in, and a template without the matching flag isn't offered on that channel. Set displayEcommerce (or displayPortal) on the service and on its template.
Step 1: create the service
const res = await fetch('https://api.qualyhq.com/v1/services/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
body: JSON.stringify({
name: 'Application fee',
displayEcommerce: true, // anyone with the link
displayPortal: false, // set true to also list it in the signed-in portal
}),
});
const { data: service } = await res.json();
If the money belongs to a school or another partner, set supplier on the service (a partnership id, code or name). Payments made through the link are then routed to that supplier.
Step 2: give it a price
A fixed price is a payment-plan-list template with one payment and one item:
await fetch('https://api.qualyhq.com/v1/templates/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'ApiKey your-api-key-here',
'X-TENANT-ID': 'your-tenant-id-here',
},
body: JSON.stringify({
name: 'Application fee',
category: 'payment-plan-list',
services: [service._id],
displayEcommerce: true,
displayPortal: false,
payments: [
{
currency: 'AUD',
items: [{ name: 'Application fee', amount: 25000 }],
},
],
}),
});
For pay-what-you-owe links (deposits, top-ups) use category: 'payment-ad-hoc-variable-amount' and leave amount off the item. The payer types the amount at checkout.
For a payment plan (a deposit now and the rest later), add more entries to payments, one per instalment. Templates can also carry splits, so a link can pay a school and keep your commission automatically. See Creating payment splits.
Step 3: share it
const link = `https://yourcompany.qualyhqportal.com/services/${service._id}?action=buy-now`;
What happens when someone pays
- The visitor enters their name, email and phone (plus any custom fields you've set up).
- Qualy creates a contact, an order with the service as its item, and a payment intent from the template. Visitors get
intentType: "ecommerce"; signed-in portal contacts get"portal". - The visitor is taken to the payment and pays the usual way.
Your integration sees the usual contacts.create, paymentIntents.create and, once paid, paymentIntents.update webhooks, so link payments need no special handling.
Using a template from the API
You can also create a payment straight from a service and its template, without the checkout:
body: JSON.stringify({
contact: 'john.doe@example.com',
services: [{ service: '6634f56c7dbbc16e07b5025e', template: '6634f5a17dbbc16e07b50261' }],
}),
Turning a link off
Set the service's status to archived (or set displayEcommerce: false) with POST /v1/services/{serviceId}/update. Payments already made aren't affected.