Skip to content
Docs / Errors

Errors

Every failure returns the same shape, with a stable machine code you can branch on and a message meant for a human reading a log.

The error shape

400 Bad Request
{
  "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"
  }
}
code string
Stable identifier. Branch on this. Messages get reworded; codes do not.
message string
Human-readable explanation. Safe to log. Do not parse, do not show verbatim to your customer — most of these describe your integration, not their payment.
param string | null
Which field caused it, where one field is responsible. Dotted for nested fields, e.g. amount.total. The key is always present; it is null when no single field is to blame.
doc_url string
Link to the section on this page describing the code.
details array Optional
Present on validation_failed only — one entry per field that failed.
Branch on the code if (error.code === "token_expired"), never if (error.message.includes("expired")). Message wording is not part of this contract.

One exception: POST /api/token predates this shape. On a 400 or 401 it answers { "error": "<message>" } — a string, not an object — so branch on the status there. See Authentication.

HTTP status codes

Status Meaning Retry?
200 Success. —
400 Your request, or your account configuration, needs changing. Not without changing something
401 Authentication failed. Only token_expired
404 No such resource — or it exists but is not yours. No
500 Something went wrong at Flik's end. Yes, with backoff

Validation

validation_failed

400. One or more fields are missing, the wrong type, or out of range. details lists every problem, so one round trip tells you everything rather than making you fix them one at a time.

400 Bad Request
{
  "error": {
    "code": "validation_failed",
    "message": "The request body failed validation.",
    "param": "amount.total",
    "doc_url": "https://flik.co.nz/docs/errors/#validation_failed",
    "details": [
      {
        "field": "amount.total",
        "constraints": { "max": "total must not be greater than 10000" }
      },
      {
        "field": "creditorReference.reference",
        "constraints": {
          "matches": "[0-9A-Za-z '.,@_-] are allowed. Maximum 12 characters."
        }
      }
    ]
  }
}

The most common causes: a total over 10000 or with more than two decimal places, a reference longer than 12 characters or containing punctuation the banks will not carry, and a redirectUrl that is not a valid absolute URL.

Account configuration

These four mean the request was well-formed but your Flik account cannot do what you asked. The fix is in the portal, not in your code. Retrying will not help.

card_not_enabled

400. You asked for card or both, but card payments are not configured for your organisation. Set card payments up in the portal, or use open_banking.

open_banking_not_enabled

400. You asked for open_banking or both, but your open-banking application has not been approved. Approval is a Flik-side process — if it is outstanding, talk to us.

settlement_account_missing

400. Bank payments settle into your nominated account and your organisation does not have one recorded. Add a bank account in organisation settings.

open_banking_currency_unsupported

400. Your organisation settles in a currency the bank rail cannot carry. Open banking is New Zealand dollars only. If you settle in AUD, use card.

This is structural rather than a configuration gap — there is nothing to switch on. It is checked before the other two so you get the real reason rather than a misleading one.

Currency

currency_mismatch

400. You sent an amount.currency that is not your organisation's settlement currency.

Flik rejects rather than converting or silently repricing. An AUD merchant whose integration still sends NZD out of habit would otherwise have every amount charged in AUD and reconciled against a number they believed was NZD. A 400 turns that into a one-line fix.

The simplest remedy is to stop sending amount.currency at all — it is optional, and Flik fills it in.

Idempotency

idempotency_error

400. You reused an Idempotency-Key with a different request body. Keys are remembered for 24 hours.

Flik refuses rather than guessing which request you meant. Either you meant to retry — in which case send the identical body — or this is a new payment and needs its own key. Deriving the key from your order id avoids the problem entirely.

Authentication

unauthorized

401. No Authorization header, or it is not in the form Bearer <token>.

token_expired

401. The token was valid but has passed its expiry. The one error worth retrying automatically — fetch a new token and repeat the request, once.

invalid_token

401. The token could not be verified. Almost always a truncated, mangled or double-prefixed header rather than anything sinister.

unknown_api_key

401. The token is well-formed but the API key it was issued for no longer exists — it was removed after the token was issued. Getting a new token will not help; you need working credentials.

Not found

not_found

404. One of three things:

  • The id does not exist.
  • It exists but belongs to another organisation. Flik answers 404 rather than 403 deliberately — a 403 would confirm that an id you are not entitled to see is real.
  • You called GET /api/transaction/{id} for a checkout session. That endpoint is not part of this API; use GET /api/checkout-sessions/{id}. For a payment link the same endpoint answers while the link is still payable and 404s once it settles — either way, do not build on it: read the link back with GET /api/payment-links/{id}.

Server errors

internal_error

500. Something failed at Flik's end. Retry with exponential backoff.

Send an Idempotency-Key so those retries are safe. Without one, a 500 is genuinely ambiguous — the session may or may not have been created before the failure, and retrying may produce a second payment. With one, a retry either completes the original request or replays its result.

If a 500 persists for more than a few minutes, contact support with the timestamp and, if you have it, the session id.

Handling errors well

  1. Switch on error.code. Never match on message text.
  2. Retry only token_expired and 500s. Everything else needs a change first, so retrying just burns requests.
  3. Log code, param and message together. The three of them usually make the cause obvious without a reproduction.
  4. Treat account-configuration errors as alerts, not customer-facing failures. They mean something needs doing in the portal, and they will affect every payment until it is.
  5. Handle unknown codes. New codes get added. Fall back to "payment could not be started" rather than crashing.