Tillify LogoTillify
Sign inGet started
Back to Guides

Tillify Connect for till and platform providers

Connect lets your customers set up Tillify from inside your product, and hands you the API credentials to take payments for them. On a till, show a checkout link as a QR code; online, send the customer to it. Customers pay by bank or card on their phone, and the money goes straight to the business's bank account.

How it works

  1. You send your user to Tillify.
  2. They sign in, and pick or create their business and bank account.
  3. We send them back to you with a one-time Setup token.
  4. Your server exchanges it for the business's API credentials.
  5. You create payments with those credentials.

What you'll receive

  • A partner key (pk_…): public, it goes in Connect links.
  • A partner secret (ps_…): stays on your servers and authenticates the exchange.
  • Your registered redirect URIs, which must match exactly. http://localhost URIs can be registered for development.

Go-live checklist

  1. 1

    Get partner access. Contact us with your company name and the exact redirect URIs you'll use. We send you a partner key and partner secret.

  2. 2

    Add a “Connect Tillify” button that sends your user to the start link (step 1).

  3. 3

    Handle the callback and check state before anything else (step 2).

  4. 4

    Exchange the Setup token from your server within 5 minutes and store the client secret encrypted (step 3).

  5. 5

    Wait for payments to be enabled before you offer Tillify at checkout.

  6. 6

    Create payments and show checkout_url as a QR code with the total (step 4).

  7. 7

    Confirm payments by verifying our webhook, or by polling the payment.

  8. 8

    Handle disconnects: a 401 means the business revoked your access. Ask them to connect again.

  9. 9

    Test with small real payments into your own test business.

  10. 10

    Point your customers at the merchant setup guide, which walks them through Connect from their side.

1

Send the user to Tillify

Redirect the user's browser to:

GET https://www.tillify.nz/api/v1/connect/start
  ?partner_key=pk_…
  &redirect_uri=https://your-app.example/tillify/callback
  &state=<random value you store against the user's session>

All three parameters are required, and state can be up to 500 characters. redirect_uri must exactly match one you registered. If the link is invalid, we respond 400 with a JSON error and don't redirect.

The user signs in or creates a Tillify account, then confirms their business, or creates one and adds the bank account payments go to. Unless their Stripe account is already connected, Tillify then offers to connect Stripe so customers can also pay by card; the user can skip this. The link is valid for 60 minutes, including any time spent setting up Stripe, and can only be completed by the first person who opens it.

2

Handle the callback

We send the user back to your redirect_uri:

OutcomeQuery parameters
Connectedsetup_token=tok_…&state=…
User cancellederror=access_denied&state=…

Always check that state matches the one you stored for this user before using the setup_token. That's what stops someone completing Connect for their own business and attaching it to your user.

3

Exchange the Setup token (server to server)

Within 5 minutes:

POST https://www.tillify.nz/api/v1/connect/exchange
X-Partner-Key: pk_…
Authorization: Bearer ps_…
Content-Type: application/json

{
  "setup_token": "tok_…",
  "webhook_url": "https://your-app.example/webhooks/tillify",  // Optional, https only
  "external_user_id": "your-user-123"                          // Optional, for support
}

Response:

{
  "client_id": "live_…",
  "client_secret": "sk_live_…",
  "webhook_secret": "whsec_…",
  "business": { "id": "…", "name": "Harbour Street Bakery" },
  "payments_enabled": true,
  "reason": null
}
  • The client_secret is only ever returned here. Store it encrypted.
  • A Setup token works once, and expires after 5 minutes. See errors for what each failure returns.
  • If the business connects your product again (for example, you lost the keys), the new exchange returns the same client_id with a new secret. The old secret keeps working until that exchange succeeds, then stops. A different webhook_url replaces the one you sent before.
  • payments_enabled can be false straight after Connect, most often because a bank account entered by hand is being checked (reason: "bank_account_pending", usually about an hour). Other reasons: no_bank_account, business_inactive, suspended_unpaid_fees. Poll GET /api/v1/account to find out when payments are enabled.
  • The business owner can see and revoke your credentials in Tillify under Integrations. Revoked credentials return 401.
