Test mode
Build against a sandbox catalogue with test keys, a seeded sample catalogue and sample events, without touching live data.
Every organisation has two catalogues: its live catalogue and a sandbox. A test key
(fo_test_… or fo_rk_test_…) reaches only the sandbox; a live key only the live catalogue.
The API, the change feed, the stream and webhooks behave the same in both: only the data
differs.
| Live mode | Test mode | |
|---|---|---|
| Keys | fo_live_…, fo_rk_live_… | fo_test_…, fo_rk_test_… |
| Catalogue | Your live catalogue, and the network catalogues your licences cover | Your sandbox catalogue only |
| Objects and events | livemode: true | livemode: false |
| Webhook endpoints | Receive live events only; URLs must be HTTPS | Receive test events only |
POST /v1/test/events | 403 test_mode_required | Emits a sample event |
| Monthly quotas | Capped by your plan; usage.threshold_reached at 80% and 100% | Counted and shown, never capped |
| Bulk exports | Your live catalogue; three open at once | Your sandbox; three open at once, apart from live |
| Rate budgets | Your organisation's live budgets | Separate budgets of the same size |
Nothing crosses between modes: a test key cannot read a live product, a test webhook endpoint
never receives a live event, and a live product's ID answers 404 to a test key. Test usage is
metered apart from live, so GET /v1/usage with a test key shows the
sandbox's figures, every quota is null, and testing never uses up a live quota.
Choosing the mode with a signed-in token
An API key's mode is fixed by its prefix. An OAuth access token (yours from furniops login, the
dashboard's, or an organisation's machine client's) has no mode of its own: it acts in live mode,
or in test mode when the request says so with the FurniOps-Mode header:
curl https://api.furniops.com/v1/products \
-H "Authorization: Bearer $FURNIOPS_ACCESS_TOKEN" \
-H "FurniOps-Mode: test"| Credential | Without the header | FurniOps-Mode: test | FurniOps-Mode: live |
|---|---|---|---|
| Access token | Live | Test | Live |
| Test key | Test | Test | 400 invalid_request |
| Live key | Live | 400 invalid_request | Live |
With the SDK, set mode on the client or on one call:
// An OAuth access token acts in live mode unless asked: here every call acts in test mode…
const sandbox = new FurniOps({ apiKey: accessToken, mode: "test" });
const sample = await sandbox.products.list({ limit: 10 });
// …and this one call in live mode.
const live = await sandbox.products.list({ limit: 10 }, { mode: "live" });
console.log(sample.data.length, live.data.length);A key named with the other mode is refused rather than ignored, so a request never acts in a mode
you did not mean. Any value but live or test is refused too. The SDK's mode option and the
CLI's --mode flag (or FURNIOPS_MODE) send the header for you.
The sample catalogue
Shortly after an organisation is created, FurniOps seeds its sandbox with a sample catalogue so there is something to read on day one:
- a sample source (
kind: "platform_feed", a Shopify-style storefront feed) with no schedule, so it never runs again on its own; - 23 products in 7 families, imported through the real pipeline (fetch, normalise, matching,
master records), so every
product.created,variant.createdandoffer.createdevent is a real one withlivemode: false; - one deliberate near-duplicate (the same product with a misspelt title), so matching has a review pair to show you.
The data is synthetic: no images, no real vendor's content. It is seeded once; deleting the sample source does not bring it back. Start a run of the sample source to watch a run end to end:
furniops sources run src_… # or POST /v1/sources/{id}/runs with a test keySample events
POST /v1/test/events emits a sample event of any public type in your sandbox, as if the change
had happened: it reaches the test webhook endpoints subscribed to that type, the sandbox's change
feed and the stream. Its objects are samples that do not exist, so do not read them back.
curl https://api.furniops.com/v1/test/events \
-H "Authorization: Bearer $FURNIOPS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"type": "offer.changed"}'It answers 202 with the event it emitted. With the SDK:
// Test mode only: a live key answers 403 test_mode_required.
const event = await furniops.test.triggerEvent({ type: "offer.changed" });
console.log(event.id, event.type, event.livemode); // livemode: falseOr with the CLI, which also explains a test_mode_required answer:
furniops trigger offer.changedThe key needs the webhooks:write scope. Every public event type can be triggered:
| Type | Sent when |
|---|---|
| source.degraded | A connection to a vendor's data |
| source.recovered | A connection to a vendor's data |
| source.blocked | A connection to a vendor's data |
| product.created | A master product's change: what moved, and to what |
| product.updated | A master product's change: what moved, and to what |
| product.discontinued | A master product's change: what moved, and to what |
| product.published | A master product's change: what moved, and to what |
| product.withdrawn | A master product's change: what moved, and to what |
| product.merged | A master product merged into another: it now redirects to `merged_into` |
| product.unmerged | A merged master product is its own again: every field, and the survivor it left |
| variant.created | A master variant's change: what moved, and to what |
| variant.updated | A master variant's change: what moved, and to what |
| variant.merged | A master variant merged into another: it now redirects to `merged_into` |
| variant.unmerged | A merged master variant is its own again: every field, and the survivor it left |
| offer.created | A source's offer changed: price, stock or availability |
| offer.changed | A source's offer changed: price, stock or availability |
| catalogue.verified | Whether a vendor catalogue is verified by its brand |
| catalogue.unverified | Whether a vendor catalogue is verified by its brand |
Testing webhooks on your machine
A webhook endpoint's URL must resolve to a public address: FurniOps refuses localhost and
private ranges, in both modes. To receive events on your laptop, use
furniops listen: it holds the event stream for you and POSTs each event to a
local URL, signed exactly as real deliveries are. No tunnel is needed.
furniops login --with-api-key # paste a fo_test_ key
furniops listen --forward-to localhost:3000/webhooks
furniops trigger offer.changed # in another terminalGoing live
When your integration works in test mode:
- Create a live key, and store it where your test key was.
- Create your webhook endpoints again in live mode: endpoints, their secrets and their deliveries belong to one mode. Live URLs must be HTTPS.
- Start your change feed sync from scratch: cursors from the sandbox mean nothing in live mode.