Connecting your ERP

For the person who has to make your ERP talk to the app.

There is no server in between and no API. The app reads and writes plain files in a place you control: a folder on the tablet, or your own FTP/SFTP server. Orders can also be sent to Google Drive, but Drive is orders out only: it cannot supply a catalogue, customers or prices. If your ERP can write a CSV to a folder on a schedule, the integration is done.

Setting the tablet end up, step by step: a folder on the tablet, an FTP or SFTP server, Google Drive for orders. The shape of each file format, with samples, is in CSV, JSON and XML files.

Read this alongside Import field reference, which lists every column the app recognises. That file is generated from the app’s own import profiles, so it cannot drift out of date; this one explains how the pieces fit together.


1. The folder layout

The app creates this structure the first time it connects. On a server it goes in a folder called SelenicOrders; on a tablet it goes straight into the folder you chose:

FolderDirectionWhat goes in it
InboundYou writeCatalogue and customer files for the app to read
ImagesYou writeProduct and category images
ConfigBothApp configuration, shared between devices
OutboundApp writesOrders and new customers, for you to collect
LogsApp writesImport results

You only need to write to Inbound and Images, and read from Outbound.


2. Naming your inbound files

This is the part that catches people out. The app decides what a file contains by its name, not its contents. A file is read as an entity when its name is that entity’s name, or begins with it followed by _ or -. Case does not matter:

File nameImported as
ProductsProducts
CustomersCustomers
CategoriesCategories
PromotionsPromotions
DeliveryMethods (or Delivery_Methods, Delivery-Methods)Delivery methods (delivery_methods)
AnnouncementsAnnouncements
UsersUsers

So these all work:

PLAINTEXT
Products.csv
Products_2026-08-09.csv
Customers-export.xml
DeliveryMethods.csv
Users.csv

And these are skipped, because their names are not one of the above: Stock_Report.csv, ItemMaster.csv, Customer_Discounts.csv, Product.csv (the singular), Productsheet.csv (no separator). A skipped file is named in the sync log, so it is never lost in silence. Rename it and sync again.

Changed 15 October 2026. Until then a keyword anywhere in the name was enough, which filed Stock_Report.csv as users (it contains “rep”) and Customer_Discounts.csv as customers. Exact names cannot be misread.

Accepted extensions: .csv, .txt, .json, .xml. Files beginning _SAMPLE_ are ignored, so the examples the app writes for you are never imported by accident.


3. What the columns must be called

See Import field reference for the full list: every column across the seven entities, each with the name to use and the format expected. It is generated from the app’s own import profiles, so it is never out of date.

Three rules that apply everywhere:

  • Column names are matched case-insensitively and ignoring surrounding spaces. ItemCode, itemcode and ItemCode are the same column.
  • Columns you do not send are left alone. A partial file updates what it mentions and leaves the rest of the record as it was.
  • Extra columns are ignored. You can send your whole export and let the app take what it recognises.

If your ERP cannot produce these column names, you do not have to change it. The app’s import profiles are editable in Settings, so the mapping can be changed at the app end instead: see Import profiles and field mapping.

Note the names differ by format. A product is ItemCode in CSV, sku in JSON and SKU inside a <Product> element in XML. (The field reference writes that as Item/SKU; the record element itself must be called <Product>. See the XML section.) The field reference lists all three per entity; start from the format your system actually exports rather than from the CSV table.

And if the shape is wrong rather than the names, that is fixable at the app end too. Settings › Data Integration › Import Profiles › Reshape Feed takes a short template that rewrites your rows before the mapping reads them: for a price list whose name and value are in two different columns, say, or one row that has to become two products. Validate shows you what it produces before you save it. Most integrations never need it; it is there so that an awkward export is a ten-minute job at this end rather than a change request at yours.

Numbers and money

