Checkout sessions
A checkout session is one payment, by one customer, for an amount you set. Flik gives you a URL to send them to and tells you when they have paid.
A session's URL expires in 24 hours, which is right for a customer paying now and wrong for an invoice that sits in someone's inbox for a fortnight. For that, use a payment link — the same call and the same kind of URL, without the deadline.
Create a checkout session
Headers
Bearer <accessToken>. See
Authentication.
Body
single — one payment rather than a recurring
series. Recurring is not implemented on this endpoint, so a
session 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>&checkoutSessionId=cs_…
to it, so your handler knows which session the customer
is asking about. Query parameters of your own are
preserved.
That id is not proof of payment. Anyone can put an id in a URL. Read the session and render from what comes back — paid, still pending, or failed.
curl -X POST https://app.flik.co.nz/api/checkout-sessions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-5591" \
-d '{
"type": "single",
"amount": { "total": 25.00 },
"creditorReference": {
"reference": "INV-1001",
"particulars": "Acme Ltd",
"code": "ORD-5591"
},
"checkoutMethod": "both",
"redirectUrl": "https://example.com/thanks",
"webhookUrl": "https://example.com/flik/webhook",
"foreignTransactionId": "order-5591"
}'
const res = await fetch(
"https://app.flik.co.nz/api/checkout-sessions",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Idempotency-Key": order.id,
},
body: JSON.stringify({
type: "single",
amount: { total: order.total },
creditorReference: {
reference: order.invoiceNumber,
particulars: "Acme Ltd",
code: order.id,
},
checkoutMethod: "both",
redirectUrl: "https://example.com/thanks",
webhookUrl: "https://example.com/flik/webhook",
foreignTransactionId: order.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,
param: body.error.param,
});
}
// Store body.id against the order, then redirect to body.url
res = requests.post(
"https://app.flik.co.nz/api/checkout-sessions",
headers={
"Authorization": f"Bearer {token}",
"Idempotency-Key": order["id"],
},
json={
"type": "single",
"amount": {"total": order["total"]},
"creditorReference": {
"reference": order["invoice_number"],
"particulars": "Acme Ltd",
"code": order["id"],
},
"checkoutMethod": "both",
"redirectUrl": "https://example.com/thanks",
"webhookUrl": "https://example.com/flik/webhook",
"foreignTransactionId": order["id"],
},
timeout=15,
)
body = res.json()
if not res.ok:
# Branch on body["error"]["code"], never on the message text.
raise FlikError(body["error"])
# Store body["id"] against the order, then redirect to body["url"]
<?php
$ch = curl_init('https://app.flik.co.nz/api/checkout-sessions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json',
'Idempotency-Key: ' . $order['id'],
],
CURLOPT_POSTFIELDS => json_encode([
'type' => 'single',
'amount' => ['total' => $order['total']],
'creditorReference' => [
'reference' => $order['invoice_number'],
'particulars' => 'Acme Ltd',
'code' => $order['id'],
],
'checkoutMethod' => 'both',
'redirectUrl' => 'https://example.com/thanks',
'webhookUrl' => 'https://example.com/flik/webhook',
'foreignTransactionId' => $order['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['id'] against the order, then redirect to $body['url']
{
"id": "cs_7Hk2pQr4TnVxWy9BdLmE",
"status": "created",
"url": "https://app.flik.co.nz/checkout/s/uZ8kT2vQ...",
"code": "ORD-5591",
"amount": { "total": 25.00, "currency": "NZD" },
"checkoutMethod": "both",
"reference": "INV-1001",
"testMode": false,
"expiresAt": "2026-09-13T04:11:00Z"
}
{
"error": {
"code": "card_not_enabled",
"message": "Card payments are not configured for this organisation",
"param": "checkoutMethod",
"doc_url": "https://flik.co.nz/docs/errors/#card_not_enabled"
}
}
Response fields
cs_. Store it
against your order — it is how you read the session back,
and what Flik support will ask for.
created on a new session. 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. details lists every one. |
| 400 | currency_mismatch |
amount.currency is not your settlement currency. |
| 400 | card_not_enabled |
card or both without card payments configured. |
| 400 | open_banking_not_enabled |
The bank rail without an approved open banking application. |
| 400 | settlement_account_missing |
The bank rail with no settlement account on the organisation. |
| 400 | open_banking_currency_unsupported |
The bank rail for an organisation that does not settle NZD. |
| 400 | idempotency_error |
The Idempotency-Key was already used with a different body. |
| 401 |
unauthorized,
token_expired,
invalid_token,
unknown_api_key
|
Missing, expired, unverifiable, or orphaned credentials. |
| 500 | internal_error |
Flik-side failure. Retry with backoff and an Idempotency-Key. |
Nothing is created when any of these is returned — an error means no session and no transaction, so a corrected retry is always safe.
Retrieve a checkout session
Read a session back to find out whether it has been paid.
Where the id comes from
{id} is the cs_ id. You never have
to store it against your order to use it, because both of the
things that tell you a payment happened carry it:
-
the webhook body, as
checkoutSessionId -
the redirect back to your site, as the
?checkoutSessionId=Flik appends to yourredirectUrl
So a webhook handler and a redirect handler can each read the session straight from what they were just given.
curl https://app.flik.co.nz/api/checkout-sessions/cs_7Hk2pQr4TnVxWy9BdLmE \
-H "Authorization: Bearer $TOKEN"
const res = await fetch(
`https://app.flik.co.nz/api/checkout-sessions/${sessionId}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
// 404 covers both "no such session" and "not yours".
if (res.status === 404) return null;
if (!res.ok) throw new Error((await res.json()).error.message);
const session = await res.json();
if (session.status === "completed") {
await fulfil(session.foreignTransactionId);
}
res = requests.get(
f"https://app.flik.co.nz/api/checkout-sessions/{session_id}",
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
# 404 covers both "no such session" and "not yours".
if res.status_code == 404:
return None
if not res.ok:
raise FlikError(res.json()["error"])
session = res.json()
if session["status"] == "completed":
fulfil(session["foreignTransactionId"])
<?php
$ch = curl_init('https://app.flik.co.nz/api/checkout-sessions/' . $sessionId);
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 covers both "no such session" and "not yours".
if ($status === 404) {
return null;
}
$session = json_decode($response, true);
if ($session['status'] === 'completed') {
fulfil($session['foreignTransactionId']);
}
{
"id": "cs_7Hk2pQr4TnVxWy9BdLmE",
"status": "completed",
"code": "ORD-5591",
"amount": { "total": 25.00, "currency": "NZD" },
"reference": "INV-1001",
"particulars": "Acme Ltd",
"checkoutMethod": "both",
"transactionType": "card",
"testMode": false,
"foreignTransactionId": "order-5591",
"expiresAt": "2026-09-13T04:11:00Z",
"createdAt": "2026-09-12T04:11:00Z",
"updatedAt": "2026-09-12T04:13:22Z"
}
{
"error": {
"code": "not_found",
"message": "Checkout session not found.",
"param": null,
"doc_url": "https://flik.co.nz/docs/errors/#not_found"
}
}
Response fields
Everything create returns except url, plus
particulars, transactionType,
foreignTransactionId, createdAt and
updatedAt.
The payer URL is not returned again. Flik stores only a hash of the token inside it, so there is nothing to rebuild it from — and a read that handed back a working payment URL to anyone with an API key would be a second way in. Capture it at create.
checkoutMethod is what you offered;
transactionType is the rail the money actually
moved on — open_banking or card.
For a both session that is how you learn what
your customer chose.
Read it only once status is
completed. Until a payment settles,
transactionType reads
open_banking on every session, including one
you created as card — it records the rail
that was used, and before anyone pays there is not one.
Errors
| Status | Code | When |
|---|---|---|
| 401 |
unauthorized,
token_expired,
invalid_token,
unknown_api_key
|
Missing, expired, unverifiable, or orphaned credentials. |
| 404 | not_found |
No such session — or it exists and belongs to another organisation. The two are indistinguishable on purpose. |
| 500 | internal_error |
Flik-side failure. Safe to retry; this call changes nothing. |
The checkout URL
url carries a high-entropy token rather than the
transaction id. Three consequences worth designing around:
- It expires after 24 hours. If you email a payment request and the customer opens it two days later, they get an expired page. Create a fresh session rather than storing a URL for later.
-
It stops disclosing details once paid. After a
successful payment the URL redirects to your
redirectUrlinstead of showing the amount and reference again. -
It is opaque. Do not build it yourself and do
not parse an id out of it. Use
idfrom the response.
The reason is that checkout URLs leak — into browser history,
Referer headers, forwarded emails, screenshots. An
expiring token limits what a leaked URL is worth.
Choosing payment methods
checkoutMethod |
Customer sees | Your account needs |
|---|---|---|
open_banking |
A list of banks; approves in their banking app | An approved open banking application and a settlement account |
card |
Card entry on the Flik checkout | Card payments configured |
both |
Chooses bank or card | Both of the above |
If your account is not set up for what you ask for, the request fails at creation with a 400. That is deliberate: better to fail when you create the session than to send a customer to a page with no way to pay. Those are fixed in the portal, not in your code.
open_banking and
both are rejected with
open_banking_currency_unsupported. Use
card.
Idempotency
Creating a payment is not something you want to do twice because a
connection dropped. Send an Idempotency-Key header
with a value unique to the operation — your order id is usually
the right choice.
| Situation | What happens |
|---|---|
| Key not seen before | Request runs normally and the response is recorded. |
| Key seen, same body |
The original response is replayed. No second session is
created. The reply carries
Idempotent-Replayed: true.
|
| Key seen, different body |
400 idempotency_error. Reusing one key for two
different payments is a bug, so Flik refuses rather than
guessing.
|
Keys are remembered for 24 hours and are scoped
to your organisation, so your order-1 can never
collide with another merchant's. Only successful responses are
replayed — if a request failed validation, fix it and retry with
the same key.
Statuses
| Status | Meaning | Webhook |
|---|---|---|
created |
Session exists; nobody has paid yet. | — |
pending |
A payment is in flight — awaiting bank approval, or a card charge is processing. | — |
completed |
Paid. Fulfil the order. | checkout_session.completed |
failed |
The last attempt did not succeed. Not terminal — the session is live and the customer can try again, which most do within seconds. There is deliberately no webhook for this; see Events. | — |
expired |
24 hours passed without payment. Create a new session. Expiry is something you read, not something Flik sends. | — |
Treat this list as open — handle an unrecognised status by not fulfilling, rather than by erroring.