Files
Price lists and catalogues as files. Fetch a file from a URL, upload one through a presigned URL, or let vendors email files to the source's own address, with sender checks and an inbox log.
A file source reads a vendor's spreadsheet or export: CSV (and tab-separated text), XLSX,
XLS, JSON or XML. The file can reach FurniOps three ways:
| Way | Set up | Runs start |
|---|---|---|
| From a URL | config.url | On the schedule, or when you ask. |
| Uploaded | No URL | When you complete an upload. |
| By email | config.inbound | When a vendor's email with an attachment arrives. |
Whichever way it comes, a file goes through the same run, and its columns are
read through the source's mapping. Set config.format when the content does
not tell the format apart (a .txt that is really CSV). A file larger than FurniOps' snapshot
limit fails as too_large. A column of shipping boxes has a format of its own, described in
Cartons.
From a URL
// A vendor's price list at a URL, read every 6 hours.
const pricelist = await furniops.sources.create({
name: "Acme price list",
kind: "file",
config: { url: "https://vendor.example/feeds/pricelist.csv", format: "csv", role: "supplier" },
schedule: { interval_minutes: 360 },
licence_basis: "vendor_agreement",
});
console.log(pricelist.id, pricelist.status); // "src_…", "active"FurniOps fetches only public http and https addresses: a URL that points at a private or
local network is refused, the run fails as fetch_refused, and the source is
blocked until the URL changes.
Upload a file
Uploading takes three calls, and the file itself never passes through the API:
Start the upload
POST /v1/sources/{id}/uploads with the file's name, type
and exact size in bytes. The answer's target is a presigned URL, valid for 15 minutes, with
the method and the headers to send.
Send the bytes
PUT the file to target.url with exactly the headers given: the same Content-Type and
Content-Length you declared.
Complete it
POST /v1/uploads/{id}/complete. FurniOps checks that
the bytes arrived at the declared size and queues a run that reads them; the answer is the run.
// A file you have, for a `file` source without a URL.
const bytes = await readFile("pricelist.xlsx");
const upload = await furniops.sources.createUpload("src_034XqBaL2mRLTZPXyRbaUc", {
filename: "pricelist.xlsx",
content_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
bytes: bytes.byteLength,
});
// PUT the bytes to the presigned URL, with exactly the headers given, within 15 minutes.
const target = upload.target;
if (target === null) throw new Error("no upload target");
const put = await fetch(target.url, {
method: target.method,
headers: target.headers,
body: bytes,
});
if (!put.ok) throw new Error(`upload failed: ${put.status}`);
// Then complete it: FurniOps checks the bytes arrived and queues a run that reads them.
const reading = await furniops.uploads.complete(upload.id);
console.log(reading.id, reading.status); // "run_…", "queued"| Type | content_type |
|---|---|
| CSV, tab-separated, plain text | text/csv, text/tab-separated-values, text/plain |
| Excel | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (XLSX), application/vnd.ms-excel (XLS) |
| JSON | application/json |
| XML | application/xml, text/xml |
- An upload is
pendinguntil completed, thencompletedwith itsrun. One never completed in time becomesexpired. - Only
filesources take uploads; other kinds answer422, as does a file larger than the snapshot limit. - Missing or different bytes answer
422on completion. An expired or already completed upload, a source with a run under way, or a paused or disabled source answer409. - The bytes are checked again when captured: the declared type is not trusted on its own.
The dashboard's Uploads page is not available yet; upload through the API or the SDK.
Receive files by email
Vendors who send price lists by email can send them straight to the source. Set config.inbound
with the senders allowed:
{
"config": {
"format": "xlsx",
"inbound": { "senders": ["[email protected]", "@vendor.example"] }
}
}The source then gets its own address, inbound_address. The part before the @ is a random
26-character token, unique to the source and kept for its life unless you rotate it: give the
address only to the vendor.
The email inbox may be off
inbound_address is null while FurniOps' email inbox is not switched on. A source can already be
set up for email; its address appears when the inbox is on.
FurniOps takes an email only when all of these hold:
- The sender is authenticated. The message passed DMARC, or carries a DKIM signature aligned with the From domain, as the mail provider reports. FurniOps never trusts the From header alone.
- The sender is allowed. The From address is on the source's list, as a whole address or as
@domainfor anyone at that domain. Up to 50 entries, each listed once. - The source is active (or degraded).
- It has an attachment in the source's format (any supported format when
config.formatis unset).
Each attachment that qualifies starts its own run; the message body and inline parts are ignored. A source runs one run at a time, so a second attachment waits for the first run to end.
The inbox log
GET /v1/sources/{id}/inbound-messages lists what
arrived, newest first: the sender, when it was received, what FurniOps did, and the run each
attachment started.
outcome | Meaning |
|---|---|
accepted | At least one attachment started a run. |
refused | FurniOps would not take it; reason_code says why. |
failed | It could not be read. |
pending | Still being handled. |
reason_code | Why it was refused |
|---|---|
not_inbound | The source does not take files by email. |
unauthenticated | It passed neither DMARC nor aligned DKIM. |
sender_not_allowed | The sender is not on the source's list. |
source_inactive | The source is paused, disabled or blocked. |
no_attachment | It had no attachment in the source's format. |
Each attachment shows started (with its run), skipped (not in the source's format) or failed
(it could not be fetched or stored). Reasons are FurniOps' own words: nothing the sender wrote is
echoed back.
A leaked address
POST /v1/sources/{id}/inbound-address/rotate
gives the source a new address at once; mail to the old one is refused from then on. Send the new
one to your vendor.
Connectors
The vendor data FurniOps reads today beyond files. A Shopify storefront's public feed, and Coaster's dealer API with its separate stock stream. Setup, schedules, credentials and the limits each one keeps.
Mappings
How a file's columns become product, variant and offer fields. FurniOps proposes a mapping for every new set of headers, a person confirms it once, and every later file with those headers imports on its own. Fields, settings, tiers and confirming through the API.