You do not need to normalise your numbers. Send them the way your system writes them.

  • Currency symbols are stripped. £1,299.00, $1299, €1.299,00 and 1299 are all read as the same amount. £ $ € ¥ ₹ are all recognised.

  • Digit grouping is ignored, whether you group with commas, dots, or spaces, including the non-breaking and narrow spaces French and German systems emit.

  • Which mark is the decimal is worked out from the value itself, because it varies by the system that wrote the file rather than by anything the app can be told in advance:

    You sendRead asWhy
    1.299,001299.00Both marks present; the later one is the decimal
    1,299.001299.00Same rule, the other way round
    1,2991299A lone comma with exactly three digits after it groups
    0,4250.425Unless the number starts with a nought: nobody writes a thousand as 0,425
    1,501.50A lone comma with anything else after it is a decimal
    1.501.50A lone dot is always the decimal point

    The last row is the one deliberate ambiguity. 1.250 in a German export means one thousand two hundred and fifty; here it reads as one and a quarter. Reading it the other way would multiply a price by a thousand on every feed that uses a dot normally, so the dot stays the decimal point. If your system groups with dots, send a decimal comma too (1.250,00), and both are read correctly.

  • Negatives are accepted in all three conventions: -12.50, 12.50-, and (12.50).

  • A decimal in a whole-number field is rounded, not truncated. A stock quantity of 12.5 becomes 13 and 12.4 becomes 12. It is worth knowing that this is rounding rather than an error, because it is silent. If your system can send fractional stock, decide deliberately whether rounding up is what you want a rep to see as available.

A value that is present but unreadable fails the row and is reported in the error log, rather than being read as zero. A blank is different: blank means “not supplied”, so the record keeps whatever it already had.

There is one exception where blank also fails: if your import profile maps a list price column and the cell is empty, the row is rejected. The feed said it would send a price and did not, and importing at zero would put a sellable free product in a rep’s catalogue. If your catalogue genuinely has no list prices (everything priced through contracted price lists), then map no price column at all, and nothing is rejected.

Contracted prices and quantity breaks

PriceLists and QtyBreaks are the two fields the pricing waterfall stands on: a price list is what a customer on a contract actually pays, and a quantity break is what they pay for a pallet. Both are a map of name to amount, and each format carries it differently:

Product page showing a customer’s contracted price against the list price, stock and key features
A product on a contracted customer’s price list: their price, the list price it replaces, and stock.
FormatHow to send it
CSVAny of the three shapes below
JSONA real object under prices / qty_breaks
XMLChild elements under Prices / QtyBreaks, e.g. <Prices><TRADE>8.50</TRADE></Prices>

CSV takes whichever shape your system already exports, because most systems store price tiers in their own table and export them one row or one column at a time. You should not have to reshape your file:

  • Everything in one cell. {"TRADE": 8.50, "WHOLESALE": 7.25}, or the shorter TRADE:8.50|WHOLESALE:7.25.
  • A column per price list. Name them all in the profile’s PriceLists mapping, separated by |: Trade|Wholesale|Pallet reads those three columns and uses the names you typed as the price list names.
  • A row per price list. Repeat the item code on each row with one price list on it. Rows sharing an item code are combined into a single product; every other column takes its value from the last row.

A cell with something unreadable in it stops that row importing and is reported. That is deliberate: a product arriving with no contract prices looks perfectly healthy, and the first symptom would be a customer invoiced at list price weeks later.

Quantity breaks are keyed by the quantity at which the price starts applying, so {"1": 9.00, "12": 8.50, "48": 7.75} means 48 or more cost 7.75 each.

A price list’s key must match the PriceList column on the customer for that customer to get it. Send TRADE on the customer and {"TRADE": 8.50} on the product. The keys are yours to choose; the app does not interpret them, it only matches them.

A customer whose PriceList names a list the product does not carry falls through to the ordinary price rather than failing, so a partial rollout of a new price list is safe.

Send nothing for these fields and the product keeps the price lists it already had. Send an empty object and it is treated as “this product has none”.

