Implementation guides

Payment links

Qualy has two kinds of payment link. Pick by whether you already know who's paying.

You want…UseLike
A link for one student and one billThe payment's own link (links.short)An invoice link
One link you can post anywhere, that anyone can payA public service with a price templateStripe Payment Links, a "pay here" button

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.


A reusable link is not its own object. It's three things working together:

  1. A service — what's being sold (an application fee, a deposit, a short course).
  2. A channel flag on the service — displayEcommerce: true makes it public, so anyone with the link can buy it. displayPortal: true shows it only to contacts signed in to your portal.
  3. 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

  1. The visitor enters their name, email and phone (plus any custom fields you've set up).
  2. 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".
  3. 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' }],
}),

Set the service's status to archived (or set displayEcommerce: false) with POST /v1/services/{serviceId}/update. Payments already made aren't affected.

Previous
Collecting payments