πŸ“– Documentation

FireBuddy Docs

Everything you need to add a Fire Open Banking payment widget to your website β€” from first sign-up to live checkout in under 10 minutes.

FireBuddy is a software service provider: every payment settles bank-to-bank into the merchant's own Fire account β€” money never touches FireBuddy.

πŸ”Œ Integrating FireBuddy as a payment provider? Grab the concise integration guide (Markdown) β€” credentials, the outbound webhook contract, and the full invoice flow on one page, ready to hand to your developers or paste into an AI assistant. The machine-readable spec is at openapi.yaml.

Why build on FireBuddy

Quickstart

Getting FireBuddy live on your site takes three steps:

  1. Get a FireBuddy account β€” book a 15-minute call and we set it up with you, connected to your Fire or Revolut Business account (see Billing).
  2. Create a product in your dashboard β€” give it a name, price, and description. You'll get a unique Product ID.
  3. Paste 2 lines of code onto any webpage. The widget appears automatically, handles the Fire payment, and sends your customer a receipt.
βœ… Most customers go from sign-up to a live payment widget in under 10 minutes β€” no developer needed.

How it works

When a customer clicks "Pay" in your widget, FireBuddy initiates a Fire Open Banking payment request. The customer is shown their bank's interface to authorise the transfer. Once authorised, the money goes directly to your Fire account and FireBuddy records the transaction.

No card details are stored. No payment processor sits in the middle. Just a direct bank-to-bank transfer, initiated via Fire's regulated Open Banking infrastructure.

Instant settlement

SEPA Instant is live on FireBuddy for euro, and Faster Payments for sterling. Funds normally reach the merchant's Fire account within seconds of the customer authorising, not on a settlement cycle. That changes how you should build against this API:

ℹ️ Not every payment is instant β€” build for that. Instant arrival depends on the payer's bank sending on SEPA Instant or Faster Payments. UK banks are on Faster Payments across the board; in Ireland most banks are live, but a few are still rolling instant out, and payments from those arrive later the same day instead of in seconds. Such a payment is not a failure: it goes to completed and fires payment.completed exactly like any other, just later. Never time out a pending payment and mark it failed β€” wait for the webhook, or check GET /payment-requests/{code}. Under EU rules every euro payment provider must support instant transfers by 9 July 2027.

Requirements

ℹ️ Don't have a Fire account yet? Sign up at fire.com β€” it's free to open and takes a few minutes for Irish and UK businesses.

Basic Embed

Add the following two lines to any HTML page, inside the <body> tag, wherever you want the payment widget to appear:

HTML
<!-- Load the FireBuddy widget script once per page -->
<script src="https://firebuddy.info/widget.js"></script>

<!-- Place this div wherever the widget should render -->
<div
  id="firebuddy"
  data-api-key="YOUR_API_KEY"
  data-product-id="YOUR_PRODUCT_ID"
></div>

Replace YOUR_API_KEY with the API key from your dashboard, and YOUR_PRODUCT_ID with the ID of the product you want to sell.

⚠️ Keep your API key private. Don't share it publicly or commit it to a public GitHub repo. If compromised, you can regenerate it from the dashboard.

Attributes

The FireBuddy <div> accepts the following data attributes:

Attribute Required Description
data-api-key Required Your FireBuddy API key, found in your dashboard.
data-product-id Required The UUID of the product to display. Each product has a unique ID.
data-theme Optional Widget colour theme. Accepts light (default) or dark.
data-button-text Optional Override the default "Pay with Fire" button label.
data-redirect-url Optional URL to redirect the customer to after successful payment.

Full example with optional attributes

HTML
<div
  id="firebuddy"
  data-api-key="fb_live_abc123xyz"
  data-product-id="uuid-of-your-product"
  data-theme="dark"
  data-button-text="Book my session"
  data-redirect-url="https://yoursite.ie/thank-you"
></div>

Platform-specific instructions

WordPress

Use the Custom HTML block in the Gutenberg editor, or paste directly into a page template via Appearance β†’ Theme Editor. The widget works in any WordPress theme.

Webflow

Add an Embed element to your page and paste the two lines of code. Make sure "Custom Code" is enabled in your Webflow project settings.

Squarespace

Use a Code Block in any section of your page. Squarespace must be on the Business plan or higher to use custom code.

Shopify

Add a Custom HTML section to any page or product template via the theme editor. Ideal for a standalone product or deposit page β€” not intended to replace the full cart checkout.

Plain HTML

Paste anywhere inside your <body> tag. No build step or framework required.

REST API

The FireBuddy REST API lets you create Fire Open Banking payment requests, read payment status, list payees, and read live balances + transactions from a connected Fire account β€” all from your own backend.

Base URL

BASE
https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/firebuddy-api

Authentication

Every request must include your merchant API key in the Authorization header. Generate one from Dashboard β†’ Developer. Keys are scoped to a single merchant and look like fb_live_xxxxxxxxxxxxxxxx.

HEADER
Authorization: Bearer fb_live_xxxxxxxxxxxxxxxx
⚠️ Treat your API key like a password. Never embed it in client-side JavaScript or commit it to a public repo. If you suspect it has been leaked, rotate it from Dashboard β†’ Developer β†’ Regenerate.

Check readiness β€” GET /me

Call this first. Read one field: ready. When it's false, show the returned message and link action_url β€” don't re-derive readiness from the individual flags.