Targeting a promotion

AppliesTo on a promotion is a pipe-separated list of customer tags: WHOLESALE|KEY_ACCOUNT. A customer gets the promotion if they carry any one of them.

Blank means everybody, which is what every promotion meant before the column existed, so adding the column does not change existing behaviour.

A tag cannot contain a |, because that is what separates them. On this column and on the customer’s own Tags column, which are read the same way. Send KEY|ACCOUNT as one tag and you get two, on both sides. Surrounding spaces are trimmed and empty entries dropped, so VIP | TRADE and VIP|TRADE mean the same thing.

Targeting by tag rather than by customer id is deliberate: your system already maintains tags, so the list of who gets a promotion can change without the promotion being rewritten.

Delivery lead times and cut-offs

LeadTimeDays and CutOffTime on a delivery method are what let the app tell a rep when an order will arrive rather than only what carriage costs.

Checkout with the delivery method, estimated delivery date, credit available and totals
At checkout the delivery method’s charge and estimated date come from these two columns; credit comes from the customer file.
  • LeadTimeDays counts working days, so weekends are skipped.
  • CutOffTime is the time of day after which the clock starts tomorrow. Send 15:00 and an order placed at 15:30 is treated as the next working day’s.
  • Leave either blank and the app shows no estimated date rather than guessing one.

Dates

Dates are read in this order:

  1. ISO 8601: 2026-08-09, 20260809 or 2026-08-09T14:30:00. Always accepted, whatever the app is configured to display. Use this if you can.
  2. Numbers in the order the app’s date format puts them: day first for dd/MM/yyyy, month first for MM/dd/yyyy, separated by /, - or .. A two-digit year is this century: 09/08/26 is 2026.
  3. A month by name: 9-Aug-2026, 9 August 2026, Aug 9, 2026.

A time after the date is ignored (09/08/2026 00:00:00 is the 9th), and so are “no date” placeholders: 00/00/0000, 0000-00-00 and 01/01/0001.

A date the app cannot read is an error, not a blank. The row is skipped, the sync log names the column and the value, and the record keeps whatever it had before. An impossible date (31/02/2026, or 09/30/2026 under a day-first format) is refused the same way rather than rolled into another month.

31/12/2026 and 12/31/2026 are genuinely ambiguous to any reader. ISO removes the guesswork.

Yes and no

A yes-or-no column (IsOnSale, IsActive, AllowBackorder, FreeDelivery and the rest) reads the same in every format, ignoring case and spaces:

  • Yes: true, t, yes, y, 1, -1, x, on, active, enabled
  • No: false, f, no, n, 0, off, inactive, disabled
  • Blank means you did not say, and the record keeps what it had.

Anything else is an error: the row is skipped and the sync log names the column and the value.

Tax codes

  • A product’s TaxCode sets its rate. These are known without any setup: T1 (20%), T5 (5%), T0 (zero-rated), T2 (exempt), plus EXEMPT and EXPORT. Other codes and rates are added in the app under Business Rules › Advanced Tax Setup.
  • A customer’s TaxCode can only reduce tax. It is for an exempt body, a charity’s zero rate or an export account, and applies when it charges less than the product’s own code would.
  • A code the app has no rate for is charged the default rate, and the sync log says how many products or customers carry it so it can be added.
  • Tax follows the delivery address, not the billing one. A customer delivered to another country, with a VAT or EORI number on file, is zero-rated.
  • The delivery charge is taxed as the goods are, when the delivery method has no TaxCode of its own: nothing on delivering zero-rated goods, the standard rate on standard-rated ones, and shared by value on a mix. Give the method a code to fix its rate instead.

Dated windows

PriceValidFrom / PriceValidTo on products, and ValidFrom / ValidUntil on promotions and announcements, all behave the same way:

  • Blank means unbounded. No end date means it runs until you turn the flag off.
  • Both ends include the whole day named. PriceValidTo of 2026-08-09 means the customer still gets that price on the 9th.
  • A window narrows a flag; it never switches one on. A sale window with IsOnSale false stays off. The app will not start discounting something you have not flagged.

