FloorLogik / API OpenAPI file

Version 1.0

Connect a website, or anything else, to FloorLogik

Send website orders and enquiries straight in, keep a website's prices and stock in step with the shop, and have other systems told the moment an order is confirmed, an invoice is paid or the goods are in.

  1. 1

    The account owner makes a key in FloorLogik: Settings, Integrations, API keys. A key is read-only unless it is made with write access, which sending orders, customers and enquiries needs. It is shown once; keep it on the server, never in a web page.

  2. 2

    Send it as a bearer token with every request. This tells you which account it is for:

    curl https://app.floorlogik.com/api/v1/me \
      -H "Authorization: Bearer fl_live_your_key_here"
  3. 3

    Every address below starts with https://app.floorlogik.com/api/v1. Requests and answers are JSON; send Content-Type: application/json with a body.

Conventions

  • Money is in pence, as whole numbers: 4788 is £47.88. Prices are before VAT. A subtotal_pence is before VAT, vat_pence is the VAT, and total_pence is the two together.
  • Dates are 2026-10-09; times are ISO 8601 with the offset, 2026-10-09T14:02:11+01:00.
  • Quantities are in the product's own unit: packs for flooring sold by the pack, square metres for carpet and vinyl. Each product says which in unit.
  • Nothing is left out: a field with no value is null, not missing.
  • Cost prices are never sent. What the shop pays its suppliers stays in FloorLogik.
  • A key sees one account only. Ids from another account answer 404.

Lists and paging

Lists come oldest first, 25 at a time unless per_page asks for up to 100. Follow links.next until it is null. To keep a copy in step, ask with updated_since set to when you last asked, and only what changed comes back.

{
  "data": [ { "id": 41, "name": "Mary Doyle", ... } ],
  "meta": { "page": 1, "per_page": 25, "total": 112, "last_page": 5 },
  "links": { "next": "https://app.floorlogik.com/api/v1/customers?page=2", "prev": null }
}

Errors and limits

Anything that goes wrong comes back the same way, with a code to check and a message to log. Each key may make 120 requests a minute; past that the answer is 429 with a Retry-After header.

{
  "error": {
    "code": "validation_failed",
    "message": "Some of what was sent is not right. See fields.",
    "fields": { "lines.1.sku": ["No product with the SKU KD-XX."] }
  }
}
CodeStatusMeaning
unauthenticated 401 No key, or not a live one. Check the Authorization header.
subscription_required 402 The account's free trial has ended. Its owner can choose a plan in FloorLogik.
account_inactive 403 The account is suspended or closed.
forbidden 403 The key is read-only and this is a write.
not_found 404 Nothing there, or it belongs to another account.
method_not_allowed 405 Wrong method for this address.
validation_failed 422 Something sent is not right. fields says what, by field name.
rate_limited 429 More than 120 requests in a minute. Wait for Retry-After seconds.
server_error 500 Our fault. It is reported to us; try again shortly.

Common jobs

Website orders into FloorLogik
When the website takes an order, send it in with your order number as external_ref, the lines by SKU, prices_include_vat if your prices include VAT, and payment if it was paid at checkout. Retrying with the same external_ref never makes a second order.
Prices and stock on the website
List products with include=stock and updated_since every so often. stock.free is what can be sold: stock set aside for customers' orders is already taken off.
Enquiry forms
Have the form add an enquiry from your server. It appears in Leads straight away.
Order progress back to the website
Add a webhook for order.status_changed, goods.received and invoice.paid, and match them to your orders by data.external_ref.

Account

Check a key

GET /api/v1/me

Which account the key belongs to and what it may do. A good first call: if this works, the key does.

Returns the account and the key.

{
    "data": {
        "account": {
            "id": 12,
            "name": "Oaklands Flooring",
            "currency": "GBP"
        },
        "key": {
            "name": "Website",
            "prefix": "fl_live_8Hq2",
            "abilities": [
                "read",
                "write"
            ]
        }
    }
}

Customers

List customers

GET /api/v1/customers

Every customer, oldest first.

Returns a list of customers.

QueryTypeWhat it does
searchstringMatches name, company, email or postcode.
emailstringExactly this email address.
pageintegerWhich page. Starts at 1.
per_pageintegerHow many a page, up to 100. 25 if not given.
updated_sincedatetimeOnly records changed since then. For keeping a copy in step: remember when you last asked.

Get a customer

GET /api/v1/customers/{id}

Returns the customer.

Add a customer

POST /api/v1/customers needs write access

Returns the customer, with status 201.