200 OK
{
  "ok": true,
  "ready": false,                     // ← the single source of truth
  "status": "connect_fire",           // "ready" | "connect_fire"
  "message": "This merchant hasn't finished connecting their Fire account…",
  "action_url": "https://firebuddy.info/dashboard.html#firesetup",
  "connected_as": "Core Kids Β· key \"NEW PORTAL KIDS TEST\"",   // ← show this after a test
  "api_key": { "label": "NEW PORTAL KIDS TEST", "created_at": "2026-08-21T18:10:46Z", "source": "merchant_api_keys" },
  "setup": { "api_key": true, "fire_connected": false, "manual_transfer": false, "webhook": false },
  "merchant": { "business_name": "…", "default_currency": "GBP" }
}

Always display connected_as after a successful connection test. Your settings page can never show a saved API key back to the user, so that one line β€” business name plus the key's own label β€” is the only way they can confirm the app is wired to the right business with the right key. Without it, a key pasted in to test one account is indistinguishable from the live one.

ready: true β†’ create payments/invoices. ready: false β†’ the merchant must finish a one-time setup (almost always connecting their own Fire account). A working key (setup.api_key: true) is separate from Fire being connected β€” both are needed to move money.

Versioning

The API is currently at v1. We will not make breaking changes to v1 endpoints without publishing a new version under a separate path. Additive changes (new fields, new endpoints, new optional parameters) may be made to v1 at any time.

The /functions/v1 segment in the base URL is the Supabase platform version, not the FireBuddy API version.

Create a payment request

POST /payment-requests β€” creates a Fire Open Banking payment request and returns a hosted URL to send the customer to.

FieldRequiredDescription
amountRequiredNumber in major units, e.g. 49.99 for €49.99.
descriptionRequiredShown to the customer in their bank app.
currencyOptionalEUR (default) or GBP. Routes to the matching connected Fire account.
referenceOptionalYour own reference. Truncated to 35 chars (EUR) / 18 chars (GBP) to satisfy Fire's myRef limits. Falls back to description.
emailOptionalCustomer email β€” stored on the payment row for receipts, and the key to the repeat-customer fast path: with the merchant's remembered_bank feature on and a previously settled payment under this address, the response's pay_url deep-links straight into the customer's own bank. Send the same lowercased address every time.
aspsp_uuidOptionalPre-select a bank (uuid from GET /aspsps) β€” pay_url deep-links into it, skipping the picker. Wins over the remembered bank. Always offer "Not your bank?" via picker_url.
returnUrlOptionalOverride the merchant's default redirect. Defaults to https://firebuddy.info/thank-you.html.
tax_amountOptionalHow much of amount is VAT, same currency. amount stays the total the customer pays β€” this only says how it splits. Wins over tax_rate when both are sent, since it's the exact figure your checkout showed.
tax_rateOptionalThe rate amount was charged at, as a decimal fraction β€” 0.23 for 23%, 0.20 for 20%, 0.135 for 13.5%. Sending 23 returns a 400. FireBuddy derives the VAT as amount βˆ’ amount / (1 + tax_rate).
If your checkout charges VAT, send it. FireBuddy can't tell whether a total contains VAT. Without tax_amount (or tax_rate) the merchant's Profit & Loss books the whole amount as net income and reports zero output VAT β€” which understates the VAT they owe on your sales. A checkout showing €1.00 net + €0.23 VAT should send "amount": 1.23, "tax_amount": 0.23. Sales sent without it are flagged on the merchant's Performance page so they know to account for them separately.
cURL
curl -X POST https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/firebuddy-api/payment-requests \
  -H "Authorization: Bearer fb_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 49.99,
    "currency": "EUR",
    "description": "Order #1234",
    "reference": "ORD-1234",
    "email": "customer@example.com",
    "tax_amount": 9.35,
    "tax_rate": 0.23
  }'

Send the customer to checkout_url β€” FireBuddy's checkout for this exact payment. It offers every way the merchant accepts: Revolut Pay, Apple Pay, Google Pay, card and pay by bank when they have Revolut checkout on, Fire bank payment otherwise. Whichever the customer picks, the same paymentId / code completes, and every webhook carries your reference plus provider (fire or revolut). pay_url is Fire's raw page (bank payment only) for flows that must go straight to the bank. The response also carries picker_url (always Fire's full bank picker β€” render it as the "Not your bank?" escape), bank_remembered (true when pay_url was deep-linked from the customer's remembered bank) and aspsp_name (that bank's display name).

Repeat customers β€” "⚑ Pay with their own bank"

GET /payment-methods?email=&currency= β€” ask before creating a request whether this customer has a remembered bank, so your checkout renders a personalised button instead of a generic one. Returns presentation facts only:

JSON
{ "currency": "EUR", "bank_remembered": true, "bank_name": "Bank of Ireland Personal" }

The recipe for an app checkout:

  1. GET /payment-methods?email= β†’ bank_remembered: true? Show "⚑ Pay with {bank_name}"; otherwise show "Pay by bank".
  2. Either way, POST /payment-requests with the customer's email and open the returned checkout_url (or pay_url to go straight to the bank β€” deep-linked into their bank for the repeat customer, Fire's picker for the first-timer).
  3. The first settled payment writes the preference automatically, so payment #2 gets the fast path with no extra work from you.
  4. Always render a "Not your bank?" link using picker_url β€” people change banks, and business bills get paid from personal accounts.
  5. Confirm on your payment.completed webhook (or GET /payments/{id}) β€” never on redirect params, and never fulfil on authorisation.