4

Take payments

Authenticate every call with the business's credentials:

Authorization: Bearer <client_secret>
X-Client-Id: <client_id>

Create a payment

POST https://www.tillify.nz/api/v1/payments
Idempotency-Key: <a unique value per payment>   // Optional, recommended

{
  "amount": 1000,                 // Cents, before any fee the business passes on
  "currency": "NZD",
  "external_order_id": "till-42", // Optional, your reference
  "description": "Invoice INV-0042", // Optional: what the payment is for
  "reference": "INV0042",         // Optional: the bank statement reference
  "success_url": "https://…",     // Optional: leave out on a till
  "cancel_url": "https://…",      // Optional
  "expires_in": 600               // Optional: seconds until it can no longer be paid
}
{
  "checkout_url": "https://www.tillify.nz/pay/cs_…",
  "checkout_session_id": "cs_…",
  "status": "pending",
  "description": "Invoice INV-0042",
  "reference": "INV0042",
  "currency": "NZD",
  "amount": 1000,
  "total": 1035,
  "expires_at": "2026-10-05T09:10:00.000Z"   // null without expires_in
}
  • Send an Idempotency-Key header (up to 255 characters) so the call is safe to retry. Repeating a request with the same key returns the payment the first one created instead of making a second. The same key with a different amount, currency, external_order_id, description or reference returns 409.
  • description (up to 200 characters) is shown to the customer on the checkout page and the payment-confirmed page, and is the description of a card payment in the business's Stripe account.
  • reference is what a Pay by Bank payment shows in the reference field of the customer's and the business's bank statements. It must be 1 to 12 letters, digits and spaces, starting and ending with a letter or digit; anything else returns 400, so what you send is what appears. Without it, the statement shows external_order_id with punctuation removed and cut to 12 characters.
  • expires_in is between 60 seconds and 30 days. Without it a payment never expires. Once it passes, the payment can no longer be started and its status becomes cancelled.
  • On a till, show checkout_url as a QR code and leave out success_url. The customer pays on their phone and sees a payment-confirmed page.
  • Online, redirect the customer to checkout_url. They're sent to success_url after paying.
  • Display total, not amount. If the business passes fees on, total includes the Pay by Bank fee. A customer who chooses card sees the card total on the checkout page.

Check a payment

GET /api/v1/payments/{checkout_session_id} returns status (pending, completed, failed, cancelled) plus, in cents: payment_method (pay_by_bank or card; null while pending), amount_requested, surcharge (the fee passed on for the method used), amount_paid (null until completed) and amount_refunded (what the business says it has since paid back; 0 if nothing). It also returns description and reference as you sent them, expires_at, refunded_at, and a legacy amount in dollars that you should ignore.

Cancel a payment

POST /api/v1/payments/{checkout_session_id}/cancel stops a payment being paid, for example when a till gives up on a QR code. It returns 200 with the payment once it can no longer be paid. It returns 409 if the payment has already been paid (reason: "already_completed") or the customer is part-way through paying (reason: "payment_in_progress"); in that case keep checking the status, because the payment may still complete.

Webhook

When a payment made with these credentials completes, we POST to your webhook. You only receive payments made with these credentials, never the business's other sales. If your endpoint doesn't answer with a 2xx status within 10 seconds, we send the same body again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, then stop. Expect the occasional duplicate and mark each payment paid only once. Check the status to learn about failed or cancelled payments; nothing is sent for those.

{
  "event": "payment.completed",
  "data": {
    "id": "…",
    "checkout_session_id": "cs_…",
    "external_order_id": "till-42",
    "status": "completed",
    "payment_method": "pay_by_bank",
    "amount_requested": 1000,
    "surcharge": 35,
    "amount_paid": 1035
  },
  "created_at": "…"
}