BodyTypeWhat it is
type string individual (the default), trade or builder.
first_name required to add string First name. Needed unless company_name is given.
last_name string Last name.
company_name string Company name.
email string Email address.
phone string Phone number.
mobile string Mobile number.
billing_address object line1, line2, line3, city, county, postcode, country. On an update, only the parts sent change.
delivery_address object The same, for deliveries.
curl -X POST https://app.floorlogik.com/api/v1/customers \
  -H "Authorization: Bearer fl_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Mary",
    "last_name": "Doyle",
    "email": "mary@example.com",
    "phone": "0113 496 0000",
    "billing_address": {
        "line1": "14 Park Lane",
        "city": "Leeds",
        "postcode": "LS6 2AB"
    }
}'

Change a customer

PATCH /api/v1/customers/{id} needs write access

Only the fields sent change.

Returns the customer.

BodyTypeWhat it is
type string individual (the default), trade or builder.
first_name string First name. Needed unless company_name is given.
last_name string Last name.
company_name string Company name.
email string Email address.
phone string Phone number.
mobile string Mobile number.
billing_address object line1, line2, line3, city, county, postcode, country. On an update, only the parts sent change.
delivery_address object The same, for deliveries.

Products

List products

GET /api/v1/products

The catalogue. Fitting rates are left out, and so are cost prices: neither is anything a website should show.

Returns a list of products.

QueryTypeWhat it does
searchstringMatches name, SKU or supplier code.
skustringExactly this SKU or supplier code.
category_idintegerIn this category, subcategories included.
supplier_idintegerFrom this supplier.
activebooleantrue for products still sold, false for ones that are not.
includestringstock, to include what is in stock.
pageintegerWhich page. Starts at 1.
per_pageintegerHow many a page, up to 100. 25 if not given.
updated_sincedatetimeOnly records changed since then. For keeping a copy in step: remember when you last asked.

Get a product

GET /api/v1/products/{id}

With its stock.

Returns the product.

Orders

List orders

GET /api/v1/orders

Returns a list of orders.

QueryTypeWhat it does
statusstringOnly orders with this status.
customer_idintegerOnly this customer's.
external_refstringThe order you sent in with this reference.
sourcestringapi for orders sent in through the API.
pageintegerWhich page. Starts at 1.
per_pageintegerHow many a page, up to 100. 25 if not given.
updated_sincedatetimeOnly records changed since then. For keeping a copy in step: remember when you last asked.

Get an order

GET /api/v1/orders/{id}

Returns the order.

Send in an order

POST /api/v1/orders needs write access

An order taken somewhere else, usually a website. It arrives as pending, the same as one raised in FloorLogik, and the shop picks, dispatches and invoices it as normal. Every line is checked before anything is saved: if one is wrong, nothing is made. Send external_ref and a retry is safe: the same reference again returns the order already made, with status 200 rather than 201.

Returns the order, with status 201.

BodyTypeWhat it is
external_ref string Your order number. Unique per account.
customer_id this or the other integer An existing customer.
customer this or the other object Or the customer's details, as for Add a customer. Matched to an existing customer by email, so somebody ordering twice is one customer.
lines[] required array What was ordered. Up to 200.
lines[].product_id integer The product.
lines[].sku string Or its SKU (or supplier code).
lines[].quantity required number How many, in the product's unit: packs for a pack product, m2 for most others. See unit on a product.
lines[].unit_price_pence pence Price per unit. If left out, what this customer pays: their price list, or their pricing tier.
lines[].vat_rate integer 0, 5 or 20. The product's rate if left out; 20 for a line with no product.
lines[].description string For a line with no product, such as a delivery charge. Needs unit_price_pence too. Refused if the shop has typed lines switched off.
lines[].unit string Only to override the product's own unit.
lines[].room string Which room it is for.
prices_include_vat boolean true if the prices sent include VAT, as most websites' do. They are taken back to ex VAT, which can move a total by a penny.
delivery_address object name, line1, line2, city, county, postcode. If left out, the customer's delivery address, or failing that their billing address.
delivery_notes string For the driver.
delivery_date date Puts the delivery in the diary.
notes string Notes on the order.
payment object Money already taken, such as at a website checkout: amount_pence, and method (card, cash, bank_transfer, cheque, bacs or stripe; card if left out). Raises the invoice and records the payment, so the order shows as paid.
curl -X POST https://app.floorlogik.com/api/v1/orders \
  -H "Authorization: Bearer fl_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "external_ref": "WC-10442",
    "customer": {
        "first_name": "Mary",
        "last_name": "Doyle",
        "email": "mary@example.com"
    },
    "prices_include_vat": true,
    "lines": [
        {
            "sku": "KD-SE-NATOAK",
            "quantity": 6,
            "unit_price_pence": 4788
        },
        {
            "description": "Delivery",
            "quantity": 1,
            "unit_price_pence": 1500
        }
    ],
    "payment": {
        "amount_pence": 30228,
        "method": "card"
    }
}'