ℹ️Preferences are per-currency and keyed on the exact lowercased email. The remembered_bank feature is per-merchant β€” ask FireBuddy to enable it. When it's off, bank_remembered is honestly false, never an error.
200 OK
{
  "paymentUrl": "https://payments.fire.com/AbCdEf12",
  "code": "AbCdEf12",
  "paymentId": "5e9c…",
  "paymentLinkId": "f12a…"
}

Send customers straight to their own bank

GET /aspsps β€” the list of banks (ASPSPs) available for open banking, so you can skip Fire's bank-picker entirely. If your checkout already knows which bank the customer uses, asking them again is a step backwards, and every screen between "pay" and their bank costs you conversion.

FieldRequiredDescription
currencyOptionalEUR or GBP β€” filters to banks that can be paid in that currency.
cURL
curl "https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/firebuddy-api/aspsps?currency=EUR" \
  -H "Authorization: Bearer fb_live_xxxxxxxxxxxxxxxx"
200 OK
{
  "total": 34,
  "aspsps": [
    {
      "aspspUuid": "c4a7e1b2-9f3d-4a55-8e21-6b0d7f9c1234",
      "alias": "Bank of Ireland",
      "logoUrl": "https://.../boi.svg",
      "country": "IE",
      "currency": "EUR"
    }
  ],
  "pay_url_template": "https://payments.fire.com/{code}/aspsps/{aspspUuid}"
}

Create the payment request as normal, then build the deep link from the code it returns and the aspspUuid of the customer's bank:

Deep link
https://payments.fire.com/AbCdEf12/aspsps/c4a7e1b2-9f3d-4a55-8e21-6b0d7f9c1234
Use pay_url_template, don't hard-code the host. Sandbox and live are different hosts, and a live payment code sent to the sandbox host simply won't open β€” with no error you'd notice. The template comes back already pointing at the right one for that merchant.
One switch on the merchant's side. Fire keeps this behind PERM_BUSINESS_GET_ASPSPS, which is off by default. If you get a 403, ask the merchant to enable it on their Fire API application β€” it's a checkbox in their Fire account, not a problem with your key. The bank list changes rarely, so cache it rather than calling it per checkout.

Confirming a payment on your own return page

If you send customers to your own page after paying β€” either with returnUrl above or the Redirect URL in Fire Setup β€” do not treat arriving on that page as proof of payment. A customer whose bank blocks them (an Irish AIB card-reader prompt, say) can hit Cancel and still be redirected to you having paid nothing.

FireBuddy appends ?fb=<payment id> to your redirect URL. That id is an unguessable uuid for this one payment. Look it up before you show a receipt, fulfil an order, or mark anything paid:

cURL
curl "https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/payment-status?id=$FB_ID"
200 OK
{
  "state": "paid",           // "paid" | "failed" | "pending"
  "source": "fire",          // "fire" = confirmed against Fire just now
  "amount": 1.23,
  "currency": "EUR",
  "reference": "ORD-1785496659222",
  "description": "Order ORD-1785496659222",
  "business_name": "Devlin Investments Ltd",
  "paid_at": "2026-07-31T11:09:05Z"
}
stateWhat it meansWhat to do
paidThe money moved. Either our record says so, or Fire confirmed it on this call.Show the receipt, fulfil the order.
failedFire reports the request rejected, cancelled or expired.Offer to try again. No money was taken.
pendingNot confirmed yet β€” including when we couldn't reach Fire. Not the same as failed.Say you're confirming, and tell them not to pay again. The webhook will follow.
This endpoint is read-only and needs no API key β€” the payment id is the credential, so treat it as one and don't log it publicly. It never returns customer email or bank details. For server-side fulfilment prefer the payment.completed webhook; this is for what the customer sees in the seconds after they get redirected back.

Get a payment by Fire code

GET /payment-requests/{code} β€” looks up the payment record for a given Fire payment code. Useful for polling status if you don't want to wait for the webhook. Payments settle in seconds, so poll every few seconds for the first minute rather than on a long interval β€” see Instant settlement.

cURL
curl https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/firebuddy-api/payment-requests/AbCdEf12 \
  -H "Authorization: Bearer fb_live_xxxxxxxxxxxxxxxx"
200 OK
{
  "id": "5e9c…",
  "status": "completed",
  "amount": 49.99,
  "currency": "EUR",
  "customer_email": "customer@example.com",
  "fire_payment_code": "AbCdEf12",
  "fire_ref": "ORD-1234",
  "authorised_at": "2026-04-07T12:35:02.000Z",
  "funds_confirmed_at": null,
  "in_flight": false,
  "settled": true,
  "created_at": "2026-04-07T12:34:56.000Z",
  "updated_at": "2026-04-07T12:35:14.000Z"
}
in_flight: true means the payer's bank approved and the money has not arrived yet. Their status is still pending β€” the same value as a customer who opened the pay page and walked away β€” because only a real Fire settlement may complete a payment. Without this field you cannot tell those two apart, so your page offers Pay Now to someone whose money is already moving, and they pay twice. Show “payment on its way” and hide the pay button while in_flight is true. Never fulfil on it β€” an authorised payment can still fail. Fulfil on settled (status: "completed"), nothing else. This is the polling equivalent of the payment.authorised webhook.

