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:
| Folder | Direction | What goes in it |
|---|---|---|
Inbound | You write | Catalogue and customer files for the app to read |
Images | You write | Product and category images |
Config | Both | App configuration, shared between devices |
Outbound | App writes | Orders and new customers, for you to collect |
Logs | App writes | Import 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 name | Imported as |
|---|---|
Products | Products |
Customers | Customers |
Categories | Categories |
Promotions | Promotions |
DeliveryMethods (or Delivery_Methods, Delivery-Methods) | Delivery methods (delivery_methods) |
Announcements | Announcements |
Users | Users |
So these all work:
Products.csv
Products_2026-08-09.csv
Customers-export.xml
DeliveryMethods.csv
Users.csvAnd 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,itemcodeandItemCodeare 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,00and1299are 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 send Read as Why 1.299,001299.00 Both marks present; the later one is the decimal 1,299.001299.00 Same rule, the other way round 1,2991299 A lone comma with exactly three digits after it groups 0,4250.425 Unless the number starts with a nought: nobody writes a thousand as 0,4251,501.50 A lone comma with anything else after it is a decimal 1.501.50 A lone dot is always the decimal point The last row is the one deliberate ambiguity.
1.250in 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.5becomes13and12.4becomes12. 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:
| Format | How to send it |
|---|---|
| CSV | Any of the three shapes below |
| JSON | A real object under prices / qty_breaks |
| XML | Child 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 shorterTRADE:8.50|WHOLESALE:7.25. - A column per price list. Name them all in the profile’s
PriceListsmapping, separated by|:Trade|Wholesale|Palletreads 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.
LeadTimeDayscounts working days, so weekends are skipped.CutOffTimeis the time of day after which the clock starts tomorrow. Send15:00and 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:
- ISO 8601:
2026-08-09,20260809or2026-08-09T14:30:00. Always accepted, whatever the app is configured to display. Use this if you can. - Numbers in the order the app’s date format puts them: day first for
dd/MM/yyyy, month first forMM/dd/yyyy, separated by/,-or.. A two-digit year is this century:09/08/26is 2026. - 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
TaxCodesets its rate. These are known without any setup:T1(20%),T5(5%),T0(zero-rated),T2(exempt), plusEXEMPTandEXPORT. Other codes and rates are added in the app under Business Rules › Advanced Tax Setup. - A customer’s
TaxCodecan 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
TaxCodeof 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.
PriceValidToof2026-08-09means the customer still gets that price on the 9th. - A window narrows a flag; it never switches one on. A sale window with
IsOnSalefalse 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:
Images/WH001.jpg -> product with SKU WH001
Images/CAT_SPIRITS.png -> category with ID CAT_SPIRITSFor several images of one product, add a separator and a suffix. The separator
is configurable in Settings and defaults to -:
Images/WH001-1.jpg
Images/WH001-2.jpgThe 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 image | about 60 KB on disk, 150 ms to process |
| A 5,000-product catalogue | about 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:
orders_export_20260809_143207_T3F9.csv
customers_export_20260809_143207_T3F9.csvThe 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:
NEW-3F2A8B1CCreate 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:
| Symptom | Cause |
|---|---|
| “Skipped file - No active profile found” | The filename is not one of the names in section 2 |
| Rows imported but fields empty | Column names do not match; check the field reference |
| Every row rejected | Usually a header row that has been renamed or removed |
| Dates a day or a month out | Ambiguous 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 appearing | Filename 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:
Products_*.csv:ItemCode,Description,PriceRRP,StockQty. That alone gives reps a working catalogue.Customers_*.csv:AccountId,CompanyName, plus the billing address columns. Now they can take orders.- Collect
Outbound. Now the orders reach you. - Images. The single biggest difference to how the app feels in front of a buyer.
- Everything else (price lists, promotions, tax rules, quantity breaks) as and when.