API reference
Every endpoint, with its parameters, its errors and a request you can copy. For the narrative versions see Checkout sessions and Payment links.
Conventions
| Item | Value |
|---|---|
| Base URL | https://app.flik.co.nz |
| Authentication |
Authorization: Bearer <accessToken> on
every endpoint except /api/token
|
| Token lifetime |
Five minutes. Reuse a token until it expires rather than
fetching one per request; expiresIn on the
token response says how long
|
| Request body |
application/json, except
/api/token which is
application/x-www-form-urlencoded
|
| Response body | application/json |
| Success status |
200 on every endpoint here, creates included
|
| Timestamps | ISO 8601, UTC |
| Amounts |
Major units as a number — 25.00 is twenty-five
dollars
|
| Webhooks |
webhookUrl on the session or link you create.
There is no account-wide endpoint. See
Webhooks
|
| Test vs live | Determined by the API key, not the URL. Same host for both |
Errors always take the shape
{ "error": { code, message, param, doc_url } }.
Branch on code, never on message. See
Errors for the full catalogue.
Create an access token
Exchanges an API key pair for a bearer token valid for five minutes. The body is form-encoded, not JSON — this is the one endpoint where that is true.
Cache the token for its hour. Fetching a fresh one per request works but doubles your round trips for no benefit. Full detail on Authentication.
Parameters
curl -X POST https://app.flik.co.nz/api/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "clientId=$FLIK_CLIENT_ID" \
-d "clientSecret=$FLIK_CLIENT_SECRET"
const BASE = "https://app.flik.co.nz";
async function getToken() {
const res = await fetch(`${BASE}/api/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
clientId: process.env.FLIK_CLIENT_ID,
clientSecret: process.env.FLIK_CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
const { accessToken } = await res.json();
return accessToken; // good for an hour
}
import os
import requests
BASE = "https://app.flik.co.nz"
def get_token():
res = requests.post(
f"{BASE}/api/token",
data={
"clientId": os.environ["FLIK_CLIENT_ID"],
"clientSecret": os.environ["FLIK_CLIENT_SECRET"],
},
timeout=15,
)
res.raise_for_status()
return res.json()["accessToken"] # good for an hour
<?php
const FLIK_BASE = 'https://app.flik.co.nz';
function flik_token(): string
{
$ch = curl_init(FLIK_BASE . '/api/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'clientId' => getenv('FLIK_CLIENT_ID'),
'clientSecret' => getenv('FLIK_CLIENT_SECRET'),
]),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException('Token request failed');
}
return json_decode($response, true)['accessToken'];
}
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": "1h"
}
Errors
This endpoint predates the error envelope: on a failure
error is a plain string, not an object with a
code. Branch on the HTTP status here.
| Status | Code | When |
|---|---|---|
| 400 |
"Invalid credentials - missing or malformed clientId/clientSecret"
|
clientId or clientSecret is
missing, or the body was sent as JSON.
|
| 401 |
"Invalid credentials"
|
The pair does not match a key. An unknown client ID and a wrong secret get the same answer, on purpose. |
| 500 |
internal_error
|
Flik's end failed. Retry with backoff. |
Create a checkout session
One payment, by one customer, for an amount you set. The response carries the URL to send them to. Narrative version: Checkout sessions.
Parameters
single.10000.
NZD or AUD. Must match your
organisation's settlement currency. Omit it and Flik fills
it in — that is the safer choice.
[0-9A-Za-z '.,@_-]. Usually your
invoice or order number.
[a-zA-Z0-9- ]. Typically your
trading name.
[a-zA-Z0-9- ]. Generated if
omitted, and returned as code.
?transactionId=<uuid>&checkoutSessionId=cs_…,
preserving any query parameters of your own. An id in a URL is not
proof of payment — read it back before you act on it.
open_banking, card, or
both. Checked against your organisation at
creation.
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": "ACME"
},
"checkoutMethod": "both",
"redirectUrl": "https://example.com/thanks",
"webhookUrl": "https://example.com/flik/webhook",
"foreignTransactionId": "order-5591",
"description": "Order 5591, two items"
}'
const BASE = "https://app.flik.co.nz";
async function createSession(token, order) {
const res = await fetch(`${BASE}/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: "ACME",
},
checkoutMethod: "both",
redirectUrl: "https://example.com/thanks",
webhookUrl: "https://example.com/flik/webhook",
foreignTransactionId: order.id,
}),
});
const body = await res.json();
if (!res.ok) {
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.
return body;
}
import requests
BASE = "https://app.flik.co.nz"
def create_session(token, order):
res = requests.post(
f"{BASE}/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": "ACME",
},
"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:
raise RuntimeError(body["error"]["code"])
# Store body["id"] against the order, then redirect to body["url"].
return body
<?php
const FLIK_BASE = 'https://app.flik.co.nz';
function flik_create_session(string $token, array $order): array
{
$ch = curl_init(FLIK_BASE . '/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' => 'ACME',
],
'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) {
throw new RuntimeException($body['error']['code']);
}
// Store $body['id'] against the order, then redirect to $body['url'].
return $body;
}
{
"id": "cs_7Hk2pQr4TnVxWy9BdLmE",
"status": "created",
"url": "https://app.flik.co.nz/checkout/s/uZ8kT2vQ...",
"code": "ACME",
"amount": { "total": 25.00, "currency": "NZD" },
"checkoutMethod": "both",
"reference": "INV-1001",
"testMode": false,
"expiresAt": "2026-09-13T04:11:00Z"
}
Errors
| Status | Code | When |
|---|---|---|
| 400 |
validation_failed
|
A field is missing, the wrong type, or out of range — an
amount over 10000, a reference with an
illegal character, a missing
redirectUrl.
|
| 400 |
currency_mismatch
|
amount.currency is not your organisation's
settlement currency. Flik rejects rather than repricing.
|
| 400 |
card_not_enabled
|
card or both asked for and
card payments are not configured.
|
| 400 |
open_banking_not_enabled
|
open_banking or both asked for
and the 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.
|
| 400 |
idempotency_error
|
The Idempotency-Key was used before with a
different body.
|
| 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. |
| 500 |
internal_error
|
Flik's end failed. Retry with backoff, and with the same
Idempotency-Key so the retry is safe.
|
Retrieve a checkout session
Read a session back to find out whether it has been paid. This is the supported way to confirm a payment server-side, and the fallback if your webhook endpoint was down.
A session belonging to another organisation returns 404, not 403. Flik will not confirm that an id it is unwilling to serve you exists at all.
Parameters
cs_ id. Both signals carry it, so you
need no stored mapping: the webhook body as
checkoutSessionId, and the redirect as
?checkoutSessionId= on your
redirectUrl.
Statuses
| Status | Meaning |
|---|---|
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. |
failed |
Attempted and did not succeed. The payer can try again while the session is live. |
expired |
24 hours passed without payment. Create a new session. |
Treat this list as open — handle an unrecognised status by not
fulfilling, rather than by erroring.
transactionType tells you which rail the money
actually moved on, which for a both session is
the only way to know what the payer chose. It is meaningful
only once status is completed;
before that it reads open_banking regardless of
checkoutMethod.
curl https://app.flik.co.nz/api/checkout-sessions/cs_7Hk2pQr4TnVxWy9BdLmE \
-H "Authorization: Bearer $TOKEN"
const BASE = "https://app.flik.co.nz";
async function getSession(token, sessionId) {
const res = await fetch(`${BASE}/api/checkout-sessions/${sessionId}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.status === 404) return null;
const body = await res.json();
if (!res.ok) throw new Error(body.error.code);
// Fulfil only on "completed".
return body;
}
import requests
BASE = "https://app.flik.co.nz"
def get_session(token, session_id):
res = requests.get(
f"{BASE}/api/checkout-sessions/{session_id}",
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
if res.status_code == 404:
return None
body = res.json()
if not res.ok:
raise RuntimeError(body["error"]["code"])
# Fulfil only on "completed".
return body
<?php
const FLIK_BASE = 'https://app.flik.co.nz';
function flik_get_session(string $token, string $sessionId): ?array
{
$ch = curl_init(FLIK_BASE . '/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);
if ($status === 404) {
return null;
}
// Fulfil only on "completed".
return json_decode($response, true);
}
{
"id": "cs_7Hk2pQr4TnVxWy9BdLmE",
"status": "completed",
"code": "ACME",
"amount": { "total": 25.00, "currency": "NZD" },
"checkoutMethod": "both",
"reference": "INV-1001",
"particulars": "Acme Ltd",
"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"
}
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. |
| 404 |
not_found
|
No such session, it belongs to another organisation, or
you used the old
GET /api/transaction/{id} path.
|
| 500 |
internal_error
|
Flik's end failed. Retry with backoff. |
Create a payment link
Creates a transaction and returns its checkout URL. A
checkout session with the expiry taken off: same body field
for field, no deadline — so it survives being emailed or
printed. The URL is addressed by the link's own id rather
than a one-time token, and url is returned here
and not on the read. Narrative version:
Payment links.
Parameters
single.10000.
NZD or AUD. Must match your
organisation's settlement currency. Omit it and Flik fills
it in — that is the safer choice.
[0-9A-Za-z '.,@_-]. Usually your
invoice or order number.
[a-zA-Z0-9- ]. Typically your
trading name.
[a-zA-Z0-9- ]. Generated if
omitted, and returned as code.
?transactionId=<uuid> — a link's
id — preserving any query parameters of your
own. An id in a URL is not proof of payment — read it
back before you act on it.
open_banking, card, or
both. Checked against your organisation at
creation.
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) {
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) throw new Error(body.error.code);
// Publish body.url. Do not rebuild it from body.id.
return body;
}
import requests
BASE = "https://app.flik.co.nz"
def create_payment_link(token):
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:
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
{
$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) {
throw new RuntimeException($body['error']['code']);
}
// Publish $body['url'].
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"
}
Errors
| Status | Code | When |
|---|---|---|
| 400 |
validation_failed
|
A field is missing, the wrong type, or out of range — 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
|
card or both asked for and
card payments are not configured.
|
| 400 |
open_banking_not_enabled
|
open_banking or both asked for
and the 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. |
| 500 |
internal_error
|
Flik's end failed. Retry with backoff. |
Retrieve a payment link
Whether the link has been paid, and everything it was created
with, except url, which is returned at create and not
here.
status runs created →
pending → completed or
failed, the same vocabulary a checkout session
uses, minus expired — a link has no deadline to
miss.
A link belonging to another organisation returns 404, not 403 — a 403 would confirm the id exists to anyone holding an API key, which would make the id space walkable.
Do not poll this to learn when an invoice is paid; that can be
a fortnight away. Set webhookUrl and read to
confirm. See Webhooks.
Parameters
?transactionId= on your
redirectUrl carry. A link has no
checkoutSessionId.
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".
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".
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. |
| 404 |
not_found
|
No such link, or it belongs to another organisation. |
| 500 |
internal_error
|
Flik's end failed. Retry with backoff. |