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.
openapi.yaml.
Why build on FireBuddy
- Honest payment states.
payment.authorisedmeans the payer's bank approved β show it on their screen;payment.completedmeans the money landed β fulfil on it. The API never blurs the two, because settled-only truth is the house rule everything here is built on. - Flat, predictable pricing. Merchants pay FireBuddy one monthly price with up to 500 payments included β no per-payment FireBuddy fee, so an integration's volume never changes their bill.
- Webhooks per tool. Register your own endpoint with your own API key; every event is HMAC-signed with your endpoint's secret. Several tools share one merchant without stepping on each other, and pushed events beat polling (see Webhooks).
- Your UI to the bank door. Bank deep links send the payer from your checkout straight into their own bank β no generic picker in your flow.
- No card data, no PCI. Bank payments are pushed bank-to-bank by the payer (no chargebacks). Card, Apple Pay and Google Pay go through Revolut's checkout on
checkout_urlpages, so no card number ever reaches FireBuddy or your system. - Safe to retry. Invoice creation is idempotent by
invoice_number, payment-request creation is guarded against doubles, and one-call /quick/invoice does the whole invoice-and-send in a single request.
Quickstart
Getting FireBuddy live on your site takes three steps:
- 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).
- Create a product in your dashboard β give it a name, price, and description. You'll get a unique Product ID.
- Paste 2 lines of code onto any webpage. The widget appears automatically, handles the Fire payment, and sends your customer a receipt.
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:
- Build webhook-first.
payment.completedtypically fires seconds after authorisation. Treat that event as your signal to fulfil the order β releasing goods on a nightly reconciliation job means holding a customer for hours after they've already paid. - Don't show a multi-day "processing" state. Anything modelled on card settlement (pending β cleared over 2β3 days) will misrepresent what actually happened.
- Poll fast, but don't give up early. If you can't take webhooks,
GET /payment-requests/{code}every few seconds catches the common case β a 15-minute loop adds 15 minutes of latency to a payment that already landed. Keep checking well beyond that though: a payer on a bank that hasn't switched instant on yet can arrive hours later, and abandoning the poll doesn't mean the payment failed. - Payment-request status stops at
AUTHORISED. Fire's request object doesn't advance to a "settled" state β authorisation is the point at which an instant payment is on its way. Use FireBuddy's ownstatusfield (completed) and thepayment.completedwebhook as the source of truth for "money has arrived".
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
- An active Fire.com business account (EUR and/or GBP)
- A FireBuddy account on the monthly plan (see Billing)
- A website where you can paste HTML β any CMS or custom site works
Basic Embed
Add the following two lines to any HTML page, inside the <body> tag, wherever you want the payment widget to appear:
<!-- 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.
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
<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
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.
Authorization: Bearer fb_live_xxxxxxxxxxxxxxxx
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.
{
"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.
| Field | Required | Description |
|---|---|---|
amount | Required | Number in major units, e.g. 49.99 for β¬49.99. |
description | Required | Shown to the customer in their bank app. |
currency | Optional | EUR (default) or GBP. Routes to the matching connected Fire account. |
reference | Optional | Your own reference. Truncated to 35 chars (EUR) / 18 chars (GBP) to satisfy Fire's myRef limits. Falls back to description. |
email | Optional | Customer 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_uuid | Optional | Pre-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. |
returnUrl | Optional | Override the merchant's default redirect. Defaults to https://firebuddy.info/thank-you.html. |
tax_amount | Optional | How 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_rate | Optional | The 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). |
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 -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=¤cy= β 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:
{ "currency": "EUR", "bank_remembered": true, "bank_name": "Bank of Ireland Personal" }
The recipe for an app checkout:
GET /payment-methods?email=βbank_remembered: true? Show "β‘ Pay with {bank_name}"; otherwise show "Pay by bank".- Either way,
POST /payment-requestswith the customer'semailand open the returnedcheckout_url(orpay_urlto go straight to the bank β deep-linked into their bank for the repeat customer, Fire's picker for the first-timer). - The first settled payment writes the preference automatically, so payment #2 gets the fast path with no extra work from you.
- Always render a "Not your bank?" link using
picker_urlβ people change banks, and business bills get paid from personal accounts. - Confirm on your
payment.completedwebhook (orGET /payments/{id}) β never on redirect params, and never fulfil on authorisation.
remembered_bank feature is per-merchant β ask FireBuddy to enable it. When it's off, bank_remembered is honestly false, never an error.{
"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.
| Field | Required | Description |
|---|---|---|
currency | Optional | EUR or GBP β filters to banks that can be paid in that currency. |
curl "https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/firebuddy-api/aspsps?currency=EUR" \ -H "Authorization: Bearer fb_live_xxxxxxxxxxxxxxxx"
{
"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:
https://payments.fire.com/AbCdEf12/aspsps/c4a7e1b2-9f3d-4a55-8e21-6b0d7f9c1234
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.
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 "https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/payment-status?id=$FB_ID"
{
"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"
}
| state | What it means | What to do |
|---|---|---|
paid | The money moved. Either our record says so, or Fire confirmed it on this call. | Show the receipt, fulfil the order. |
failed | Fire reports the request rejected, cancelled or expired. | Offer to try again. No money was taken. |
pending | Not 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. |
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 https://srjrxyobhfegimgqfpor.supabase.co/functions/v1/firebuddy-api/payment-requests/AbCdEf12 \ -H "Authorization: Bearer fb_live_xxxxxxxxxxxxxxxx"
{
"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.
| Field | Type | Notes |
|---|---|---|
invoice_number | string | Required. Unique per account β the natural key for idempotency. |
client_name / client_email | string | The customer. Email lets FireBuddy send it. |
currency | string | EUR or GBP (default EUR). |
due_date | date | Optional, YYYY-MM-DD. |
items | array | Required. Each: {description, quantity, unit_price}. |
create_payment_link | boolean | Mint a Fire pay link + attach it. |
send | boolean | FireBuddy emails the invoice (Pay Now + QR). |
mark_sent | boolean | Record as "sent" β you email it yourself (FireBuddy won't). |
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.
POST /invoices/{id}/sendβ FireBuddy emails the invoice (Pay Now + QR). Optional body:to,subject,message. Defaultstoto the linked client's email.GET /invoicesβ list invoices (?status&from&to&limit&offset).GET /invoices/{id}β one invoice with line items.PATCH /invoices/{id}β update a draft/sent invoice (status,due_date,notes).POST /invoices/{id}/mark-paidβ record an out-of-band payment. Withconfirmed:trueit flips the invoice to paid and fires thepayment.completed+invoice.paidwebhooks.POST /invoices/chaseβ re-send the reminder email for every sent-but-unpaid invoice that is overdue (or older thanolder_than_days). Defaults to a preview (dry_run:true) listing who would be chased; passdry_run:falseto send. Clients with no email are returned underskipped_no_email. Invoices the client has queried are never chased β they come back underneeds_your_replyfor you to answer. Uses the same email path as the dashboard's Send button.GET /receivablesβ risk-ranked list of everything you are owed, worst first, with the reasons attached. See below.
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.
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:
verify_paymentβ the customer says they already paid. Chasing is paused and the invoice goes to the top of the list. Check Fire. FireBuddy will never settle an invoice on a customer's say-so β only Fire's word about that exact payment does that.stop_chasingβ the customer asked not to be contacted, or told us we have the wrong person. Reminders stop until you say otherwise.on_holdβ the customer promised a date. Chasing pauses untilhold_until, then resumes by itself.needs_replyβ the client raised a query. Never chased. Answer the question; replying resets the invoice and chasing resumes on its own.handle_personallyβ a client who has always paid on time is unusually late, or the amount is well above your normal. Ring them; a form email is the wrong instrument.human_reviewβ the reminder budget is spent, there is no email on file, the client cancelled, or they raised difficulty paying. A person decides.at_riskβ not late yet, but this client usually is. Fires only with at least two settled invoices of history (one late payment is an anecdote) and only inside 14 days, when a nudge is still cheap. A warning two months out is noise.chaseβ safe to send the standard reminder.pausedβ you silenced this invoice.waitβ not due yet.
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:
| Intent | What FireBuddy does |
|---|---|
claims_paid | Stops chasing, ranks it first, asks you to verify against Fire |
dispute | Marks the invoice queried — which already excludes it from every chase path |
promise_to_pay | Holds until the promised date, then resumes on its own |
hardship | Stops and escalates — payment terms are your decision, not the software's |
wrong_recipient / stop_contact | Stops chasing that invoice |
acknowledgement | Nothing — recorded only |
unclear | Stops 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:
- They say they paid → a note asking for the date, amount and reference — the three things that let you find the lodgement in Fire. Asking for the reference is the opposite of assuming the payment happened.
- They queried it → an answer written from the invoice's own line items. Where the arithmetic is unambiguous (“billed 3, only 2 arrived”) it names the credit and the revised total.
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:
max_ordersβ order limit. The page shows βOnly 3 of 10 leftβ and then Sold out; the checkout refuses the next buyer. Counted from Fire-confirmed sales only.1= one buyer, full stop.countdown:trueβ stream mode: a live countdown to the expiry on the page (server time, so a fast phone can't end it early). At zero the page says βendedβ and the checkout refuses, in the same second.
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).
[
{
"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.
- Off by default. The key needs
payouts:write, which FireBuddy switches on per key; the merchant's per-payout and daily limits must be set. Otherwise403withcodepayouts_not_enabledorcaps_not_set. - Approved payees only (
409 payee_not_approved). Add the payee in FireBuddy, approve it in the Fire app, then Sync. idempotency_keyis required. A repeat returns the original payout (200,"duplicate":true), never a second one. If Fire refuses, nothing is kept and the same key can be retried.- Limits:
422 over_payout_cap/over_daily_cap(per key, per UTC day, per currency).422 insufficient_fundsif the Fire account is short.
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.
{
"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.
- Off by default. The key needs
payouts:write, FireBuddy must switch Revolut payouts on for the merchant, Revolut must be connected with "prepare payouts", and a daily limit must be set (payouts_not_enabled,revolut_payouts_not_enabled,revolut_not_connected,revolut_read_only,caps_not_set). idempotency_keyis required. A repeat returns the original run (200,"duplicate":true), never a second draft.- Every payee must exist in Revolut with an account in that currency (
404 counterparty_not_found,409 counterparty_currency_mismatch, withindex). Per-payment limit (when set) and daily limit across this key's runs (422 over_payout_cap/over_daily_cap);422 insufficient_fundsif the Revolut account is short. - Each payment gets a unique reference of at most 18 characters: your text shortened to 12, a space, and a 5-character token (e.g.
CK wages Oct K7Q2M). Keep it β it is what appears on the coach's statement. - If Revolut refuses the draft:
502 revolut_refused, the run is kept asfailed; retry with a new key.502 revolut_uncertain: Revolut did not answer and a draft may exist β check Revolut first.
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).
{
"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.
{
"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).
GET /exports/payments.csvβ every payment, including fee and net columns.GET /exports/payouts.csvβ every payout with payee IBAN/BIC.GET /exports/invoices.csvβ every invoice with subtotal, tax rate, tax amount and total.
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.
{
"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.
feature_flags.allow_compliance_export = false. Disabled tenants receive 403. Merchants on the root platform are allowed by default.
{
"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.
{
"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.
Errors
All errors return JSON of the shape { "error": "human-readable message" } with the HTTP status below.
| Status | Meaning | When |
|---|---|---|
400 | Bad request | Missing or invalid request body field. |
401 | Unauthorized | Missing, malformed, or revoked API key. |
403 | Forbidden | The endpoint is disabled for this merchant's tenant (e.g. compliance bundle). |
404 | Not found | The resource does not exist for this merchant. |
422 | Unprocessable | Merchant has not finished configuring Fire credentials. |
502 | Upstream error | Fire's API rejected the call. Check the response body for the upstream message. |
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.
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 -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.
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")
);
}
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 }:
{
"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"
}
| Event | Fires when |
|---|---|
payment.authorised | The 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.completed | A payment settled (Fire lodgement, or a confirmed mark-paid). This is the money event. |
invoice.paid | An invoice was paid. |
payment.failed | A payment was rejected, cancelled or expired. If was_authorised is true, revoke what you released on payment.authorised for that payment_id. |
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
- In Claude: Settings β Connectors β Add custom connector
- Name it FireBuddy and use the URL
https://firebuddy.info/mcpβ leave the OAuth fields empty - 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.
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
- βCreate a β¬50 payment link for a deposit on the blue BMW and give me the URL.β
- βInvoice Jane Doe β¬120 + VAT for a service call, due Friday, and email it to her with a pay button.β
- βDid invoice INV-2026-0112 get paid yet?β
- βWhatβs my EUR balance, and what were the last five payments in?β
- βGive me a VAT summary for July and August.β
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.
invoice John 250 brake jobβ invoice created (client matched, next number, your usual VAT, emailed if John has an address), branded link back.link 500 deposit on the BMWβ a pay link.offer valet 60 first 5 ends 2 daysβ an offer page with an order limit and a live countdown.paid? INV-2026-0113β the honest status.balanceβ your Fire balances.
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).
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.
- Go to Products & Widgets in your dashboard
- Click New Product and fill in the name, description, price, and icon
- Click Embed next to any product to get the ready-to-paste code snippet
<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.
- Find your key on the API Keys tab (or the Overview "Connect FireBuddy" card) in your dashboard
- If you suspect your key has been leaked, regenerate it from the API Keys tab
- On the monthly plan there is no credit to run out. On an older pay-as-you-go account, running out of credit never deactivates your key β it only pauses creating new payment requests (HTTP 402) until they top up; everything else keeps working
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.
- No credit, no top-ups β creating payment requests and pay links is never paused for billing reasons on the monthly plan.
- Fire and Revolut fees are theirs β each charges its own fee for a payment on the merchant's account, deducted by them. See the merchant's agreement with Fire or Revolut for those rates.
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.