Balances and credit limits

Send each customer’s CreditBalance as your system has it. The tablet adds to it the orders it has taken that your figure cannot include yet, so a rep cannot pass a credit limit by syncing between orders:

  • Orders not yet sent to you always count.
  • An order already sent counts until a customers file you wrote after receiving it arrives. That is right when your balance includes open orders, which most systems’ do. The time is taken from the file’s modified date on your server, so keep it truthful.
  • If your balance only includes orders once they are invoiced, set Business Rules › Sent orders count against credit to the number of days you usually take to invoice. Sent orders then count for that long.

A CreditLimit of 0 means no limit by default. If your system sends 0 for customers who must pay up front, turn on Business Rules › A credit limit of 0 means no credit, and the tablet refuses their account orders. A customer whose row leaves CreditLimit blank, or a file without the column, has no limit either way.

Balances only ever travel from you to the tablet. Taking an order does not send the customer back to you; only a change a rep makes to the customer does.

Customers billed in a currency other than the catalogue’s are not given fixed-amount promotions or a delivery charge on the tablet; those are in the catalogue’s currency. Percentage promotions still apply. Add carriage to their orders in your system.


4. Images

Put image files in Images. The app matches an image to a product by its filename without the extension, against the product’s SKU:

PLAINTEXT
Images/WH001.jpg         ->  product with SKU WH001
Images/CAT_SPIRITS.png    ->  category with ID CAT_SPIRITS

For several images of one product, add a separator and a suffix. The separator is configurable in Settings and defaults to -:

PLAINTEXT
Images/WH001-1.jpg
Images/WH001-2.jpg

The whole name is tried first, so ABC-123.jpg is the picture of the SKU ABC-123 even with the default separator. Only if no product or category has that name is everything after the last separator discarded, so WH-001-2.jpg matches the SKU WH-001. The sync log names the product or category each image went to, and any image that matched nothing.

Accepted: .jpg, .jpeg, .png, .webp, .gif.

What images cost

Measured, not estimated. Each image is converted to two WebP files: a 300px thumbnail for the grid and a 1000px version for the product page:

Per imageabout 60 KB on disk, 150 ms to process
A 5,000-product catalogueabout 297 MB and 12 minutes

That twelve minutes is a one-off on the first sync, and it happens on the tablet. Later syncs fetch and convert only images that are new or changed, judged by each file’s size and modified time. A server that does not report both (some plain-FTP servers) has every image fetched each time, because a replaced photograph must never be mistaken for the old one. Tell reps to do the first sync somewhere with power and a good connection, not in a car park before a call.

Images are never deleted, because one image can belong to several products. Replacing an image means writing a new file over the old name.


5. What comes back out

Files appear in Outbound named like this:

PLAINTEXT
orders_export_20260809_143207_T3F9.csv
customers_export_20260809_143207_T3F9.csv

The timestamp is the moment of export, so files sort in chronological order. The four characters at the end say which tablet sent the file. Each tablet keeps its own code for good, so two tablets sharing an Outbound folder never write the same name. If your import watches for a pattern, match orders_export_*.csv, which covers every tablet.

Order numbers carry the same code. An order from that tablet is ORD-T3F9-0001: the Order Prefix set in Business Rules, the tablet’s code, then a number that counts up on that tablet and never starts again, even if the tablet is reset. So two tablets never send the same order number, and the code tells you which tablet took an order. The app calls it the Tablet ID, under Settings › About & help › About.

A file may hold one order or many. A sync sends everything still pending; a rep can also send a single order on its own the moment it is taken, without waiting for the next sync. Both produce the same file in the same place through the same template, so there is nothing to tell apart at your end, but do not assume one file is one day’s trading.

