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
- You send your user to Tillify.
- They sign in, and pick or create their business and bank account.
- We send them back to you with a one-time Setup token.
- Your server exchanges it for the business's API credentials.
- 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://localhostURIs can be registered for development.
Go-live checklist
- 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
Add a “Connect Tillify” button that sends your user to the start link (step 1).
- 3
Handle the callback and check
statebefore anything else (step 2). - 4
Exchange the Setup token from your server within 5 minutes and store the client secret encrypted (step 3).
- 5
Wait for payments to be enabled before you offer Tillify at checkout.
- 6
Create payments and show
checkout_urlas a QR code with thetotal(step 4). - 7
Confirm payments by verifying our webhook, or by polling the payment.
- 8
Handle disconnects: a
401means the business revoked your access. Ask them to connect again. - 9
Test with small real payments into your own test business.
- 10
Point your customers at the merchant setup guide, which walks them through Connect from their side.
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. The link is valid for 60 minutes and can only be completed by the first person who opens it.
Handle the callback
We send the user back to your redirect_uri:
| Outcome | Query parameters |
|---|---|
| Connected | setup_token=tok_…&state=… |
| User cancelled | error=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.
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_secretis 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_idwith a new secret. The old secret keeps working until that exchange succeeds, then stops. A differentwebhook_urlis added alongside the old one. payments_enabledcan befalsestraight 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. PollGET /api/v1/accountto find out when payments are enabled.- The business owner can see and revoke your credentials in Tillify under Integrations. Revoked credentials return
401.
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
{
"amount": 1000, // Cents, before any fee the business passes on
"currency": "NZD",
"external_order_id": "till-42", // Optional, your reference
"success_url": "https://…", // Optional: leave out on a till
"cancel_url": "https://…" // Optional
}{
"checkout_url": "https://www.tillify.nz/pay/cs_…",
"checkout_session_id": "cs_…",
"status": "pending",
"currency": "NZD",
"amount": 1000,
"total": 1035
}- On a till, show
checkout_urlas a QR code and leave outsuccess_url. The customer pays on their phone and sees a payment-confirmed page. - Online, redirect the customer to
checkout_url. They're sent tosuccess_urlafter paying. - Display
total, notamount. If the business passes fees on,totalincludes 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) and amount_paid (null until completed).
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.
{
"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);
// Mark event.data.external_order_id as paid, then acknowledge
res.status(200).end();
}
);Check the business can take payments
GET /api/v1/account returns { "business": { "id", "name" }, "payments_enabled", "reason" }.
Errors
Every error is JSON with an error message.
| Endpoint | Status | Meaning |
|---|---|---|
| start | 400 | A missing parameter, an unknown partner key, or an unregistered redirect_uri |
| exchange | 400 | Invalid body, e.g. a webhook_url that isn't https |
| exchange | 401 | Wrong partner key or secret |
| exchange | 404 | The Setup token doesn't exist or isn't yours |
| exchange | 410 | The Setup token was already used or has expired, or the owner revoked the credentials first |
| exchange | 429 | Too many failed attempts from your IP: 10 in 15 minutes |
| payments | 401 | Unknown or revoked API credentials |
| payments | 403 | The business can't take payments right now; see reason |
| payments | 422 | The business has no bank account (reason: no_bank_account) |
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.