Quotes

List quotes

GET /api/v1/quotes

Returns a list of quotes.

QueryTypeWhat it does
statusstringOnly quotes with this status.
customer_idintegerOnly this customer's.
pageintegerWhich page. Starts at 1.
per_pageintegerHow many a page, up to 100. 25 if not given.
updated_sincedatetimeOnly records changed since then. For keeping a copy in step: remember when you last asked.

Get a quote

GET /api/v1/quotes/{id}

Returns the quote.

Invoices

List invoices

GET /api/v1/invoices

Returns a list of invoices.

QueryTypeWhat it does
statusstringOnly invoices with this status.
customer_idintegerOnly this customer's.
order_idintegerOnly this order's.
pageintegerWhich page. Starts at 1.
per_pageintegerHow many a page, up to 100. 25 if not given.
updated_sincedatetimeOnly records changed since then. For keeping a copy in step: remember when you last asked.

Get an invoice

GET /api/v1/invoices/{id}

Returns the invoice.

Leads

List leads

GET /api/v1/leads

Returns a list of leads.

QueryTypeWhat it does
statusstringOnly leads with this status.
pageintegerWhich page. Starts at 1.
per_pageintegerHow many a page, up to 100. 25 if not given.
updated_sincedatetimeOnly records changed since then. For keeping a copy in step: remember when you last asked.

Get a lead

GET /api/v1/leads/{id}

Returns the lead.

Add an enquiry

POST /api/v1/leads needs write access

From a contact form, a sample request or a "book a measure" button. It lands in Leads as new, the same as one typed in. Samples asked for go on the shop's list of samples to send, so it can see what is going out and what turned into an order.

Returns the lead, with status 201.

BodyTypeWhat it is
first_name required string First name.
last_name string Last name.
email string Email address.
phone string Phone number.
address object line1, line2, line3, city, county, postcode.
source string Where it came from. website if left out.
customer_type string retail or trade.
product_interest[] array carpet, vinyl, laminate, hardwood, tile or other.
area_m2 number Roughly how much floor.
rooms integer How many rooms.
notes string Their message.
samples[] array Samples they asked for. They go on the shop's "to send" list and the noticeboard.
samples[].product_id integer The product.
samples[].sku string Or its SKU (or supplier code).
samples[].description string Or what it is, in words, for something not in the catalogue.
samples[].quantity integer How many. 1 if left out.
curl -X POST https://app.floorlogik.com/api/v1/leads \
  -H "Authorization: Bearer fl_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Sam",
    "email": "sam@example.com",
    "phone": "07700 900123",
    "product_interest": [
        "carpet"
    ],
    "rooms": 3,
    "notes": "Stairs and landing, after a measure next week.",
    "samples": [
        {
            "sku": "CORMAR-APOLLO-OAT",
            "quantity": 1
        },
        {
            "description": "Grey stair runner swatch"
        }
    ],
    "address": {
        "postcode": "LS8 1AA"
    }
}'

Diary

List jobs

GET /api/v1/jobs

Measures, fittings and deliveries, in date order.

Returns a list of job (in the diary).

QueryTypeWhat it does
typestringOnly this type, such as installation.
statusstringOnly this status.
order_idintegerOnly this order's.
fromdateOn or after this day.
todateOn or before this day.
pageintegerWhich page. Starts at 1.
per_pageintegerHow many a page, up to 100. 25 if not given.
updated_sincedatetimeOnly records changed since then. For keeping a copy in step: remember when you last asked.

Records

Each record looks the same wherever it appears: in a list, on its own, or in a webhook.

Customer

FieldTypeWhat it is
id integer The customer's id.
type string individual, trade or builder.
name string The name to show: the company name for a business, otherwise first and last name.
first_name string First name.
last_name string Last name.
company_name string Company name, for a business.
email string Email address.
phone string Phone number.
mobile string Mobile number.
billing_address object Where invoices go. line1, line2, line3, city, county, postcode, country
delivery_address object Where goods go, when different. line1, line2, line3, city, county, postcode, country
pricing_tier integer Which price the customer pays: 1 (standard), 2 or 3. See price_pence on a product.
created_at datetime When the customer was added.
updated_at datetime When the customer was last changed.