The X-Tillify-Signature header is the hex HMAC-SHA256 of the raw body using the webhook_secret. Verify it against the raw body: parsing the JSON and serialising it again can change the bytes. For example, with Node.js and Express:

const crypto = require("crypto");

app.post(
  "/webhooks/tillify",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const expected = crypto
      .createHmac("sha256", webhookSecretFor(req)) // The business's webhook_secret
      .update(req.body)                           // A Buffer of the raw body
      .digest("hex");
    const received = req.get("X-Tillify-Signature") ?? "";

    const valid =
      received.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
    if (!valid) return res.status(401).end();

    const event = JSON.parse(req.body);
    if (event.event === "payment.completed") {
      // Mark event.data.external_order_id as paid
    } else if (event.event === "payment.refunded") {
      // Record event.data.amount_refunded (cents) against the order
    } else if (event.event === "payment.failed" || event.event === "payment.cancelled") {
      // The payment can no longer be paid: let the customer try again with a new one
    }
    // Acknowledge every event, including ones you don't recognise
    res.status(200).end();
  }
);

Refunds

Tillify never holds the customer's money, so it can't refund a payment and there's no refund API. A customer who paid by Pay by Bank can ask the business for a refund from their receipt. The business pays it from its own bank account and records the amount in Tillify. When it does, Tillify sends payment.refunded to your webhook: the same body as payment.completed, still with "status": "completed", plus amount_refunded (cents, possibly less than amount_paid) and refunded_at. Only mark a payment paid on payment.completed.

Failed and cancelled payments

When a payment can no longer be paid, Tillify sends payment.failed (the bank or card provider rejected it) or payment.cancelled (the customer backed out, you cancelled it, or it expired) to the webhook you registered at the exchange. The body is the same as payment.completed, with status set to failed or cancelled and amount_paid null. To let the customer try again, create a new payment.

Check the business can take payments

GET /api/v1/account returns { "business": { "id", "name" }, "payments_enabled", "reason" }.

Change your webhook URL

If your own address changes, you don't need to send every business through Connect again. For each connected business, call PUT /api/v1/account/webhook with that business's credentials and { "webhook_url": "https://…" } (https only). It returns 200 with the new webhook_url, and the webhook_secret stays the same. It returns 403 for credentials the business owner created by hand rather than through Connect. A delivery already being retried keeps going to the old URL, so keep that endpoint answering for about 33 hours.

Errors

Every error is JSON with an error message.

EndpointStatusMeaning
start400A missing parameter, an unknown partner key, or an unregistered redirect_uri
exchange400Invalid body, e.g. a webhook_url that isn't https
exchange401Wrong partner key or secret
exchange404The Setup token doesn't exist or isn't yours
exchange410The Setup token was already used or has expired, or the owner revoked the credentials first
exchange429Too many failed attempts from your IP: 10 in 15 minutes
payments401Unknown or revoked API credentials
payments403The business can't take payments right now; see reason
payments409The Idempotency-Key was already used for a different payment (reason: idempotency_key_reused)
payments422The business has no bank account (reason: no_bank_account)
cancel409Already paid (reason: already_completed), or the customer is part-way through paying (reason: payment_in_progress)

Testing

There's no sandbox yet. Test against production with small real payments into your own test business; we refund them on request. The Tillify fees on those payments are billed to that test business like any other, so ask us to waive them.

Questions, or ready to start? Contact us.

Tillify LogoTillify

QR code, card and bank payments for New Zealanders.

Features

  • Delegates
  • Tickets
  • Till
  • QR Codes
  • E-commerce

Product

  • Pricing
  • How It Works
  • Calculator

Help

  • FAQ
  • Customer Guide
  • Contact Us
  • Guides
  • About Us

Legal

  • Website Terms
  • Merchant Terms
  • Customer Terms
  • Privacy Policy

Powered by

Akahu

Pay by Bank payments run through Akahu, New Zealand's open banking platform.

© 2026 Tillify. All rights reserved.

Made in New Zealand