Get a payment by FireBuddy ID

GET /payments/{id} β€” same as the previous endpoint but keyed on the FireBuddy paymentId returned from POST /payment-requests. Returns extra fee + settlement fields, plus the same in_flight / settled pair.

Manual bank-transfer details

GET /bank-details β€” returns the merchant's own receiving account so you can show a "pay by bank transfer instead" fallback on your page: manual_transfer_enabled, account_holder, iban, bic (EUR) or sort_code, account_number (GBP). Only render the fallback when manual_transfer_enabled is true and the fields are present. (GET /accounts returns Fire balances β€” ICAN, not the payable IBAN/BIC β€” so use this endpoint for the transfer account.)

Invoices

Create and manage invoices via the API. You can have FireBuddy mint a Fire pay link, email the invoice for you, or just record one you send yourself.

POST /invoices β€” create an invoice with line items. Idempotent by invoice_number: posting the same number again returns the existing invoice with already: true instead of creating a duplicate β€” retries and re-run syncs are safe. Use PATCH /invoices/{id} to change one. Order-flow expiry: an invoice that mints a pay link with no due_date auto-cancels after 14 days unpaid (an abandoned checkout, not a receivable) β€” tune it with expires_after_days (1–365, or 0 to opt out), or set a due_date: invoices with a due date are never auto-cancelled. A customer who pays late anyway still marks it paid β€” settlement always wins. VAT: tax_rate is a decimal fraction (0.23 for 23%; 1 or more returns 400). Omit it and the invoice uses the client's own VAT rate if the merchant set one on the client's record, otherwise 0.

FieldTypeNotes
invoice_numberstringRequired. Unique per account β€” the natural key for idempotency.
client_name / client_emailstringThe customer. Email lets FireBuddy send it.
currencystringEUR or GBP (default EUR).
due_datedateOptional, YYYY-MM-DD.
itemsarrayRequired. Each: {description, quantity, unit_price}.
create_payment_linkbooleanMint a Fire pay link + attach it.
sendbooleanFireBuddy emails the invoice (Pay Now + QR).
mark_sentbooleanRecord as "sent" β€” you email it yourself (FireBuddy won't).
cURL
curl -X POST https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/firebuddy-api/invoices \
  -H "Authorization: Bearer fb_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_number": "INV-1001",
    "client_name": "Jane Doe",
    "client_email": "jane@example.com",
    "currency": "EUR",
    "due_date": "2026-07-15",
    "items": [{ "description": "Consultation", "quantity": 1, "unit_price": 100 }],
    "create_payment_link": true,
    "send": true
  }'

The response includes the invoice id, invoice_url, payment_url and payment_reference. Share invoice_url (also returned as checkout_url) β€” the branded hosted invoice page (line items, VAT, your logo) with every way to pay the merchant accepts (Revolut Pay, Apple/Google Pay, card and pay by bank when Revolut checkout is on). payment_url is the raw Fire checkout, for embedding in your own flow. Whichever way the customer pays, invoice.paid fires; if you poll the original payment, invoice_paid: true + paid_by_payment_id tell you it was paid another way.

Who to chase first β€” GET /receivables

Ranking overdue invoices by age alone is wrong: a €80 invoice a week late from a reliable client is not the problem, and a €4,000 invoice from someone who has been late four times running is. GET /receivables scores every open invoice 0–100 and returns them worst first, with a plain-English reason for every point that landed.

GET
curl https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/firebuddy-api/receivables \
  -H 'Authorization: Bearer fb_live_xxxxx'

Score components: lateness (max 40) · that client's own payment record (25) · size relative to your median invoice (20) · reminders already ignored (15). Bands: critical ≥70, high ≥50, medium ≥30, low ≥15, watch below.

Read suggested_action, not just the score. It overrides the ranking wherever a reminder would be the wrong instrument:

Each invoice also carries client_history (their paid count, late count and average days to pay) so the ranking can be checked rather than trusted.

Reading the replies

When a customer replies on the invoice page, FireBuddy reads it and adjusts the chase. Plain-language patterns are matched directly; anything freer is classified by Claude (your own key from Settings → Integrations, if you have added one). The result appears on the invoice as reply_intent:

IntentWhat FireBuddy does
claims_paidStops chasing, ranks it first, asks you to verify against Fire
disputeMarks the invoice queried — which already excludes it from every chase path
promise_to_payHolds until the promised date, then resumes on its own
hardshipStops and escalates — payment terms are your decision, not the software's
wrong_recipient / stop_contactStops chasing that invoice
acknowledgementNothing — recorded only
unclearStops and hands it to you unlabelled. Guessing would be worse

And it drafts the answer

Stopping is safe but half a job. For the two intents with an obvious next move, FireBuddy writes the reply for you and leaves it in Getting paid to approve:

Via the API: GET /receivables returns drafted_reply and draft_proposal; POST /invoices/{id}/reply sends it. There is no “send the draft as-is” shortcut — the caller must pass the message text, which means somebody read it.

A proposed credit is advice, not an action. FireBuddy never edits the invoice, issues a credit note, or changes what is owed. If you agree with the figure, you send a corrected invoice yourself.