Product

FieldTypeWhat it is
id integer The product's id.
sku string The shop's code for it. Orders can name products by this.
supplier_ref string The supplier's code for it.
name string Name.
description string Description.
type string roll, cut, pack, make_to_order or accessory.
unit string What a quantity of it is counted in on an order: pack, m2, m, item, box...
category object Its category. A subcategory is named with its parent, as "Wood › Herringbone". id, name
supplier object Who supplies it. id, name
colour string Colour.
width_mm integer Board, tile or roll width in millimetres.
thickness_mm number Thickness in millimetres.
length object Board or tile length. text, mm, min_mm, max_mm
pack_coverage_m2 number Square metres in a pack, for pack products.
price_pence pence Standard selling price per unit, ex VAT.
price_tier2_pence pence Price for tier 2 customers, ex VAT. Null means the standard price.
price_tier3_pence pence Price for tier 3 customers, ex VAT. Null means the tier 2 price.
vat_rate integer VAT rate in percent: 0, 5 or 20.
is_active boolean False for a product the shop no longer sells.
stock object What is in stock. Only with include=stock on a list; always on a single product. Null for things not stocked by the roll or pack. See stock.
updated_at datetime When the product was last changed, price included.

Stock

FieldTypeWhat it is
unit string m2 for rolls, pack for packs.
in_stock number Everything booked in and on the shelf.
allocated number Of that, how much is set aside for customers' orders.
free number What is free to sell: in_stock less allocated.
rolls integer Rolls only: how many rolls.
metres number Rolls only: metres left across them.

Order

FieldTypeWhat it is
id integer The order's id.
ref string The shop's order number, such as ORD-2026-0042.
external_ref string Your reference, when the order came in through the API.
source string api for an order sent in through the API; null for one raised in FloorLogik.
status string pending, confirmed, goods_received (everything ordered from suppliers is in), completed or cancelled.
payment_status string unpaid, deposit_paid (something paid, not all) or paid.
customer object Who the order is for. id, name
quote_id integer The quote it came from, if any.
subtotal_pence pence Total before VAT.
vat_pence pence VAT.
total_pence pence Total including VAT.
paid_pence pence Paid so far, a measure fee taken off it included.
balance_due_pence pence Still to pay.
deposit_pct integer Deposit asked for, in percent.
fitting_type string supply_only, supply_and_install or supply_third_party (fitted by somebody else).
delivery_date date When it is to be delivered, if booked.
dispatched boolean Whether it has been picked and sent out.
delivery_address object Where it goes. name, line1, line2, city, county, postcode, notes
notes string Notes on the order.
goods object Where the goods ordered from suppliers for it have got to. Null when nothing has been ordered. state, label
lines[] array What is on it. See line (on an order or a quote).
created_at datetime When the order was raised.
updated_at datetime When the order was last changed.

Line (on an order or a quote)

FieldTypeWhat it is
id integer The line's id.
type string product for goods; labour for fitting; discount.
product_id integer The product, if it is one.
description string What it says on the paperwork.
quantity number How many, in unit.
unit string pack, m2, m, item...
unit_label string The unit as a customer reads it: "packs", "m²".
unit_price_pence pence Price per unit, ex VAT.
vat_rate integer VAT rate in percent.
total_pence pence The line including VAT.
room string The room it is for.
area_m2 number The room's area, where measured.
cuts array Carpet only: the cuts off the roll, each with its width and length.

Quote

FieldTypeWhat it is
id integer The quote's id.
ref string The shop's quote number.
status string draft, sent, approved (accepted by the customer online), declined or converted (made into an order).
customer object Who it is for. id, name
subtotal_pence pence Total before VAT.
vat_pence pence VAT.
total_pence pence Total including VAT.
valid_until date When it runs out.
sent_at datetime When it was sent.
accepted_at datetime When the customer accepted it online.
lines[] array What is on it. See line (on an order or a quote).
created_at datetime When it was made.
updated_at datetime When it was last changed.

Invoice

FieldTypeWhat it is
id integer The invoice's id.
ref string The invoice number.
type string deposit, balance, full or measure (a measure fee).
status string draft, sent, overdue, paid or voided.
customer object Who it is to. id, name
order object The order it is for. Null for a measure fee. id, ref
subtotal_pence pence Before VAT.
vat_pence pence VAT.
total_pence pence Including VAT.
paid_pence pence Paid so far.
due_pence pence Still owed.
due_date date When it is due.
issued_at datetime When it was issued.
paid_at datetime When it was paid in full.
pay_url string A page where the customer can pay it by card, when the shop takes card payments and something is owed.
created_at datetime When it was raised.

