FurniOpsDocs

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:

WaySet upRuns start
From a URLconfig.urlOn the schedule, or when you ask.
UploadedNo URLWhen you complete an upload.
By emailconfig.inboundWhen 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

sources.ts
// 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"
Typecontent_type
CSV, tab-separated, plain texttext/csv, text/tab-separated-values, text/plain
Excelapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet (XLSX), application/vnd.ms-excel (XLS)
JSONapplication/json
XMLapplication/xml, text/xml
  • An upload is pending until completed, then completed with its run. One never completed in time becomes expired.
  • Only file sources take uploads; other kinds answer 422, as does a file larger than the snapshot limit.
  • Missing or different bytes answer 422 on completion. An expired or already completed upload, a source with a run under way, or a paused or disabled source answer 409.
  • 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:

  1. 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.
  2. The sender is allowed. The From address is on the source's list, as a whole address or as @domain for anyone at that domain. Up to 50 entries, each listed once.
  3. The source is active (or degraded).
  4. It has an attachment in the source's format (any supported format when config.format is 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.

outcomeMeaning
acceptedAt least one attachment started a run.
refusedFurniOps would not take it; reason_code says why.
failedIt could not be read.
pendingStill being handled.
reason_codeWhy it was refused
not_inboundThe source does not take files by email.
unauthenticatedIt passed neither DMARC nor aligned DKIM.
sender_not_allowedThe sender is not on the source's list.
source_inactiveThe source is paused, disabled or blocked.
no_attachmentIt 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.

On this page