The boundary: every one of those outcomes either stops the chase or flags it for a human. The classifier cannot send a message, and it cannot mark an invoice paid — “the customer says they paid” is a claim, and only money landing in your Fire account settles it. Every classification is written to your audit log with the reason and the confidence, so “why did chasing stop on that invoice?” always has an answer.

Omit invoice_number on create and FireBuddy assigns the next number in your own sequence.

Quick invoice β€” one call, no questions

POST /quick/invoice β€” the fast path. Send only what a person would say: to (a client name or email β€” optional), amount (what the customer pays, major units) and for (what it's for). FireBuddy resolves everything else from the merchant's account: matches the client (email exact, name fuzzy β€” most-recently-invoiced wins a tie), picks the next invoice number, applies the client's own VAT rate if one is set on their record, else the merchant's usual rate (explicit tax_rate wins; vat:false forces none), backs the net out so the total lands on the number given, mints the pay link, and emails it when the client has an address (send:false to stop that). Returns the created invoice plus a resolved block saying exactly what it chose.

POST /quick/invoice
{ "to": "John Devlin", "amount": 250, "for": "Brake job" }

β†’ { …invoice, "invoice_url": "https://firebuddy.info/invoice?r=hiebhryi",
    "resolved": { "client": "John Devlin", "client_matched": true,
                  "invoice_number": "INV-2026-0113",
                  "tax_rate": 0.23, "tax_rate_source": "your usual rate",   // or "this client's rate" / "given" / "none"
                  "emailed": true } }

Offers β€” one-page landing pages with a Pay button

POST /offers β€” creates a hosted offer page (title, headline, bullets, price, optional photo) and its payment link; returns offer_url. Requires the offer_pages feature on the account (a 403 names the flag, not a bad key). Fields: title, price (VAT-inclusive total unless price_includes_vat:false), currency, tax_rate, headline, description, bullets[], image_url (https), expires_in_days, and two availability controls:

Expiry, pause and limits are all enforced where the money moves, not just where the button is drawn β€” the page and the checkout share one answer.

Offer feed β€” your shop window, for machines

PUT /feed with {"slug":"devlins-garage"} publishes a public, key-less, machine-readable list of every active, purchasable offer at https://firebuddy.info/feed/<slug>: price, what's included, how many left, when it ends, the buy URL β€” plus a schema.org OfferCatalog/Offer JSON-LD twin so any AI assistant or search engine can read it without knowing FireBuddy. Sold-out, expired and paused offers drop off automatically; nothing appears that the checkout would refuse. Opt-in: until you set a slug the feed is 404. DELETE /feed unpublishes.

List saved payees

GET /payees β€” returns the merchant's saved payout destinations (IBAN/BIC for EUR, sort code/account number for GBP).

200 OK
[
  {
    "id": "a9b1…",
    "name": "Acme Supplies Ltd",
    "iban": "IE12BOFI…",
    "bic": "BOFIIE2D",
    "currency": "EUR",
    "created_at": "2026-03-12T09:12:00.000Z"
  }
]

Each payee also has is_approved (approved in the merchant's Fire app β€” only approved payees can be paid through the API) and masked_account (last four digits, e.g. β€’β€’β€’β€’ 4821), so your app can show which account without holding the number.

Refunds β€” back to the payer's card or Revolut account

POST /payments/{id}/refund with {"amount":25.00,"reason":"Week cancelled","idempotency_key":"your-unique-id"} refunds a payment made through Revolut checkout (Revolut Pay or card) back to the payer's original method, from the merchant's own Revolut balance. {id} is the payment id or the payment-request code. Leave out amount to refund everything left. It returns 201 with refunded_amount and status (refunded once refunded in full). The same idempotency_key never refunds twice.

POST /refunds with {"idempotency_key":"…","reason":"Club cancelled","payments":[{"payment_id":"…"},{"payment_id":"…","amount":20}]} refunds up to 200 payments at once and returns a result per payment. A payment made by bank transfer can't be pulled back: it comes back not_revolut so you can refund it from the bank. Needs the account's refunds switch, which FireBuddy turns on.

Payouts β€” pay an approved payee

POST /payouts with {"payee_id":"…","amount":182.50,"reference":"Commission Oct","idempotency_key":"your-unique-id","metadata":{…}} creates a Fire bank-transfer batch to that payee and submits it. It returns 202 with "status":"awaiting_bank_approval": the money moves only after the merchant approves the batch in their Fire app. A key alone can never move money.

GET /payouts/{id} returns awaiting_bank_approval β†’ sent | failed (with failure_reason). sent means Fire reports the batch COMPLETE, nothing else. When that happens FireBuddy sends payout.completed or payout.failed to your webhook endpoints (payout_id, amount, currency, reference, idempotency_key, metadata, sent_at/failure_reason), signed like every other event. Webhooks are not retried β€” poll GET /payouts/{id} for anything still open as a backstop.

202 Accepted
{
  "id": "5c0e…",
  "status": "awaiting_bank_approval",
  "amount": 182.5,
  "currency": "GBP",
  "payee_id": "9b2f…",
  "reference": "Commission Oct",
  "idempotency_key": "your-unique-id",
  "metadata": { "partner": "emma" },
  "created_at": "2026-11-05T10:02:11.000Z",
  "sent_at": null,
  "failure_reason": null
}

Revolut payout runs β€” pay many people with one draft

For merchants who pay out of Revolut Business (e.g. a monthly wages run). FireBuddy creates one Revolut payment draft holding every payment. Nothing is paid until the merchant opens Transfers β†’ Drafts in the Revolut Business app and presses Send. FireBuddy only ever holds Revolut's READ and WRITE scopes (WRITE = payees and payment drafts) β€” never PAY.

GET /revolut/payees β€” the merchant's live Revolut payees, one entry per account: [{"id","name","masked_account","currency","account_id"}]. masked_account is the last four digits only; full account numbers are never returned. Payees are added in the Revolut Business app.

POST /payout-runs with {"title":"CK wages Oct","currency":"GBP","idempotency_key":"hq-wages-2026-10","payments":[{"counterparty_id":"…","amount":412.50,"reference":"CK wages Oct","metadata":{…}}]} (1–200 payments) returns 202 with "status":"awaiting_bank_approval", the Revolut draft_id, the total and one item per payment.

GET /payout-runs/{id} checks Revolut first (at most every 30 seconds; a background sweep every 10 minutes for 14 days) and returns the run with each item's status, sent_at and failure_reason. Run status: awaiting_bank_approval β†’ sent | part_sent | failed. Each payment sends payout.completed or payout.failed once (payout_id = item id, run_id, amount, currency, reference, metadata), signed like every other event.

Live Fire balances

GET /accounts β€” proxies the merchant's Fire /business/v1/accounts call server-side and returns simplified balances. Amounts are in major units (the Fire response is in cents).

200 OK
{
  "accounts": [
    {
      "ican": 1234567,
      "name": "Main EUR account",
      "currency": "EUR",
      "balance": 18420.55,
      "availableBalance": 18420.55,
      "status": "LIVE"
    }
  ]
}

Recent transactions

GET /transactions?ican=1234567&limit=100 β€” recent transactions for a given Fire account. ican defaults to the merchant's primary EUR/GBP account if omitted. limit caps at 200.

200 OK
{
  "ican": 1234567,
  "transactions": [
    {
      "txnId": "fire-txn-7788",
      "date": "2026-04-07T11:02:00.000Z",
      "amount": 49.99,
      "currency": "EUR",
      "direction": "IN",
      "reference": "ORD-1234",
      "description": "Open Banking payment",
      "balance": 18420.55
    }
  ]
}

CSV exports

Three CSV endpoints stream a UTF-8 file (with BOM, so Excel opens it correctly) containing every row in the requested range. Filter with optional from and to query parameters (ISO-8601 timestamps, both inclusive, applied to created_at).

cURL
curl -L -o payments.csv \
  "https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/firebuddy-api/exports/payments.csv?from=2026-01-01&to=2026-03-31" \
  -H "Authorization: Bearer fb_live_xxxxxxxxxxxxxxxx"

VAT summary

GET /exports/vat-summary?from=…&to=… β€” both sides of the VAT return on the invoice basis: sales (output VAT, T1) from issued invoices and purchases (input VAT, T2) from approved supplier bills, each broken down by currency Γ— rate, plus vat_position_by_currency (T1 βˆ’ T2 per currency; positive = payable, negative = reclaim). Sales tax_rate is a fraction (0.23); purchases vat_rate_percent is a percentage (23.0). Draft and cancelled/void documents are excluded on both sides; to includes the whole of that day. On the FireBuddy company account, output VAT also includes VAT on FireBuddy credit top-ups received (credit_topups_breakdown, already in the totals). All figures are estimates from FireBuddy records β€” confirm with an accountant before filing.

200 OK
{
  "from": "2026-01-01",
  "to": "2026-03-31",
  "breakdown": [
    { "currency": "EUR", "tax_rate": 0.23, "invoice_count": 12, "net": 4810.00, "vat": 1106.30, "gross": 5916.30 },
    { "currency": "EUR", "tax_rate": 0.13, "invoice_count": 4,  "net": 920.00,  "vat": 119.60,  "gross": 1039.60 }
  ],
  "totals_by_currency": [
    { "currency": "EUR", "net": 5730.00, "vat": 1225.90, "gross": 6955.90 }
  ]
}

Compliance bundle

GET /exports/compliance-bundle?from=…&to=… β€” returns a single JSON document containing every payment, payout and invoice in the requested range, ready to hand to an external auditor or archive for compliance.

ℹ️ White-label tenants can disable this endpoint per-tenant by setting feature_flags.allow_compliance_export = false. Disabled tenants receive 403. Merchants on the root platform are allowed by default.
200 OK
{
  "generated_at": "2026-04-07T12:00:00.000Z",
  "range": { "from": "2026-01-01", "to": "2026-03-31" },
  "counts": { "payments": 142, "payouts": 18, "invoices": 16 },
  "payments": [ /* … */ ],
  "payouts":  [ /* … */ ],
  "invoices": [ /* … */ ]
}

Audit log

GET /audit-log?from=…&to=…&limit=200 β€” returns recent merchant-side audit log entries. Mutating API calls (creating a payment request, generating a GDPR export, etc.) write here automatically. Use GET /exports/audit-log.csv for the CSV equivalent suitable for handing to an auditor.

200 OK
{
  "entries": [
    {
      "id": "9e1b…",
      "action": "payment_request.created",
      "resource_type": "payment",
      "resource_id": "be7a…",
      "actor": "api",
      "actor_ip": "203.0.113.42",
      "metadata": { "amount": 49.99, "currency": "EUR", "fire_code": "AbCdEf12" },
      "created_at": "2026-04-07T11:02:00.000Z"
    }
  ]
}

GDPR data export

GET /exports/me.json β€” returns a single JSON document containing every record FireBuddy holds for the authenticated merchant: settings, API key metadata (no secret material), payments, payouts, invoices, payees and the full audit log. Generating an export is itself recorded as a gdpr_export.generated audit entry.

ℹ️ Use this endpoint to satisfy GDPR Article 20 (data portability) or Article 15 (right of access) requests. The export contains the merchant's own data only β€” never another merchant's.

Errors

All errors return JSON of the shape { "error": "human-readable message" } with the HTTP status below.

StatusMeaningWhen
400Bad requestMissing or invalid request body field.
401UnauthorizedMissing, malformed, or revoked API key.
403ForbiddenThe endpoint is disabled for this merchant's tenant (e.g. compliance bundle).
404Not foundThe resource does not exist for this merchant.
422UnprocessableMerchant has not finished configuring Fire credentials.
502Upstream errorFire's API rejected the call. Check the response body for the upstream message.
πŸ” Retry 5xx once before treating it as failure. A 502/503 that isn't from Fire (empty or non-JSON body) is almost always the API host briefly restarting β€” for example during a deploy β€” and lasts a few seconds. One retry after ~1–2s makes these invisible. Better still, build on webhooks rather than polling: pushed events don't care when the host restarts.
ℹ️ The full machine-readable spec is published at openapi.yaml and is suitable for importing into Postman, Insomnia, or any OpenAPI 3.1 codegen tool.

Webhooks

FireBuddy forwards payment events to your backend in near real-time β€” with instant settlement, payment.completed normally lands within seconds of the customer authorising, so this is the event to build fulfilment on (see Instant settlement). Exact timing depends on the merchant's Fire setup: where Fire notifies FireBuddy directly the event is near-instant; otherwise FireBuddy detects the settlement on a 60-second check, so allow up to ~a minute. Either way the event fires only on Fire's own confirmation of that exact payment β€” never on a guess. A merchant account can have multiple webhook endpoints β€” register one per tool. Every event is delivered to all active endpoints, each HMAC-SHA256 signed with that endpoint's own secret, so several apps can share one FireBuddy account without stepping on each other.

Register an endpoint either in Dashboard β†’ API Keys β†’ Webhook endpoints, or self-register from your app with its API key:

cURL
curl -X POST https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/firebuddy-api/webhooks \
  -H "Authorization: Bearer fb_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-app.com/webhooks/firebuddy", "label": "My tool" }'

The response includes the endpoint's secret once β€” store it and use it to verify deliveries (you can also supply your own secret in the request, min 16 chars). Manage endpoints with GET /webhooks, PATCH /webhooks/{id} (is_active) and DELETE /webhooks/{id}.

Verifying the signature

Every webhook request includes an x-firebuddy-signature header containing a hex-encoded HMAC-SHA256 of the raw request body, computed with your merchant signing secret. Reject any request whose signature does not match.

Node.js
import crypto from "node:crypto";

export function verifyFireBuddySignature(rawBody, headerSig, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  // constant-time compare to avoid timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(headerSig, "hex")
  );
}
⚠️ Use the raw request body bytes β€” not a re-stringified JSON object. Your framework may parse the body before you see it; configure it to expose the raw bytes (e.g. express.raw()) on this route.

