Integration Testing
Integration testing is a way to check that your integration consumes the Partners API correctly. Working through the scenarios is a structured way to surface bugs in your own integration - pagination gaps, missed version updates, mishandled substitutions or payments - against known, scripted data, before they show up in your live operation.
Each scenario exercises one behaviour of the API. Which scenarios are relevant depends on how your integration uses it - for example, if you only ingest completed sales the lifecycle/cancellation case matters less, and if you don't reconcile payments those scenarios won't apply. Work through the ones that match how your integration actually behaves.
This page describes the scenarios on offer and how each is checked.
New here? The reference client is a complete, runnable example that consumes the API correctly and can run these scenarios against the test environment in one command.
How it works
The suite is split into two tracks, because the two kinds of test are checked in different ways.
Track 1 - Connection and sync. These check that your integration talks to the API correctly: it authenticates, reads the order stream in order, remembers its place between runs, never processes the same order twice, and backs off when rate-limited. We check these for you - you don't submit anything. As your integration runs through the scenarios, the requests it makes pass through Scoffable, and we confirm from our side that they follow the rules. There's nothing to screenshot or send us; we just need you to run each scenario.
Track 2 - Order handling. These check that your integration does the right thing with an order once it has received it - for example voiding a cancelled order, recording the item that was actually delivered after a substitution, or reconciling a split payment. The result of this lands in your own system (your till, accounting platform, etc.), which we can't see - so for these you show us the result: process the scenario and share your system's resulting state (for example a screenshot or an export). Each scenario lists the expected outcome so you know what "correct" looks like.
Getting set up
1. Request a test token
These tests run against a test environment that returns scripted orders instead of real data. Access requires a test token, which is separate from your live token and cannot read live data.
To obtain one, contact Scoffable. Test tokens are issued manually and are time-limited; if yours expires, contact us again for a replacement.
2. Point your client at a scenario
Each scenario has a descriptive id (e.g. test-rate-limit, test-substitutions). You run a scenario by
using its id as a path segment in front of the normal endpoint path, on the test host:
GET https://test-partners-api.scoffable.com/test-pagination/v1/orderUpdates
Authorization: Bearer YOUR_TEST_TOKEN
Everything after the scenario segment works exactly as in production - the same endpoints, response
shape, pagination and order structure. In practice you only
change two things: the base URL (to https://test-partners-api.scoffable.com/test-pagination) and the
token.
Some scenarios simulate a failure partway through the stream - a rate-limit or a transient server error
on a page request after the first - and then succeed when you retry the same request. These
exercise your retry and back-off logic, so retry exactly as you would in production (waiting any
Retry-After the response asks for).
The test environment returns canned data. Order ids, vendor ids, barcodes and amounts are
illustrative and do not correspond to real orders. Monetary amounts are integer minor units (e.g. 2930
= £29.30) in the currency given in the payload. Timestamps are generated relative to the current time so
the data always falls within the supported window. For the same reason, the time-window query parameters
minAgeMinutes and fromTimestamp are accepted but ignored here - each scenario always serves its full
canned stream.
3. Re-running a scenario
The stateful scenarios (rate limiting and transient errors) remember how far through the fault you are,
per token - so the failure fires once, partway through, rather than on every request. To run one again
from the start, begin a fresh stream: make a request with no pageId, and the scenario restarts. If
you simply stop, the state also clears itself after a few minutes of inactivity. Every other scenario is
stateless, so re-running is just requesting from the beginning again.
Track 1 - Connection and sync
We verify these from the requests your integration makes - run each scenario and let your integration behave exactly as it would in production.
test-authentication - Authentication
- Simulates: a normal
200order stream (data/hasMore/nextPageId). - We check: you send the token in the
Authorizationheader (never the URL or logs) and parse the200.
test-invalid-token - Rejected token
- Simulates:
401 {"message": "Unauthorized"}on every request - as if the token were missing, expired or invalid. Your test token is valid; this scenario401s regardless, so you can test handling without crafting a broken token. - We check: your system stops and reports an error rather than retrying in a tight loop. (Test tokens are issued manually, so there's nothing to re-authenticate against - surfacing the failure is the correct response.)
test-pagination - Reading the stream
- Simulates: a large, multi-page backlog with an opaque
nextPageIdcursor. - We check that you:
- start with no
pageIdand followhasMoreto the end; - persist
nextPageIdand resume from it (even after a restart); - never skip or double-process an order, even if a page repeats;
- keep
pageSizewithin 1-100 and poll at a sensible cadence.
- start with no
test-rate-limit - Rate limiting
- Simulates: a mid-stream page request returns
429+Retry-After; the same request succeeds once you've waited (too soon →429again). - We check: you honour
Retry-After, retry the same request, resume with no gap, and back off on repeats. (Rate Limits)
test-transient-error - Transient errors
- Simulates: a mid-stream page request returns a transient
5xx; the same request succeeds on retry. - We check: you retry the failed request from your saved cursor - no gap, no full restart.
test-expired-page-id - Expired page id
- Simulates: your first request (no
pageId) returns a normal page, but thenextPageIdit gives you is already expired - so your next request (the first one carrying thatpageId) returns404 {"message": "pageId not found"}. - We check: you recognise the
404and restart cleanly from the beginning (a request with nopageId) rather than failing or treating it as fatal. (Pagination) - Implementation note: this test re-expires the cursor on every restart, so a client that restarts unconditionally would loop against it. As a defensive measure you may want to cap consecutive restarts and alert if the cap is hit (the reference client uses a
MAX_RESTARTSguard). In production this is belt-and-braces - a cursor that keeps expiring straight after a clean restart would imply a bug, so you'd rarely hit the cap.
test-empty-stream - No orders yet
- Simulates: a normal
200response with an empty stream -{ "data": [], "hasMore": false }and nonextPageId. This is the first-run state of a freshly-connected integration, or any vendor with no orders in the window. - We check: you treat an empty page as "caught up, zero orders" - finishing cleanly rather than erroring, retrying, or assuming at least one order is present.
Track 2 - Order handling
For each scenario, process the order(s) as you would in production and show us the resulting state of your system (e.g. a screenshot or export). Each scenario states the expected outcome.
test-order-lifecycle - Order status and lifecycle
- Simulates: an order delivered across three successive polls -
placed(poll 1) →accepted(poll 2) →cancelled(poll 3), each version the order's current latest at its poll - plus, in the final poll, arejected, anaccepted, and aplaced-only order. - Expected outcome: record the sale when you see the order
accepted, and reverse it when a later poll shows itcancelled; the standaloneacceptedorder also counts as a sale;rejectedandplaced-only never do. Because each transition arrives aslatestVersion: true, a client that skips superseded versions still sees every one.
test-versioning - Versioning
- Simulates: one order as four versions (
placed→accepted→accepted→accepted/delivered), the money changing between them - delivered across three successive polls: poll 1 returns v1, poll 2 returns the next two versions (v2, v3) that have appeared since, poll 3 returns v4. The latest version at each poll carrieslatestVersion: true. - Expected outcome: persist
nextPageIdbetween polls and resume from it, so each poll picks up the new versions that have arrived; they converge to a single, current record (not four); you end on the latest amount; reprocessing an applied version is idempotent. (Order Versioning) - Skipping superseded versions:
latestVersionis a flag on the whole order, not on the page -trueonly on the version that is the order's current latest at the moment it's served, andfalseon any a newer one has already superseded. Poll 2 hands you v2 (latestVersion: false- v3 had already superseded it) and v3 (latestVersion: true). If you only track an order's current state, you can skip everylatestVersion: falseversion and apply only thetrueones: the stream still delivers each order's current-latest version, so you converge to the same result with fewer downstream writes - here you write v3 (then v4) and skip v2, avoiding writing v2's £34 only to immediately correct it to v3's £17. The reference client does this by default; run it withVERBOSE=1to seeskip order 200 v2logged. Applying every version in order instead is equally correct - it just does the extra write.
test-out-of-order-versions - Out-of-order versions
- Simulates: the same order delivered newer-version-first - the
acceptedv2 arrives before the olderplacedv1. - Expected outcome: you keep the newer version and ignore the late-arriving older one - the order does not revert to
placed. Compare byversion(or trustlatestVersion), don't blindly apply whatever arrives last. (Order Versioning) - Note: in production an order's versions always arrive in order (oldest first), so you should never actually see this. The scenario delivers them out of order deliberately, to check your handling stays defensive if a stale version ever did arrive late.
test-items-and-options - Items, options and quantities
- Simulates: a customised product (one option with a premium surcharge), an
offerline, and abarcodeon packaged products (the composite set meal has none). The first version is the full order; a later version has one item out of stock, so itsquantityFulfilleddrops belowquantityOrdered(and the order total with it). - Expected outcome: correct name/quantity/price per line; the fulfilled quantity used where it differs; premium surcharges included; the
offertreated as a discount, not a product; products mapped bybarcodewhere present, and handled gracefully where it's absent.
test-substitutions - Substitutions
- Simulates: the substitution emerging across versions - a first version is the vanilla order as placed, then a later version shows each out-of-stock original (now
quantityFulfilled: 0) replaced by the delivered substitute, cross-linked viasubstitutionDetails(a one-to-one and a many-to-one). - Expected outcome: you re-read the later version and record the items actually delivered (the substitutes), not the originals, linked correctly. (Substitutions)
test-price-adjustments - Price adjustments
- Simulates: across versions - a vanilla order, then a later version where the item is out of stock and substituted with a dearer one, plus a linked negative
adjustmentthat price-matches it back down to the original price. - Expected outcome: apply the adjustment to the right item (the customer pays the original price); treat it as a correction, not a product. (Price Adjustments)
test-vouchers - Vouchers and promotions
- Simulates: one order with both kinds of voucher at once - a vendor-funded
10% off promotionas a negative-priced item (−£2.40), and a £5.00 Scoffable voucher incustomerPayments(collectedBy: scoffable) alongside £16.60 cash. The two act in different places:total = Family Feast 2400 + voucher item (−240) = 2160
payments = Scoffable voucher 500 + cash 1660 = 2160 ✓ reconciles - Expected outcome: the voucher item reduces the order total (takings); the Scoffable voucher payment is money received toward that total - the vendor still gets full value, so it must not be netted off as well (doing so double-counts it). (Vouchers and Promotions)
test-payments - Payments and reconciliation
- Simulates: two orders covering the common payment shapes - one split across collectors (vendor cash + Scoffable voucher) and one paid online (card via Scoffable) partly offset by a Scoffable voucher. Each carries a
deliveryFeeandserviceFee, and its payments add up tototal. - Expected outcome: each payment recorded with its
collectedBy(scoffable/vendor); every order's payments add up tototal, including the fees. (Customer Payments)
test-delivery - Delivery and collection
- Simulates: a
collectionorder, an Uberdelivery(hasdeliveredAt), and a vendordelivery(nodeliveredAt). - Expected outcome: collection isn't treated as delivery; provider differences handled; you don't block on a
deliveredAtthat may never arrive. (Delivery Information)
test-multi-location - Multiple locations
- Simulates: two orders from two different vendor locations.
- Expected outcome: each order is routed to the correct vendor by
vendorId- orders from different locations don't get merged or mis-attributed.
Working through the scenarios
Work through the scenarios relevant to your integration. For the Track 1 scenarios you run, we confirm from the requests we observe that they follow the rules; for the Track 2 scenarios, compare your system's resulting state against the expected outcome listed for each. Anything that doesn't line up is a bug worth fixing before it reaches your live operation.
If you have any questions, contact Scoffable.