The format and content are entirely up to you: exports are Liquid templates, editable in Settings, so you can produce whatever shape your ERP already imports rather than writing a converter. CSV, JSON and XML defaults are supplied.

Two things to build on your side.

Delete or move files once you have taken them. The app never removes anything from Outbound.

Expect the occasional duplicate, and never a missing order. An order is only marked as sent once the file has been written successfully. If a sync is interrupted, the order stays pending and goes out again next time. That is deliberate: a duplicate you can spot is better than an order that vanished. Make your import idempotent on the order id.

New customers created on a tablet

A rep can add a customer while offline. Because your ERP has not seen it yet, it arrives with a temporary account id, and with everything the rep entered: contact details, tax numbers and the billing address, plus any delivery addresses in the JSON and XML formats:

Customer profile with contacts, credit used against the limit and tax status
A customer as the rep sees it. Balances and credit limits come only from your customer file.
PLAINTEXT
NEW-3F2A8B1C

Create the account as normal, then echo that temporary id back in the OriginalAccountId column of your next customer export, alongside the real account id you assigned. The app uses it to replace the placeholder with the real record and move any orders across.

If you do not send OriginalAccountId, nothing breaks, but the tablet keeps both the placeholder and the real account, and the rep sees the same business twice.


6. When something goes wrong

The app never deletes data it could not read. A file that fails to import leaves the existing records exactly as they were, and the sync log names the file and the reason.

What happens to a file once it is read: nothing. The app never moves or deletes a file in Inbound, because every tablet reads the same folder. Each tablet keeps its own record of which files it has read, and reads only what is new or changed to it. So Inbound is also the history of what every tablet has been given; tidy it yourself whenever you like, once every tablet has synced.

A file is new to a tablet if its name is, or if its contents have changed. Overwriting Products.csv each night is read every night; leaving yesterday’s file untouched is not read again. A tablet set up later reads everything still in Inbound, oldest first, so it ends up where the others are.

A file with any failed row is tried again next sync, so fix and replace it rather than renaming it, and the sync log says why it was tried again. Once a newer file of the same kind has been read, an older one is not imported again, so yesterday’s file can never put yesterday’s prices back over today’s; the log names the newer file that replaced it. This uses the times the folder listing gives. A plain FTP server that lists no times leaves the app nothing to compare, and the older file is tried again as before. A file that imported nothing but had no errors counts as read: a nightly delta with no changes in it is a header row and nothing else, which is a successful sync of an empty file, not a failure.

Write each file under a temporary name, then rename it. The app may sync while your system is still writing. It looks at each file twice, a few seconds apart, and leaves one that is still changing for the next sync, but a pause in your export longer than that can still look finished. A name the app does not pick up (Products.csv.tmp), renamed to Products.csv once complete means it only ever sees a finished file. If you cannot do that, end every file with a line break: a CSV that stops partway through its last row, with no line break after it, is refused whole and read again next sync, rather than imported with half a row.

Common causes, in the order they actually happen:

SymptomCause
“Skipped file - No active profile found”The filename is not one of the names in section 2
Rows imported but fields emptyColumn names do not match; check the field reference
Every row rejectedUsually a header row that has been renamed or removed
Dates a day or a month outAmbiguous dd/mm vs mm/dd; switch to ISO
“could not read … as a date”A date in no shape above; the row was skipped and kept what it had
Images not appearingFilename does not match the SKU exactly, including case; the sync log lists images that matched nothing

7. A minimal first integration

You do not need all of this on day one. In order of value:

  1. Products_*.csv: ItemCode, Description, PriceRRP, StockQty. That alone gives reps a working catalogue.
  2. Customers_*.csv: AccountId, CompanyName, plus the billing address columns. Now they can take orders.
  3. Collect Outbound. Now the orders reach you.
  4. Images. The single biggest difference to how the app feels in front of a buyer.
  5. Everything else (price lists, promotions, tax rules, quantity breaks) as and when.