Events & schema

Each delivery is a POST with two headers β€” x-firebuddy-event (the event name) and x-firebuddy-signature (the HMAC) β€” and a flat JSON body of the shape { event, ...data, timestamp }:

JSON
{
  "event": "invoice.paid",
  "invoice_id": "5e9c…",
  "invoice_number": "INV-1001",
  "payment_id": "a1b2…",
  "amount": 100.00,
  "currency": "EUR",
  "timestamp": "2026-07-03T12:35:14.000Z"
}
EventFires when
payment.authorisedThe payer's bank approved the payment. Approve the order on this event β€” the money follows, usually within seconds. Carries settled: false (reconcile on payment.completed) and stage (authorised or funds_confirmed).
payment.completedA payment settled (Fire lodgement, or a confirmed mark-paid). This is the money event.
invoice.paidAn invoice was paid.
payment.failedA payment was rejected, cancelled or expired. If was_authorised is true, revoke what you released on payment.authorised for that payment_id.
βœ… The fulfilment rule: approve on payment.authorised, reconcile on payment.completed, revoke on payment.failed. A customer who has just paid for an event or booking should get access straight away, not wait for the bank batch. payment.authorised is a real bank approval and the money follows, usually within seconds; settled: false simply tells you which event to reconcile your books on. On the rare occasion a bank pulls an authorised payment you will receive payment.failed with was_authorised: true for the same payment_id β€” that is your signal to revoke.