Lead

FieldTypeWhat it is
id integer The lead's id.
status string new, contacted, sample_sent, measure_booked, quoted, won or lost.
source string Where it came from, such as website, phone, walk_in, referral, checkatrade or trade_show.
first_name string First name.
last_name string Last name.
email string Email address.
phone string Phone number.
address object Their address. line1, line2, line3, city, county, postcode
customer_type string retail or trade.
product_interest[] array What they are interested in: carpet, vinyl, laminate, hardwood, tile or other.
area_m2 number Roughly how much floor.
rooms integer How many rooms.
notes string Anything else they said.
customer_id integer The customer they became, once converted.
samples[] array Samples asked for or sent: id, product_id, description, quantity, status (requested, sent, follow_up_due or followed_up), requested_at, sent_at.
created_at datetime When the enquiry came in.

Job (in the diary)

FieldTypeWhat it is
id integer The job's id.
ref string The job number.
type string measure, installation, delivery, meeting, callback, survey or collection.
status string scheduled, in_progress, completed, cancelled, no_access or rescheduled.
customer object Who it is for. id, name
order object The order it is for, if any. id, ref
scheduled_date date The day.
end_date date The last day, for a job over several.
start_time string Start time, HH:MM.
end_time string End time, HH:MM.
day_part string am or pm, when booked by part of the day instead of a time.
address object Where. line1, city, postcode
completed_at datetime When it was marked done.
created_at datetime When it was booked.

Webhooks

A webhook tells another system when something happens, by posting JSON to a web address of its choosing. The account owner adds one in Settings, Integrations, Webhooks, with the events it wants, and is given a signing secret for it. The address must start with https://.

Each event is sent once the change behind it is complete, usually within a minute, and data is the record as the API describes it. An event fires however the change was made: in FloorLogik, through the API, or by a customer paying online.

EventWhen
customer.createdA customer is added, in the app or through the API.
lead.createdAn enquiry is added.
quote.acceptedA customer accepts a quote: approved from its link, or turned into an order by the shop. Sent once per quote.
order.createdAn order is raised: from a quote, in the app, or through the API.
order.status_changedAn order moves on, for example from pending to confirmed. Says what it was before, as previous_status.
invoice.createdAn invoice is raised.
invoice.paidAn invoice is paid in full.
job.bookedA measure, fitting or delivery is put in the diary.
job.completedA job in the diary is marked done.
goods.receivedEverything ordered from suppliers for an order has been booked in.
{
  "id": "0b6c1d2e-6f1a-4c55-9a43-6c8e2f0d9a11",
  "event": "order.status_changed",
  "created_at": "2026-10-09T14:02:11+01:00",
  "data": { "id": 1042, "ref": "ORD-2026-0042", "status": "confirmed", ... },
  "previous_status": "pending"
}

Headers

  • FloorLogik-Event: the event, such as order.created.
  • FloorLogik-Delivery: the event's id, the same as id in the body. A retry carries the same id, so use it to ignore one you have already handled.
  • FloorLogik-Signature: t=<unix time>,v1=<signature>, explained next.

Checking it came from us

The signature is a hex HMAC-SHA256 of the time, a full stop, and the body exactly as sent, keyed with the webhook's secret. Work it out the same way and compare. Refuse anything more than five minutes old, so a captured request cannot be replayed.

PHP

$body   = file_get_contents('php://input');
$header = $_SERVER['HTTP_FLOORLOGIK_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $sig);   // t=..., v1=...

$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, $secret);
$fresh    = abs(time() - (int) ($sig['t'] ?? 0)) <= 300;

if (! $fresh || ! hash_equals($expected, $sig['v1'] ?? '')) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);   // then answer 200 quickly

Node.js

const crypto = require('crypto');

// rawBody must be the body exactly as received, before any JSON parsing.
function verify(rawBody, header, secret) {
  const sig = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const expected = crypto.createHmac('sha256', secret)
    .update(`${sig.t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) <= 300;
  return fresh && sig.v1?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1));
}

Answering, and retries

Answer with any 2xx status within 10 seconds, and do any slow work afterwards. Anything else, or no answer, is tried again: 6 tries in all, after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours. Redirects are not followed.

A webhook that fails 25 events in a row is switched off, and shows as off in Settings. Switch it back on there once the receiving end is fixed. Settings also shows the last few deliveries and can send a test.