Payment links
A payment link creates a transaction and hands you its checkout URL. It is a checkout session with the expiry taken off — same request, same hosted page, same transaction behind it — on a URL that waits as long as your customer does.
Link or checkout session?
Both create one transaction for an amount you set, and both send the payer to the same hosted checkout. Two differences: how long the URL lives, and what it is addressed by.
| Payment link | Checkout session | |
|---|---|---|
| Lifetime | Until it is paid, however long that takes | 24 hours, or until it is paid |
| Addressed by | Its own transaction id. While it is unpaid the page shows the amount and your name to whoever opens it, as an invoice would; once paid it stops disclosing them and says only that it has been paid | A one-time token. The id-addressed page answers 404 for a session, paid or not |
| The URL | Safe to commit to paper: it is still live when the invoice is found in a drawer | Not meant to be written down — it is gone by tomorrow |
| Reach for it when | An invoice you email, a reminder you post, a QR code on a statement — the payer gets to it in their own time | A customer you are redirecting out of your own checkout right now, where a URL that expires is one less thing left lying around |
redirectUrl; if they open the URL again from the
same browser — a bookmark, the Back button, the emailed invoice
next week — they get a page telling them it is already paid,
with your URL offered as a link they can follow rather than one
they are thrown at. That way a revisit does not look like a
fresh payment to whatever you have watching
redirectUrl.
If you need the same amount from several people, create a link each — that is also the only way to tell afterwards who paid which.
Before you start
Every endpoint below needs
Authorization: Bearer <accessToken>. Tokens
last one hour and come from
POST /api/token — see
Authentication. The base URL
is https://app.flik.co.nz.
Flik checks that your organisation can actually take the payment method you ask for, at the moment you create the link. That is deliberate: a link that cannot be paid is worse than no link, because you find out only when someone tries.
-
cardorbothneeds card payments configured on your organisation. -
open_bankingorbothneeds two things: an approved open banking application, and a settlement account on the organisation. The application is what lets Flik ask a bank to move the money; the settlement account is where it lands. Having one without the other is a common state and it fails. -
Open banking is New Zealand dollars only. If your organisation
settles in AUD, only
cardworks.
Create a payment link
This creates a transaction and returns the checkout URL for it. The body is a checkout session's body, field for field — if you have written one of these calls you have written both.
What comes back is different in two ways. A session's URL
dies in 24 hours; this one does not, which is why it can go
in an emailed invoice or a printed reminder and still work a
fortnight later. And a session's URL carries a one-time
token, while a link's is addressed by its own id — the
id in the response — so the portal can show it
again whenever you need to re-send it.
Nothing about it changes after this call — the amount, the reference and the webhook address are fixed for the life of the transaction. That is what makes the URL safe to commit to paper.
Store url when you get it.
Reading the link back over the API gives you its status, not
the URL, so this response is where your integration captures
it. Treat it as returned rather than constructed — the shape
is Flik's to change — and if it is lost, the link's page in
the portal shows it again.
Headers
Bearer <accessToken>. See
Authentication.
Body
single — one payment rather than a recurring
series. Recurring is not implemented on this endpoint, so a
link created as recurring is taken as a single
payment.
10000. Major units —
25.00 is twenty-five dollars, not
twenty-five cents.
NZD or AUD. Your organisation
has a settlement currency and that is what the payment
is taken in. Omit this and Flik fills it in.
If you do send it, it is checked. A currency that does
not match your organisation is rejected with
currency_mismatch rather than silently
repriced — an AUD merchant still sending
NZD out of habit should find out, not
discover it at reconciliation.
' . , @ _ -. Usually your invoice or order
number.
Up to 12 characters, same character set as particulars. The third and last of the New Zealand bank statement fields, shown to your customer alongside the reference and particulars.
Omit it and Flik generates one — the
code in the response, of the form
FLIK-00042. Send your own if you reconcile
against a code your accounting system already issues;
it must be unique to you, and Flik does not check that.
Where Flik sends the customer after a successful
payment. Flik appends
?transactionId=<uuid> to it — the
same id this call returned, so you can
read the link straight back. Query parameters of your
own are preserved.
That id is not proof of payment. Anyone can put an id in a URL. Read the link and render from what comes back.
open_banking, card, or
both. Checked against your organisation as
the link is created — see
Before you start.
curl -X POST https://app.flik.co.nz/api/payment-links \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "single",
"amount": { "total": 250.00 },
"creditorReference": {
"reference": "INV-1001",
"particulars": "Acme Ltd",
"code": "ORD-5591"
},
"checkoutMethod": "both",
"webhookUrl": "https://example.org/flik/webhook",
"foreignTransactionId": "invoice-1001"
}'
const BASE = "https://app.flik.co.nz";
async function createPaymentLink(token, invoice) {
const res = await fetch(`${BASE}/api/payment-links`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "single",
amount: { total: invoice.total },
creditorReference: {
reference: invoice.number,
particulars: "Acme Ltd",
code: invoice.id,
},
checkoutMethod: "both",
webhookUrl: "https://example.org/flik/webhook",
foreignTransactionId: invoice.id,
}),
});
const body = await res.json();
if (!res.ok) {
// Branch on body.error.code, never on the message text.
throw Object.assign(new Error(body.error.message), {
code: body.error.code,
});
}
// Store body.url — it is not returned again.
return body;
}
import requests
BASE = "https://app.flik.co.nz"
def create_payment_link(token, invoice):
res = requests.post(
f"{BASE}/api/payment-links",
headers={"Authorization": f"Bearer {token}"},
json={
"type": "single",
"amount": {"total": invoice["total"]},
"creditorReference": {
"reference": invoice["number"],
"particulars": "Acme Ltd",
"code": invoice["id"],
},
"checkoutMethod": "both",
"webhookUrl": "https://example.org/flik/webhook",
"foreignTransactionId": invoice["id"],
},
timeout=15,
)
body = res.json()
if not res.ok:
# Branch on body["error"]["code"], never on the message text.
raise RuntimeError(body["error"]["code"])
# Store body["url"] — it is not returned again.
return body
<?php
const FLIK_BASE = 'https://app.flik.co.nz';
function flik_create_payment_link(string $token, array $invoice): array
{
$ch = curl_init(FLIK_BASE . '/api/payment-links');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'type' => 'single',
'amount' => ['total' => $invoice['total']],
'creditorReference' => [
'reference' => $invoice['number'],
'particulars' => 'Acme Ltd',
'code' => $invoice['id'],
],
'checkoutMethod' => 'both',
'webhookUrl' => 'https://example.org/flik/webhook',
'foreignTransactionId' => $invoice['id'],
]),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$body = json_decode($response, true);
if ($status !== 200) {
// Branch on $body['error']['code'], never on the message text.
throw new RuntimeException($body['error']['message']);
}
// Store $body['url'] — it is not returned again.
return $body;
}
{
"id": "9f2c1a70-4de8-4b6a-9c31-52a7b0e4d183",
"status": "created",
"url": "https://app.flik.co.nz/checkout/transaction/9f2c1a70-4de8-4b6a-9c31-52a7b0e4d183",
"code": "ORD-5591",
"amount": { "total": 250.00, "currency": "NZD" },
"reference": "INV-1001",
"checkoutMethod": "both",
"foreignTransactionId": "invoice-1001",
"testMode": false,
"createdAt": "2026-09-12T02:14:09Z"
}
Response fields
cs_ id; here
there is no wrapper, so the transaction is the object.
created on a new link. See
Statuses.
FLIK-00042 Flik generated. What your customer
quotes when they call you.
Errors
| Status | Code | When |
|---|---|---|
| 400 |
validation_failed
|
A field is missing, the wrong type, or out of range:
a missing or non-positive amount.total,
more than two decimal places on it, a
creditorReference field over 12
characters, or a redirectUrl that is not
a URL. Identical rules to a checkout session.
|
| 400 |
currency_mismatch
|
amount.currency is not your organisation's
settlement currency. Flik rejects rather than repricing.
|
| 400 |
idempotency_error
|
The Idempotency-Key was used before with a
different body.
|
| 400 |
card_not_enabled
|
You asked for card or both and
card payments are not configured.
|
| 400 |
open_banking_not_enabled
|
You asked for open_banking or
both and the open banking application is
not approved.
|
| 400 |
settlement_account_missing
|
A bank rail was asked for and the organisation has no settlement account. |
| 400 |
open_banking_currency_unsupported
|
Your organisation settles in AUD, which the bank rail
cannot carry. Use card.
|
| 401 |
unauthorized
|
No Authorization header, or not in the form
Bearer <token>.
|
| 401 |
token_expired
|
The token has expired. Fetch a new one and retry, once. |
| 401 |
invalid_token
|
The token could not be verified — usually truncated or double-prefixed. |
| 401 |
unknown_api_key
|
The API key the token was issued for no longer exists. A new token will not help. |
| 500 |
internal_error
|
Flik's end failed. Retry with backoff. |
Retrieve a payment link
This is how you find out whether the link has been paid.
Everything create returned comes back with current values,
plus particulars, transactionType,
foreignTransactionId, createdAt and
updatedAt — with one exception.
There is no url. The API returns it once, at
create, and this read is for the link's status. Capture it
from the create response; the portal shows it again if you
need to re-send it.
status runs created →
pending → completed or
failed, exactly as a session's does. There is no
expired: that is the whole point of a link. See
Statuses.
checkoutMethod is what you offered;
transactionType is the rail the money actually
moved on. For a both link that is how you learn
what your customer chose.
It is only meaningful once status is
completed. Before that it reads
open_banking on every link, whatever you set
checkoutMethod to.
redirectUrl. What does not work on a link is
tight polling: an invoice can sit unpaid for a fortnight,
and a loop that asks every few seconds spends two weeks
learning nothing.
So for a link, pair the read with something that says when to make it — a sweep over your unpaid invoices on whatever cycle your business already runs on, or a
webhookUrl that tells you the moment it lands.
Either way the answer you act on comes from reading the link
back. See Webhooks.
Where the id comes from
A link's id is its transaction id, and both of
the things that tell you a payment happened hand it to you:
the webhook body, as
transactionId, and the redirect back to your
site, as the ?transactionId= Flik appends to your
redirectUrl. Either one can be read straight
back — there is no mapping to keep.
Parameters
id from the create response — the
transaction id, the same value the webhook and the
redirect give you.
Someone else's link is a 404
Ask for a link that belongs to another organisation and Flik
returns 404, not 403. That is on
purpose. A 403 would confirm the id exists, which
would let anyone holding a valid API key walk the id space and
learn which links are real. Flik will not say anything about
an id it will not serve you.
So a 404 means one of two things: the link does
not exist, or it is not yours. Check the id under
Payment Links
in the portal before you assume it was deleted.
curl https://app.flik.co.nz/api/payment-links/7b3e4d21-9a6c-4f08-b512-6de3a9c47f10 \
-H "Authorization: Bearer $TOKEN"
const BASE = "https://app.flik.co.nz";
async function getPaymentLink(token, id) {
const res = await fetch(`${BASE}/api/payment-links/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
// 404 means "no such link, or not yours" — both are the same answer here.
if (res.status === 404) return null;
const body = await res.json();
if (!res.ok) throw new Error(body.error.code);
return body;
}
import requests
BASE = "https://app.flik.co.nz"
def get_payment_link(token, link_id):
res = requests.get(
f"{BASE}/api/payment-links/{link_id}",
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
# 404 means "no such link, or not yours" — same answer either way.
if res.status_code == 404:
return None
body = res.json()
if not res.ok:
raise RuntimeError(body["error"]["code"])
return body
<?php
const FLIK_BASE = 'https://app.flik.co.nz';
function flik_get_payment_link(string $token, string $id): ?array
{
$ch = curl_init(FLIK_BASE . '/api/payment-links/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 404 means "no such link, or not yours".
if ($status === 404) {
return null;
}
return json_decode($response, true);
}
{
"id": "9f2c1a70-4de8-4b6a-9c31-52a7b0e4d183",
"status": "completed",
"code": "ORD-5591",
"amount": { "total": 250.00, "currency": "NZD" },
"reference": "INV-1001",
"particulars": "Acme Ltd",
"checkoutMethod": "both",
"transactionType": "open_banking",
"foreignTransactionId": "invoice-1001",
"testMode": false,
"createdAt": "2026-09-12T02:14:09Z",
"updatedAt": "2026-09-14T21:06:44Z"
}
Errors
| Status | Code | When |
|---|---|---|
| 401 |
unauthorized
|
No Authorization header, or not in the form
Bearer <token>.
|
| 401 |
token_expired
|
The token has expired. Fetch a new one and retry, once. |
| 401 |
invalid_token
|
The token could not be verified — usually truncated or double-prefixed. |
| 401 |
unknown_api_key
|
The API key the token was issued for no longer exists. A new token will not help. |
| 404 |
not_found
|
No such link, or it belongs to another organisation. |
| 500 |
internal_error
|
Flik's end failed. Retry with backoff. |
Statuses
| Status | Meaning | Webhook |
|---|---|---|
created |
The link exists; nobody has paid yet. | — |
pending |
A payment is in flight — awaiting bank approval, or a card charge is processing. | — |
completed |
Paid. The URL stops taking money. | checkout_session.completed |
failed |
The last attempt did not succeed. Not terminal — the link stays live and the customer can open it and try again. There is deliberately no webhook for this; see Events. | — |
The same vocabulary a
checkout session
uses, minus expired — a link has no deadline to
miss. Which leaves exactly one webhook a link can send,
checkout_session.completed, on one shared event
stream: a receiver written for sessions already handles links.
Next steps
- Webhooks — a link can be paid at any time by anyone, so a webhook is the only way to learn about a payment without polling.
- Errors — particularly the eligibility codes, which mean something needs configuring in the portal.
- Checkout sessions — for when you know the amount.