Acknowledge with 2xx within 10 seconds. Make your endpoint idempotent (key on payment_id / invoice_id) β€” retries can occur.

Claude connector

FireBuddy connects to Claude (claude.ai) as a connector, so you can take payments by talking: β€œInvoice John €250 for the brake job and send him the pay link” β€” Claude creates the invoice on your account, emails it with a Pay Now button, and can tell you the moment the money lands.

Connect it

  1. In Claude: Settings β†’ Connectors β†’ Add custom connector
  2. Name it FireBuddy and use the URL https://firebuddy.info/mcp β€” leave the OAuth fields empty
  3. Click Connect β€” you’ll be sent to FireBuddy to log in and approve. Approving creates an API key labelled β€œClaude connector” on your account (Dashboard β†’ API Keys). Revoking that key disconnects Claude instantly.
ℹ️ No FireBuddy account yet? Sign up first β€” you’ll need your Fire business account connected before payment links can settle.

What Claude can do

The fast path: β€œinvoice John €250 for the brake job” β€” one call, no questions; FireBuddy resolves the client, the invoice number, your usual VAT and whether to email it. Also: payment links Β· full multi-line invoices Β· offer pages (with order limits and a live countdown) Β· chase everyone who owes you (preview first, send on your say-so) Β· publish your offer feed Β· check whether a payment has landed Β· list invoices and transactions Β· live Fire balances Β· VAT summaries for your accountant. Claude asks your approval before money-touching actions by default β€” keep it that way.

Slash command: install the /firebuddy skill (ask us for the file, or find it in the FireBuddy repo under skills/) and type /firebuddy invoice John 250 for the brake job β€” or say it into the mic.

Try saying

Honesty rules built in

The connector carries the same guarantees as everything else here: a payment is only ever reported paid when Fire has confirmed that exact payment landed in your account β€” never guessed. Money settles to your own Fire account; FireBuddy never holds it. VAT figures are estimates to confirm with your accountant before filing.

Data handling

Claude’s requests are forwarded to the same REST API documented above under your own revocable key. FireBuddy stores what the API already stores β€” your payments, invoices and account metadata β€” and nothing extra for Claude. OAuth codes and tokens are held only as hashes. See the privacy policy.

WhatsApp β€” run FireBuddy by message

Message FireBuddy's WhatsApp number from a linked phone and get the link back in seconds. Same fast path as the Claude connector; the door merchants already have open all day.

Link a phone

The easy way: Dashboard β†’ Settings β†’ Integrations β†’ WhatsApp β†’ Add a phone. Programmatically: POST /whatsapp/link with {"phone":"+353871234567"} (or ask Claude: β€œlink my WhatsApp +353…”) returns a 6-digit code. WhatsApp that code to FireBuddy's number from the phone being linked within 15 minutes; the number is then bound and gets its own API key labelled WhatsApp Β· +353… on your API Keys page β€” revoke it there to unlink, or DELETE /whatsapp/link. GET /whatsapp/links lists bound and pending numbers plus display_number (the number to message).

πŸ”’ A phone number alone never authorises anything: the code binds it, every WhatsApp verb creates or reads (nothing moves money out), every message is signature-verified from Meta and logged with what was understood and done. Amounts are what the customer pays; a rate calculation like β€œ30 hrs at 100” is asked about rather than guessed.

Creating Products

Each product represents something you're selling β€” a service, session, product, or booking slot. You can have as many products as you like.

  1. Go to Products & Widgets in your dashboard
  2. Click New Product and fill in the name, description, price, and icon
  3. Click Embed next to any product to get the ready-to-paste code snippet
ℹ️ Each product has a unique UUID. To show two different products on the same page, use two separate <div id="firebuddy"> elements with different data-product-id values and unique wrapper IDs.

API Keys

Your API key identifies your account when the widget loads or you call the REST API. You have one active key at a time.

Billing β€” one monthly plan

FireBuddy is a done-for-you service: €500 one-off setup, then €149 a month (plus VAT; UK businesses are quoted in sterling). Everything is included, with up to 500 payments a month and no per-payment FireBuddy fee. Billed monthly by invoice; no long contract.

Older pay-as-you-go accounts

Some accounts opened before October 2026 are still on pay-as-you-go: a flat €0.30/Β£0.30 per payment, drawn from prepaid FireBuddy Credits (minimum top-up €100/Β£100, credit never expires). On those accounts only, when credit runs out, creating new payment requests and pay links pauses (the API returns 402 credit_exhausted); existing links keep working and settling as normal. Integrations should handle 402 by telling the merchant to top up.

FAQ

Does FireBuddy work outside Ireland?

FireBuddy uses Fire Open Banking, which is available in Ireland and the UK. Your customers must have a bank that supports Open Banking (most major Irish and UK banks do).

How much does FireBuddy cost?

€500 one-off setup, then €149 a month (plus VAT) with everything included and up to 500 payments a month β€” no per-payment FireBuddy fee (see Billing). Fire and Revolut charge their own fees on the merchant's account.

Can I have multiple widgets on one page?

Yes. Give each widget div a unique id attribute and a different data-product-id. The widget script handles multiple instances on the same page.

What happens if a customer's payment fails?

Open Banking payments are authorised at the bank level β€” the customer sees a failure message in their banking app. The widget displays an error state and the customer can try again.

Can I customise the widget's appearance?

Basic theming is available via data-theme="dark". More advanced styling options (custom colours, fonts) are on the roadmap for a future release.

I lost my API key β€” what do I do?

Your key is always visible in the dashboard Overview tab. If you think it's been compromised, contact hello@firebuddy.info to rotate it.

Need help?

Email us at hello@firebuddy.info β€” we typically reply within one business day.